Skip to content

Commit

Permalink
Improve questions layouting (#8574)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
OlgaLarina and OlgaLarina authored Jul 19, 2024
1 parent 094f0e5 commit d30f80f
Show file tree
Hide file tree
Showing 20 changed files with 1,264 additions and 67 deletions.
6 changes: 6 additions & 0 deletions src/base-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -96,6 +97,7 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner {
value: any,
displayValue: string
): string;
gridLayoutEnabled: boolean;
isDisplayMode: boolean;
isDesignMode: boolean;
areInvisibleElementsShowing: boolean;
Expand Down Expand Up @@ -282,6 +284,7 @@ export interface IElement extends IConditionRunner, ISurveyElement {
isCollapsed: boolean;
rightIndent: number;
startWithNewLine: boolean;
colSpan?: number;
registerPropertyChangedHandlers(propertyNames: Array<string>, handler: any, key: string): void;
registerFunctionOnPropertyValueChanged(name: string, func: any, key: string): void;
unRegisterFunctionOnPropertyValueChanged(name: string, key: string): void;
Expand All @@ -295,6 +298,7 @@ export interface IElement extends IConditionRunner, ISurveyElement {
clearErrors(): any;
dispose(): void;
needResponsiveWidth(): boolean;
updateRootStyle(): void;
}

export interface IQuestion extends IElement, ISurveyErrorOwner {
Expand Down Expand Up @@ -327,6 +331,8 @@ export interface IPanel extends ISurveyElement, IParentElement {
getQuestionTitleWidth(): string;
getQuestionStartIndex(): string;
getQuestionErrorLocation(): string;
getColumsForElement(el: IElement): Array<PanelLayoutColumnModel>;
updateColumns(): void;
parent: IPanel;
elementWidthChanged(el: IElement): any;
indexOf(el: IElement): number;
Expand Down
5 changes: 3 additions & 2 deletions src/jsonobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface IPropertyDecoratorOptions<T = any> {
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(
Expand Down Expand Up @@ -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);
}
},
});
Expand Down
36 changes: 36 additions & 0 deletions src/panel-layout-column.ts
Original file line number Diff line number Diff line change
@@ -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()
);
162 changes: 154 additions & 8 deletions src/panel.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -295,6 +296,11 @@ export class PanelModelBase extends SurveyElement<Question>
private elementsValue: Array<IElement>;
private isQuestionsReady: boolean = false;
private questionsValue: Array<Question> = new Array<Question>();
private _columns: Array<PanelLayoutColumnModel> = undefined;
private _columnsReady = false;

@propertyArray() layoutColumns: Array<PanelLayoutColumnModel>;

addElementCallback: (element: IElement) => void;
removeElementCallback: (element: IElement) => void;
onGetQuestionTitleLocation: () => string;
Expand All @@ -314,7 +320,8 @@ export class PanelModelBase extends SurveyElement<Question>
isAnimationEnabled: () => this.animationAllowed,
getAnimatedElement: (row: QuestionRowModel) => row.getRootElement(),
getLeaveOptions: (_: QuestionRowModel) => {
return { cssClass: this.cssClasses.rowFadeOut,
return {
cssClass: this.cssClasses.rowFadeOut,
onBeforeRunAnimation: beforeRunAnimation
};
},
Expand Down Expand Up @@ -397,6 +404,9 @@ export class PanelModelBase extends SurveyElement<Question>
this.updateDescriptionVisibility(this.description);
this.markQuestionListDirty();
this.onRowsChanged();
this.layoutColumns.forEach(col => {
col.onPropertyValueChangedCallback = this.onColumnPropertyValueChangedCallback;
});
}

@property({ defaultValue: true }) showTitle: boolean;
Expand Down Expand Up @@ -1095,6 +1105,61 @@ export class PanelModelBase extends SurveyElement<Question>
}
}
}
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<PanelLayoutColumnModel>): 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();
Expand Down Expand Up @@ -1154,6 +1219,65 @@ export class PanelModelBase extends SurveyElement<Question>
getQuestionTitleWidth(): string {
return this.questionTitleWidth || this.parent && this.parent.getQuestionTitleWidth();
}
public get columns(): Array<PanelLayoutColumnModel> {
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<PanelLayoutColumnModel> {
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;
Expand Down Expand Up @@ -1230,6 +1354,7 @@ export class PanelModelBase extends SurveyElement<Question>
if (this.isLoadingFromJson) return;
this.blockAnimations();
this.setArrayPropertyDirectly("rows", this.buildRows());
this.updateColumns();
this.releaseAnimations();
}

Expand Down Expand Up @@ -1409,10 +1534,8 @@ export class PanelModelBase extends SurveyElement<Question>
}
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,
Expand Down Expand Up @@ -1611,6 +1734,7 @@ export class PanelModelBase extends SurveyElement<Question>
if(this.wasRendered) {
element.onFirstRendering();
}
this.updateColumns();
return true;
}
public insertElement(element: IElement, dest?: IElement, location: "bottom" | "top" | "left" | "right" = "bottom"): void {
Expand Down Expand Up @@ -1721,6 +1845,7 @@ export class PanelModelBase extends SurveyElement<Question>
return false;
}
this.elements.splice(index, 1);
this.updateColumns();
return true;
}
public removeQuestion(question: Question) {
Expand Down Expand Up @@ -1812,6 +1937,16 @@ export class PanelModelBase extends SurveyElement<Question>
return new CssClassBuilder().append(cssClasses.error.root).toString();
}

public getSerializableColumnsValue(): Array<PanelLayoutColumnModel> {
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) {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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" },
{
Expand Down Expand Up @@ -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 },
{
Expand Down
Loading

0 comments on commit d30f80f

Please sign in to comment.