diff --git a/packages/survey-angular-ui/src/questions/text.component.html b/packages/survey-angular-ui/src/questions/text.component.html index 68c8cafd51..f03cd056e0 100644 --- a/packages/survey-angular-ui/src/questions/text.component.html +++ b/packages/survey-angular-ui/src/questions/text.component.html @@ -8,7 +8,7 @@
{{ value }}
any): any { const res = this.getPropertyValueWithoutDefault(name); if (this.isPropertyEmpty(res)) { const locStr = this.localizableStrings ? this.localizableStrings[name] : undefined; if (locStr) return locStr.text; if (defaultValue !== null && defaultValue !== undefined) return defaultValue; + if(!!calcFunc) { + const newVal = calcFunc(); + if(newVal !== undefined) { + this.setPropertyValueDirectly(name, newVal); + return newVal; + } + } const propDefaultValue = this.getDefaultPropertyValue(name); if (propDefaultValue !== undefined) return propDefaultValue; } diff --git a/packages/survey-core/src/dropdownListModel.ts b/packages/survey-core/src/dropdownListModel.ts index 9ea4070f24..327ede6f57 100644 --- a/packages/survey-core/src/dropdownListModel.ts +++ b/packages/survey-core/src/dropdownListModel.ts @@ -39,7 +39,6 @@ export class DropdownListModel extends Base { } private itemsSettings: { skip: number, take: number, totalCount: number, items: any[] } = { skip: 0, take: 0, totalCount: 0, items: [] }; protected listModel: ListModel; - protected popupCssClasses = "sv-single-select-list"; protected listModelFilterStringChanged = (newValue: string) => { if (this.filterString !== newValue) { this.filterString = newValue; @@ -134,7 +133,6 @@ export class DropdownListModel extends Base { this.listModel.registerPropertyChangedHandlers(["showFilter"], () => { this.updatePopupFocusFirstInputSelector(); }); - this._popupModel.cssClass = this.popupCssClasses; this._popupModel.onVisibilityChanged.add((_, option: { isVisible: boolean }) => { if (option.isVisible) { this.listModel.renderElements = true; @@ -251,8 +249,9 @@ export class DropdownListModel extends Base { model.isAllDataLoaded = !this.question.choicesLazyLoadEnabled; model.actions.forEach(a => a.disableTabStop = true); } + protected getPopupCssClasses(): string { return "sv-single-select-list"; } public updateCssClasses(popupCssClass: string, listCssClasses: any): void { - this.popupModel.cssClass = new CssClassBuilder().append(popupCssClass).append(this.popupCssClasses).toString(); + this.popupModel.cssClass = new CssClassBuilder().append(popupCssClass).append(this.getPopupCssClasses()).toString(); this.listModel.cssClasses = listCssClasses; } protected resetFilterString(): void { @@ -413,7 +412,6 @@ export class DropdownListModel extends Base { constructor(protected question: Question, protected onSelectionChanged?: (item: IAction, ...params: any[]) => void) { super(); this.htmlCleanerElement = DomDocumentHelper.createElement("div") as HTMLDivElement; - this.question.ariaExpanded = "false"; question.onPropertyChanged.add(this.questionPropertyChangedHandler); this.showInputFieldComponent = this.question.showInputFieldComponent; @@ -424,6 +422,8 @@ export class DropdownListModel extends Base { this.setTextWrapEnabled(this.question.textWrapEnabled); this.createPopup(); this.resetItemsSettings(); + const classes = question.cssClasses; + this.updateCssClasses(classes.popup, classes.list); } get popupModel(): PopupModel { diff --git a/packages/survey-core/src/dropdownMultiSelectListModel.ts b/packages/survey-core/src/dropdownMultiSelectListModel.ts index 5b79ef68da..2e35397511 100644 --- a/packages/survey-core/src/dropdownMultiSelectListModel.ts +++ b/packages/survey-core/src/dropdownMultiSelectListModel.ts @@ -9,8 +9,6 @@ import { settings } from "./settings"; import { IsTouch } from "./utils/devices"; export class DropdownMultiSelectListModel extends DropdownListModel { - protected popupCssClasses = "sv-multi-select-list"; - @property({ defaultValue: "" }) filterStringPlaceholder: string; @property({ defaultValue: true }) closeOnSelect: boolean; @@ -41,6 +39,7 @@ export class DropdownMultiSelectListModel extends DropdownListModel { return super.getFocusFirstInputSelector(); } } + protected getPopupCssClasses(): string { return "sv-multi-select-list"; } protected createListModel(): MultiSelectListModel { const visibleItems = this.getAvailableItems(); let _onSelectionChanged = this.onSelectionChanged; @@ -70,7 +69,6 @@ export class DropdownMultiSelectListModel extends DropdownListModel { elementId: this.listElementId }; const res = new MultiSelectListModel(listOptions); - res.actions.forEach(a => a.disableTabStop = true); this.setOnTextSearchCallbackForListModel(res); res.forceShowFilter = true; return res; diff --git a/packages/survey-core/src/jsonobject.ts b/packages/survey-core/src/jsonobject.ts index f7244a1419..dfe715a659 100644 --- a/packages/survey-core/src/jsonobject.ts +++ b/packages/survey-core/src/jsonobject.ts @@ -86,9 +86,11 @@ 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, prevValue); + if(newValue !== prevValue) { + this.setPropertyValue(key, newValue); + if (!!options && options.onSet) { + options.onSet(newValue, this, prevValue); + } } }, }); diff --git a/packages/survey-core/src/list.ts b/packages/survey-core/src/list.ts index 0168699a34..cfa2b12b4a 100644 --- a/packages/survey-core/src/list.ts +++ b/packages/survey-core/src/list.ts @@ -120,6 +120,9 @@ export class ListModel extends ActionContainer if (!!this.onFilterStringChangedCallback) { this.onFilterStringChangedCallback(text); } + this.updateIsEmpty(); + } + private updateIsEmpty(): void { this.isEmpty = this.renderedActions.filter(action => this.isItemVisible(action)).length === 0; } private scrollToItem(selector: string, ms = 0): void { @@ -160,6 +163,7 @@ export class ListModel extends ActionContainer (this as any)[key] = options[key]; } }); + this.updateActionsIds(); } else { this.setItems(items as Array); this.selectedItem = selectedItem; @@ -173,13 +177,16 @@ export class ListModel extends ActionContainer } public setItems(items: Array, sortByVisibleIndex = true): void { super.setItems(items, sortByVisibleIndex); - if (this.elementId) { - this.renderedActions.forEach((action: IAction) => { action.elementId = this.elementId + action.id; }); - } + this.updateActionsIds(); if (!this.isAllDataLoaded && !!this.actions.length) { this.actions.push(this.loadingIndicator); } } + private updateActionsIds(): void { + if (this.elementId) { + this.renderedActions.forEach((action: IAction) => { action.elementId = this.elementId + action.id; }); + } + } public setSearchEnabled(newValue: boolean): void { this.searchEnabled = newValue; this.showSearchClearButton = newValue; @@ -316,7 +323,11 @@ export class ListModel extends ActionContainer } public onPointerDown(event: PointerEvent, item: any) { } public refresh(): void { // used in popup - this.filterString = ""; + if(this.filterString !== "") { + this.filterString = ""; + } else { + this.updateIsEmpty(); + } this.resetFocusedItem(); } public onClickSearchClearButton(event: any) { diff --git a/packages/survey-core/src/page.ts b/packages/survey-core/src/page.ts index 7059cf2eda..598c580d7a 100644 --- a/packages/survey-core/src/page.ts +++ b/packages/survey-core/src/page.ts @@ -108,10 +108,6 @@ export class PageModel extends PanelModelBase implements IPage { this.removeSelfFromList(this.survey.pages); } } - public onFirstRendering(): void { - if (this.wasShown) return; - super.onFirstRendering(); - } /** * The visible index of the page. It has values from 0 to visible page count - 1. * @see SurveyModel.visiblePages diff --git a/packages/survey-core/src/panel.ts b/packages/survey-core/src/panel.ts index f50de69d30..dcec2829e6 100644 --- a/packages/survey-core/src/panel.ts +++ b/packages/survey-core/src/panel.ts @@ -396,7 +396,7 @@ export class PanelModelBase extends SurveyElement } ); this.registerPropertyChangedHandlers(["title"], () => { - this.calcHasTextInTitle(); + this.resetHasTextInTitle(); }); this.dragDropPanelHelper = new DragDropPanelHelperV1(this); @@ -404,7 +404,8 @@ export class PanelModelBase extends SurveyElement public getType(): string { return "panelbase"; } - public setSurveyImpl(value: ISurveyImpl, isLight?: boolean) { + public setSurveyImpl(value: ISurveyImpl, isLight?: boolean): void { + //if(this.surveyImpl === value) return; TODO refactor this.blockAnimations(); super.setSurveyImpl(value, isLight); if (this.isDesignMode) this.onVisibleChanged(); @@ -425,11 +426,12 @@ export class PanelModelBase extends SurveyElement @property({ defaultValue: true }) showTitle: boolean; - @property({ defaultValue: false }) public hasTextInTitle: boolean; - protected calcHasTextInTitle(): void { - this.hasTextInTitle = !!this.title; + public get hasTextInTitle(): boolean { + return this.getPropertyValue("hasTextInTitle", undefined, (): boolean => !!this.title); + } + private resetHasTextInTitle(): void { + this.resetPropertyValue("hasTextInTitle"); } - get hasTitle(): boolean { return ( (this.canShowTitle(this.survey) && (this.hasTextInTitle || this.locTitle.textOrHtml.length > 0)) || @@ -1357,14 +1359,11 @@ export class PanelModelBase extends SurveyElement } this.onElementVisibilityChanged(this); this.releaseAnimations(); - this.calcHasTextInTitle(); } - public onFirstRendering(): void { - super.onFirstRendering(); - for (var i = 0; i < this.elements.length; i++) { - this.elements[i].onFirstRendering(); - } + protected onFirstRenderingCore(): void { + super.onFirstRenderingCore(); this.onRowsChanged(); + this.elements.forEach(el => el.onFirstRendering()); } public updateRows(): void { if (this.isLoadingFromJson) return; @@ -1759,22 +1758,20 @@ export class PanelModelBase extends SurveyElement protected getPanelStartIndex(index: number): number { return index; } - protected isContinueNumbering() { - return true; - } + protected isContinueNumbering(): boolean { return true; } public get isReadOnly(): boolean { var isParentReadOnly = !!this.parent && this.parent.isReadOnly; var isSurveyReadOnly = !!this.survey && this.survey.isDisplayMode; return this.readOnly || isParentReadOnly || isSurveyReadOnly; } - protected onReadOnlyChanged() { + protected onReadOnlyChanged(): void { for (var i = 0; i < this.elements.length; i++) { var el = (this.elements[i]); el.setPropertyValue("isReadOnly", el.isReadOnly); } super.onReadOnlyChanged(); } - public updateElementCss(reNew?: boolean) { + public updateElementCss(reNew?: boolean): void { super.updateElementCss(reNew); for (let i = 0; i < this.elements.length; i++) { const el = (this.elements[i]); @@ -2068,7 +2065,7 @@ export class PanelModel extends PanelModelBase implements IElement { } }); this.registerPropertyChangedHandlers( - ["indent", "innerIndent", "rightIndent"], () => { this.onIndentChanged(); }); + ["indent", "innerIndent", "rightIndent"], () => { this.resetIndents(); }); this.registerPropertyChangedHandlers(["colSpan"], () => { this.parent?.updateColumns(); }); } public getType(): string { @@ -2083,15 +2080,6 @@ export class PanelModel extends PanelModelBase implements IElement { } return super.getSurvey(live); } - onSurveyLoad() { - super.onSurveyLoad(); - this.onIndentChanged(); - } - protected onSetData() { - super.onSetData(); - this.onIndentChanged(); - this.calcHasTextInTitle(); - } public get isPanel(): boolean { return true; } @@ -2269,24 +2257,33 @@ export class PanelModel extends PanelModelBase implements IElement { this.setPropertyValue("allowAdaptiveActions", val); } get innerPaddingLeft(): string { - return this.getPropertyValue("innerPaddingLeft", ""); + const func = (): string => { + return this.getIndentSize(this.innerIndent); + }; + return this.getPropertyValue("innerPaddingLeft", undefined, func); } set innerPaddingLeft(val: string) { this.setPropertyValue("innerPaddingLeft", val); } - private onIndentChanged() { - if (!this.getSurvey()) return; - this.innerPaddingLeft = this.getIndentSize(this.innerIndent); - this.paddingLeft = this.getIndentSize(this.indent); - this.paddingRight = this.getIndentSize(this.rightIndent); + protected calcPaddingLeft(): string { + return this.getIndentSize(this.indent); } + protected calcPaddingRight(): string { + return this.getIndentSize(this.rightIndent); + } + protected resetIndents(): void { + this.resetPropertyValue("innerPaddingLeft"); + super.resetIndents(); + } + private getIndentSize(indent: number): string { + if(!this.survey) return undefined; if (indent < 1) return ""; var css = (this).survey["css"]; - if (!css || !css.question.indent) return ""; + if (!css || !css.question || !css.question.indent) return ""; return indent * css.question.indent + "px"; } - public clearOnDeletingContainer() { + public clearOnDeletingContainer(): void { this.elements.forEach((element) => { if (element instanceof Question || element instanceof PanelModel) { element.clearOnDeletingContainer(); diff --git a/packages/survey-core/src/question.ts b/packages/survey-core/src/question.ts index b364d10062..43b806d610 100644 --- a/packages/survey-core/src/question.ts +++ b/packages/survey-core/src/question.ts @@ -91,27 +91,7 @@ export class Question extends SurveyElement private isReadyValue: boolean = true; private commentElements: Array; private dependedQuestions: Array = []; - public commentTextAreaModel: TextAreaModel; - - private getCommentTextAreaOptions(): ITextArea { - const options: ITextArea = { - question: this, - id: () => this.commentId, - propertyName: "comment", - className: () => this.cssClasses.comment, - placeholder: () => this.renderedCommentPlaceholder, - isDisabledAttr: () => this.isInputReadOnly || false, - rows: () => this.commentAreaRows, - autoGrow: () => this.autoGrowComment, - maxLength: () => this.getOthersMaxLength(), - ariaRequired: () => this.a11y_input_ariaRequired, - ariaLabel: () => this.a11y_input_ariaLabel, - getTextValue: () => { return this.comment; }, - onTextAreaChange: (e) => { this.onCommentChange(e); }, - onTextAreaInput: (e) => { this.onCommentInput(e); }, - }; - return options; - } + private commentTextAreaModelValue: TextAreaModel; /** * An event that is raised when the question's ready state has changed (expressions are evaluated, choices are loaded from a web resource specified by the `choicesByUrl` property, etc.). @@ -162,7 +142,6 @@ export class Question extends SurveyElement this.createNewArray("validators", (validator: any) => { validator.errorOwner = this; }); - this.commentTextAreaModel = new TextAreaModel(this.getCommentTextAreaOptions()); this.addExpressionProperty("visibleIf", (obj: Base, res: any) => { this.visible = res === true; }); this.addExpressionProperty("enableIf", (obj: Base, res: any) => { this.readOnly = res === false; }); @@ -194,7 +173,7 @@ export class Question extends SurveyElement this.registerPropertyChangedHandlers( ["indent", "rightIndent"], () => { - this.onIndentChanged(); + this.resetIndents(); } ); @@ -225,7 +204,31 @@ export class Question extends SurveyElement this.locProcessedTitle.sharedData = locTitleValue; return locTitleValue; } - + public get commentTextAreaModel(): TextAreaModel { + if(!this.commentTextAreaModelValue) { + this.commentTextAreaModelValue = new TextAreaModel(this.getCommentTextAreaOptions()); + } + return this.commentTextAreaModelValue; + } + private getCommentTextAreaOptions(): ITextArea { + const options: ITextArea = { + question: this, + id: () => this.commentId, + propertyName: "comment", + className: () => this.cssClasses.comment, + placeholder: () => this.renderedCommentPlaceholder, + isDisabledAttr: () => this.isInputReadOnly || false, + rows: () => this.commentAreaRows, + autoGrow: () => this.autoGrowComment, + maxLength: () => this.getOthersMaxLength(), + ariaRequired: () => this.a11y_input_ariaRequired, + ariaLabel: () => this.a11y_input_ariaLabel, + getTextValue: () => { return this.comment; }, + onTextAreaChange: (e) => { this.onCommentChange(e); }, + onTextAreaInput: (e) => { this.onCommentInput(e); }, + }; + return options; + } public getSurvey(live: boolean = false): ISurvey { if (live) { return !!this.parent ? ((this.parent)).getSurvey(live) : null; @@ -645,7 +648,6 @@ export class Question extends SurveyElement if (isLight !== true) { this.runConditions(); } - this.calcRenderedCommentPlaceholder(); if (!this.visible) { this.updateIsVisibleProp(); } @@ -661,7 +663,9 @@ export class Question extends SurveyElement if (this.parent === val) return; this.removeFromParent(); this.setPropertyValue("parent", val); - this.updateQuestionCss(); + if(!!val) { + this.updateQuestionCss(); + } this.onParentChanged(); } protected onParentChanged(): void { } @@ -897,7 +901,7 @@ export class Question extends SurveyElement * @see comment * @see commentText */ - @property({ localizable: true, onSet: (val, target) => target.calcRenderedCommentPlaceholder() }) commentPlaceholder: string; + @property({ localizable: true, onSet: (val, target) => target.resetRenderedCommentPlaceholder() }) commentPlaceholder: string; public get commentPlaceHolder(): string { return this.commentPlaceholder; @@ -906,11 +910,13 @@ export class Question extends SurveyElement this.commentPlaceholder = newValue; } public get renderedCommentPlaceholder(): string { - return this.getPropertyValue("renderedCommentPlaceholder"); + const func = (): any => { + return !this.isReadOnly ? this.commentPlaceHolder : undefined; + }; + return this.getPropertyValue("renderedCommentPlaceholder", undefined, func); } - private calcRenderedCommentPlaceholder() { - const res = !this.isReadOnly ? this.commentPlaceHolder : undefined; - this.setPropertyValue("renderedCommentPlaceholder", res); + private resetRenderedCommentPlaceholder() { + this.resetPropertyValue("renderedCommentPlaceholder"); } public getAllErrors(): Array { return this.errors.slice(); @@ -933,7 +939,7 @@ export class Question extends SurveyElement } public localeChanged(): void { super.localeChanged(); - this.calcRenderedCommentPlaceholder(); + this.resetRenderedCommentPlaceholder(); if (!!this.localeChangedCallback) { this.localeChangedCallback(); } @@ -1181,7 +1187,7 @@ export class Question extends SurveyElement if (reNew) { this.updateQuestionCss(true); } - this.onIndentChanged(); + this.resetIndents(); } protected updateQuestionCss(reNew?: boolean): void { if ( @@ -1233,9 +1239,11 @@ export class Question extends SurveyElement public get renderCssRoot(): string { return this.cssClasses.root || undefined; } - private onIndentChanged() { - this.paddingLeft = this.getIndentSize(this.indent); - this.paddingRight = this.getIndentSize(this.rightIndent); + protected calcPaddingLeft(): string { + return this.getIndentSize(this.indent); + } + protected calcPaddingRight(): string { + return this.getIndentSize(this.rightIndent); } private getIndentSize(indent: number): string { if (indent < 1 || !this.getSurvey() || !this.cssClasses || !this.cssClasses.indent) return ""; @@ -1463,7 +1471,7 @@ export class Question extends SurveyElement this.clearErrors(); } this.updateQuestionCss(); - this.calcRenderedCommentPlaceholder(); + this.resetRenderedCommentPlaceholder(); } /** * A Boolean expression. If it evaluates to `false`, this question becomes read-only. @@ -1532,14 +1540,10 @@ export class Question extends SurveyElement if (this.isEmpty()) { this.initDataFromSurvey(); } - this.calcRenderedCommentPlaceholder(); - this.onIndentChanged(); } protected onSetData(): void { super.onSetData(); - if (!this.survey) return; - this.onIndentChanged(); - if(!this.isDesignMode) { + if(!this.isDesignMode && !!this.survey && !this.isLoadingFromJson) { this.initDataFromSurvey(); this.onSurveyValueChanged(this.value); this.updateValueWithDefaults(); @@ -1705,7 +1709,6 @@ export class Question extends SurveyElement protected clearValueIfInvisibleCore(reason: string): void { if (this.canClearValueAsInvisible(reason)) { this.clearValue(); - this.setValueChangedDirectly(undefined); } } /** diff --git a/packages/survey-core/src/question_baseselect.ts b/packages/survey-core/src/question_baseselect.ts index e8586a9ab3..280acfa8f3 100644 --- a/packages/survey-core/src/question_baseselect.ts +++ b/packages/survey-core/src/question_baseselect.ts @@ -14,7 +14,7 @@ import { settings } from "./settings"; import { SurveyElement } from "./survey-element"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { ITextArea, TextAreaModel } from "./utils/text-area"; -import { cleanHtmlElementAfterAnimation, mergeValues, prepareElementForVerticalAnimation, setPropertiesOnElementForAnimation } from "./utils/utils"; +import { cleanHtmlElementAfterAnimation, prepareElementForVerticalAnimation, setPropertiesOnElementForAnimation } from "./utils/utils"; import { AnimationGroup, IAnimationGroupConsumer } from "./utils/animation"; /** @@ -23,8 +23,8 @@ import { AnimationGroup, IAnimationGroupConsumer } from "./utils/animation"; export class QuestionSelectBase extends Question { public visibleChoicesChangedCallback: () => void; public loadedChoicesFromServerCallback: () => void; - public otherTextAreaModel: TextAreaModel; public renderedChoicesChangedCallback: () => void; + private otherTextAreaModelValue: TextAreaModel; private filteredChoicesValue: Array; private conditionChoicesVisibleIfRunner: ConditionRunner; private conditionChoicesEnableIfRunner: ConditionRunner; @@ -49,26 +49,6 @@ export class QuestionSelectBase extends Question { } }) protected selectedItemValues: any; - private getOtherTextAreaOptions(): ITextArea { - const options: ITextArea = { - question: this, - id: () => this.otherId, - propertyName: "otherValue", - className: () => this.cssClasses.other, - placeholder: () => this.otherPlaceholder, - isDisabledAttr: () => this.isInputReadOnly || false, - rows: () => this.commentAreaRows, - maxLength: () => this.getOthersMaxLength(), - autoGrow: () => this.survey && this.survey.autoGrowComment, - ariaRequired: () => this.ariaRequired || this.a11y_input_ariaRequired, - ariaLabel: () => this.ariaLabel || this.a11y_input_ariaLabel, - getTextValue: () => { return this.otherValue; }, - onTextAreaChange: (e) => { this.onOtherValueChange(e); }, - onTextAreaInput: (e) => { this.onOtherValueInput(e); }, - }; - return options; - } - constructor(name: string) { super(name); this.noneItemValue = this.createDefaultItem(settings.noneItemValue, "noneText", "noneItemText"); @@ -90,7 +70,6 @@ export class QuestionSelectBase extends Question { this.registerPropertyChangedHandlers(["hideIfChoicesEmpty"], () => { this.onVisibleChanged(); }); - this.otherTextAreaModel = new TextAreaModel(this.getOtherTextAreaOptions()); this.createNewArray("visibleChoices", () => this.updateRenderedChoices(), () => this.updateRenderedChoices()); this.setNewRestfulProperty(); var locOtherText = this.createLocalizableString("otherText", this.otherItemValue, true, "otherItemText"); @@ -126,6 +105,31 @@ export class QuestionSelectBase extends Question { q.removeDependedQuestion(this); } } + public get otherTextAreaModel(): TextAreaModel { + if(!this.otherTextAreaModelValue) { + this.otherTextAreaModelValue = new TextAreaModel(this.getOtherTextAreaOptions()); + } + return this.otherTextAreaModelValue; + } + private getOtherTextAreaOptions(): ITextArea { + const options: ITextArea = { + question: this, + id: () => this.otherId, + propertyName: "otherValue", + className: () => this.cssClasses.other, + placeholder: () => this.otherPlaceholder, + isDisabledAttr: () => this.isInputReadOnly || false, + rows: () => this.commentAreaRows, + maxLength: () => this.getOthersMaxLength(), + autoGrow: () => this.survey && this.survey.autoGrowComment, + ariaRequired: () => this.ariaRequired || this.a11y_input_ariaRequired, + ariaLabel: () => this.ariaLabel || this.a11y_input_ariaLabel, + getTextValue: () => { return this.otherValue; }, + onTextAreaChange: (e) => { this.onOtherValueChange(e); }, + onTextAreaInput: (e) => { this.onOtherValueInput(e); }, + }; + return options; + } protected resetDependedQuestion(): void { this.choicesFromQuestion = ""; } @@ -2019,7 +2023,7 @@ export class QuestionSelectBase extends Question { public get questionName() { return this.name + "_" + this.id; } - public getItemEnabled(item: ItemValue) { + public getItemEnabled(item: ItemValue): boolean { return !this.isDisabledAttr && item.isEnabled; } private focusOtherComment() { @@ -2049,22 +2053,6 @@ export class QuestionSelectBase extends Question { public set itemComponent(value: string) { this.setPropertyValue("itemComponent", value); } - protected updateCssClasses(res: any, css: any) { - super.updateCssClasses(res, css); - if (!!this.dropdownListModel) { - const listCssClasses = {}; - mergeValues(css.list, listCssClasses); - mergeValues(res.list, listCssClasses); - res["list"] = listCssClasses; - } - } - protected calcCssClasses(css: any): any { - const classes = super.calcCssClasses(css); - if (this.dropdownListModel) { - this.dropdownListModel.updateCssClasses(classes.popup, classes.list); - } - return classes; - } } /** * A base class for multiple-selection question types that can display choice items in multiple columns ([Checkbox](https://surveyjs.io/form-library/documentation/questioncheckboxmodel), [Radiogroup](https://surveyjs.io/form-library/documentation/questionradiogroupmodel), [Image Picker](https://surveyjs.io/form-library/documentation/questionimagepickermodel)). diff --git a/packages/survey-core/src/question_comment.ts b/packages/survey-core/src/question_comment.ts index 9213a1a125..77cdfe79dd 100644 --- a/packages/survey-core/src/question_comment.ts +++ b/packages/survey-core/src/question_comment.ts @@ -12,8 +12,17 @@ import { Helpers } from "./helpers"; */ export class QuestionCommentModel extends QuestionTextBase { private element: HTMLElement; - public textAreaModel: TextAreaModel; + private textAreaModelValue: TextAreaModel; + constructor(name: string) { + super(name); + } + public get textAreaModel(): TextAreaModel { + if(!this.textAreaModelValue) { + this.textAreaModelValue = new TextAreaModel(this.getTextAreaOptions()); + } + return this.textAreaModelValue; + } private getTextAreaOptions(): ITextArea { const _this = this; const updateQuestionValue = (newValue: any) => { @@ -49,12 +58,6 @@ export class QuestionCommentModel extends QuestionTextBase { }; return options; } - - constructor(name: string) { - super(name); - - this.textAreaModel = new TextAreaModel(this.getTextAreaOptions()); - } /** * Specifies the visible height of the comment area, measured in lines. * diff --git a/packages/survey-core/src/question_custom.ts b/packages/survey-core/src/question_custom.ts index 77d9849bad..dddb4e5966 100644 --- a/packages/survey-core/src/question_custom.ts +++ b/packages/survey-core/src/question_custom.ts @@ -570,12 +570,12 @@ export abstract class QuestionCustomModelBase extends Question ); } } - public onFirstRendering() { + protected onFirstRenderingCore(): void { + super.onFirstRenderingCore(); const el = this.getElement(); if (!!el) { el.onFirstRendering(); } - super.onFirstRendering(); } public onHidingContent(): void { super.onHidingContent(); diff --git a/packages/survey-core/src/question_dropdown.ts b/packages/survey-core/src/question_dropdown.ts index 4a492178f5..41bd7013de 100644 --- a/packages/survey-core/src/question_dropdown.ts +++ b/packages/survey-core/src/question_dropdown.ts @@ -8,6 +8,7 @@ import { PopupModel } from "./popup"; import { EventBase } from "./base"; import { DropdownListModel } from "./dropdownListModel"; import { settings } from "./settings"; +import { updateListCssValues } from "./utils/utils"; /** * A class that describes the Dropdown question type. @@ -34,6 +35,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { constructor(name: string) { super(name); + this.ariaExpanded = "false"; this.createLocalizableString("placeholder", this, false, true); this.createLocalizableString("clearCaption", this, false, true); this.registerPropertyChangedHandlers(["choicesMin", "choicesMax", "choicesStep"], () => { @@ -201,7 +203,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { */ @property({ onSet: (newValue: boolean, target: QuestionDropdownModel) => { - if (!!target.dropdownListModel) { + if (!!target.dropdownListModelValue) { target.dropdownListModel.setSearchEnabled(newValue); } } @@ -235,7 +237,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { */ @property({ onSet: (newValue: boolean, target: QuestionDropdownModel) => { - if (!!target.dropdownListModel) { + if (!!target.dropdownListModelValue) { target.dropdownListModel.setChoicesLazyLoadEnabled(newValue); } } @@ -257,6 +259,19 @@ export class QuestionDropdownModel extends QuestionSelectBase { .append(this.cssClasses.controlInputFieldComponent, !!this.inputFieldComponentName) .toString(); } + protected updateCssClasses(res: any, css: any): void { + super.updateCssClasses(res, css); + if (this.useDropdownList) { + updateListCssValues(res, css); + } + } + protected calcCssClasses(css: any): any { + const classes = super.calcCssClasses(css); + if (this.dropdownListModelValue) { + this.dropdownListModel.updateCssClasses(classes.popup, classes.list); + } + return classes; + } @property() suggestedItem: ItemValue; public get selectedItemLocText() { @@ -276,8 +291,9 @@ export class QuestionDropdownModel extends QuestionSelectBase { const item = this.selectedItem; return !!item ? item.text : ""; } + private get useDropdownList(): boolean { return this.renderAs !== "select"; } public get dropdownListModel(): DropdownListModel { - if (this.renderAs !== "select" && !this.dropdownListModelValue) { + if (this.useDropdownList && !this.dropdownListModelValue) { this.dropdownListModelValue = new DropdownListModel(this); } return this.dropdownListModelValue; @@ -286,7 +302,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { this.dropdownListModelValue = val; } public get popupModel(): PopupModel { - return this.dropdownListModel?.popupModel; + return this.dropdownListModel.popupModel; } public onOpened: EventBase = this.addEvent(); @@ -294,7 +310,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { this.onOpened.fire(this, { question: this, choices: this.choices }); } protected onSelectedItemValuesChangedHandler(newValue: any): void { - this.dropdownListModel?.setInputStringFromSelectedItem(newValue); + this.dropdownListModelValue?.setInputStringFromSelectedItem(newValue); super.onSelectedItemValuesChangedHandler(newValue); } protected hasUnknownValue( @@ -321,7 +337,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { protected onVisibleChoicesChanged(): void { super.onVisibleChoicesChanged(); - if (!this.isLoadingFromJson && this.popupModel) { + if (!!this.dropdownListModelValue) { this.dropdownListModel.updateItems(); } } @@ -335,7 +351,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { public clearValue(keepComment?: boolean): void { super.clearValue(keepComment); this.lastSelectedItemValue = null; - this.dropdownListModel?.clear(); + this.dropdownListModelValue?.clear(); } public afterRenderCore(el: any): void { @@ -359,11 +375,11 @@ export class QuestionDropdownModel extends QuestionSelectBase { } protected supportEmptyValidation(): boolean { return true; } protected onBlurCore(event: any): void { - this.dropdownListModel?.onBlur(event); + this.dropdownListModel.onBlur(event); super.onBlurCore(event); } protected onFocusCore(event: any): void { - this.dropdownListModel?.onFocus(event); + this.dropdownListModel.onFocus(event); super.onFocusCore(event); } @@ -371,6 +387,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { super.dispose(); if(!!this.dropdownListModelValue) { this.dropdownListModelValue.dispose(); + this.dropdownListModelValue = undefined; } } } diff --git a/packages/survey-core/src/question_file.ts b/packages/survey-core/src/question_file.ts index c37177a3b1..aeea73108a 100644 --- a/packages/survey-core/src/question_file.ts +++ b/packages/survey-core/src/question_file.ts @@ -654,7 +654,7 @@ export class QuestionFileModel extends QuestionFileModelBase { this.setPropertyValue("isPlayingVideo", show); } private updateCurrentMode(): void { - if (!this.isDesignMode) { + if (!this.isDesignMode && this.survey) { if (this.sourceType !== "file") { this.camera.hasCamera((res: boolean) => { this.setPropertyValue("currentMode", res && this.isDefaultV2Theme ? this.sourceType : "file"); @@ -830,9 +830,12 @@ export class QuestionFileModel extends QuestionFileModelBase { public canPreviewImage(fileItem: any): boolean { return this.allowImagesPreview && !!fileItem && this.isFileImage(fileItem); } + private prevLoadedPreviewValue: any; protected loadPreview(newValue: any): void { + if (this.showPreview && this.prevLoadedPreviewValue === newValue) return; this.previewValue.splice(0, this.previewValue.length); if (!this.showPreview || !newValue) return; + this.prevLoadedPreviewValue = newValue; var newValues = Array.isArray(newValue) ? newValue : !!newValue diff --git a/packages/survey-core/src/question_paneldynamic.ts b/packages/survey-core/src/question_paneldynamic.ts index 53a72aac28..30b03b5f3d 100644 --- a/packages/survey-core/src/question_paneldynamic.ts +++ b/packages/survey-core/src/question_paneldynamic.ts @@ -1837,21 +1837,21 @@ export class QuestionPanelDynamicModel extends Question private get canBuildPanels(): boolean { return !this.isLoadingFromJson && !this.useTemplatePanel; } - public onFirstRendering(): void { - super.onFirstRendering(); + protected onFirstRenderingCore(): void { + super.onFirstRenderingCore(); this.buildPanelsFirstTime(); this.template.onFirstRendering(); for (var i = 0; i < this.panelsCore.length; i++) { this.panelsCore[i].onFirstRendering(); } } - public localeChanged() { + public localeChanged(): void { super.localeChanged(); for (var i = 0; i < this.panelsCore.length; i++) { this.panelsCore[i].localeChanged(); } } - public runCondition(values: HashTable, properties: HashTable) { + public runCondition(values: HashTable, properties: HashTable): void { super.runCondition(values, properties); this.runPanelsCondition(this.panelsCore, values, properties); } @@ -2119,12 +2119,14 @@ export class QuestionPanelDynamicModel extends Question if (!this.isDesignMode && !this.isReadOnly && !this.isValueEmpty(panel.getValue())) { this.runPanelsCondition([panel], this.getDataFilteredValues(), this.getDataFilteredProperties()); } - panel.onFirstRendering(); var questions = panel.questions; for (var i = 0; i < questions.length; i++) { questions[i].setParentQuestion(this); } - panel.locStrsChanged(); + if(this.wasRendered) { + panel.onFirstRendering(); + panel.locStrsChanged(); + } panel.onGetFooterActionsCallback = () => { return this.getPanelActions(panel); }; @@ -2216,7 +2218,7 @@ export class QuestionPanelDynamicModel extends Question } protected onSetData(): void { super.onSetData(); - if (this.useTemplatePanel) { + if(!this.isLoadingFromJson && this.useTemplatePanel) { this.setTemplatePanelSurveyImpl(); this.rebuildPanels(); } diff --git a/packages/survey-core/src/question_rating.ts b/packages/survey-core/src/question_rating.ts index 09e7c3e4d3..983b021f84 100644 --- a/packages/survey-core/src/question_rating.ts +++ b/packages/survey-core/src/question_rating.ts @@ -7,7 +7,7 @@ import { settings } from "./settings"; import { getLocaleString } from "./surveyStrings"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { Base } from "./base"; -import { mergeValues } from "./utils/utils"; +import { updateListCssValues } from "./utils/utils"; import { DropdownListModel } from "./dropdownListModel"; import { SurveyModel } from "./survey"; import { ISurveyImpl } from "./base-interfaces"; @@ -869,7 +869,8 @@ export class QuestionRatingModel extends Question { } protected onBeforeSetCompactRenderer(): void { if (!this.dropdownListModelValue) { - this.dropdownListModel = new DropdownListModel(this); + this.dropdownListModelValue = new DropdownListModel(this); + this.ariaExpanded = "false"; } } protected getCompactRenderAs(): string { @@ -882,6 +883,7 @@ export class QuestionRatingModel extends Question { private dropdownListModelValue: DropdownListModel; public set dropdownListModel(val: DropdownListModel) { this.dropdownListModelValue = val; + this.ariaExpanded = !!val ? "false" : undefined; this.updateElementCss(); } public get dropdownListModel(): DropdownListModel { @@ -896,17 +898,12 @@ export class QuestionRatingModel extends Question { } protected updateCssClasses(res: any, css: any) { super.updateCssClasses(res, css); - if(!!this.dropdownListModel) { - const listCssClasses = {}; - mergeValues(css.list, listCssClasses); - mergeValues(res.list, listCssClasses); - res["list"] = listCssClasses; - } + updateListCssValues(res, css); } protected calcCssClasses(css: any): any { const classes = super.calcCssClasses(css); - if(this.dropdownListModel) { - this.dropdownListModel.updateCssClasses(classes.popup, classes.list); + if(this.dropdownListModelValue) { + this.dropdownListModelValue.updateCssClasses(classes.popup, classes.list); } return classes; } @@ -925,6 +922,7 @@ export class QuestionRatingModel extends Question { super.dispose(); if(!!this.dropdownListModelValue) { this.dropdownListModelValue.dispose(); + this.dropdownListModelValue = undefined; } } } diff --git a/packages/survey-core/src/question_tagbox.ts b/packages/survey-core/src/question_tagbox.ts index 7c5d7535bc..6be64a809b 100644 --- a/packages/survey-core/src/question_tagbox.ts +++ b/packages/survey-core/src/question_tagbox.ts @@ -8,6 +8,7 @@ import { DropdownMultiSelectListModel } from "./dropdownMultiSelectListModel"; import { EventBase } from "./base"; import { settings } from "./settings"; import { ItemValue } from "./itemvalue"; +import { updateListCssValues } from "./utils/utils"; /** * A class that describes the Multi-Select Dropdown (Tag Box) question type. @@ -15,12 +16,13 @@ import { ItemValue } from "./itemvalue"; * [View Demo](https://surveyjs.io/form-library/examples/how-to-create-multiselect-tag-box/ (linkStyle)) */ export class QuestionTagboxModel extends QuestionCheckboxModel { - dropdownListModel: DropdownMultiSelectListModel; + private dropdownListModelValue: DropdownMultiSelectListModel; private itemDisplayNameMap: { [key: string]: string} = {}; private deselectAllItemText: LocalizableString; constructor(name: string) { super(name); + this.ariaExpanded = "false"; this.createLocalizableString("placeholder", this, false, true); this.createLocalizableString("clearCaption", this, false, true); this.createLocalizableString("readOnlyText", this, true); @@ -33,7 +35,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { public locStrsChanged(): void { super.locStrsChanged(); this.updateReadOnlyText(); - this.dropdownListModel?.locStrsChanged(); + this.dropdownListModelValue?.locStrsChanged(); } private updateReadOnlyText(): void { this.readOnlyText = this.displayValue || this.placeholder; @@ -41,18 +43,14 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { protected getDefaultItemComponent(): string { return ""; } - public onSurveyLoad(): void { - super.onSurveyLoad(); - this.createDropdownListModel(); - } - protected onSetData(): void { - super.onSetData(); - this.createDropdownListModel(); - } - private createDropdownListModel(): void { - if (!this.dropdownListModel && !this.isLoadingFromJson) { - this.dropdownListModel = new DropdownMultiSelectListModel(this); + public get dropdownListModel(): DropdownMultiSelectListModel { + if (!this.dropdownListModelValue) { + this.dropdownListModelValue = new DropdownMultiSelectListModel(this); } + return this.dropdownListModelValue; + } + public set dropdownListModel(val: DropdownMultiSelectListModel) { + this.dropdownListModelValue = val; } /** * Specifies a comparison operation used to filter the drop-down list. Applies only if [`searchEnabled`](#searchEnabled) is `true`. @@ -74,7 +72,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { */ @property({ onSet: (newValue: boolean, target: QuestionTagboxModel) => { - if (!!target.dropdownListModel) { + if (!!target.dropdownListModelValue) { target.dropdownListModel.setSearchEnabled(newValue); } } @@ -85,7 +83,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { */ @property({ onSet: (newValue: boolean, target: QuestionTagboxModel) => { - if (!!target.dropdownListModel) { + if (!!target.dropdownListModelValue) { target.dropdownListModel.setHideSelectedItems(newValue); } } @@ -98,7 +96,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { */ @property({ onSet: (newValue: boolean, target: QuestionTagboxModel) => { - if (!!target.dropdownListModel) { + if (!!target.dropdownListModelValue) { target.dropdownListModel.setChoicesLazyLoadEnabled(newValue); } } @@ -156,7 +154,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { return "combobox"; } public get popupModel(): PopupModel { - return this.dropdownListModel?.popupModel; + return this.dropdownListModel.popupModel; } public getControlClass(): string { @@ -170,6 +168,17 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { .append(this.cssClasses.controlPreview, this.isPreviewStyle) .toString(); } + protected updateCssClasses(res: any, css: any): void { + super.updateCssClasses(res, css); + updateListCssValues(res, css); + } + protected calcCssClasses(css: any): any { + const classes = super.calcCssClasses(css); + if (this.dropdownListModelValue) { + this.dropdownListModel.updateCssClasses(classes.popup, classes.list); + } + return classes; + } public onOpened: EventBase = this.addEvent(); public onOpenedCallBack(): void { this.onOpened.fire(this, { question: this, choices: this.choices }); @@ -188,7 +197,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { protected onVisibleChoicesChanged(): void { super.onVisibleChoicesChanged(); - if (this.popupModel) { + if (!!this.dropdownListModelValue) { this.dropdownListModel.updateItems(); } } @@ -236,11 +245,11 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { } protected supportEmptyValidation(): boolean { return true; } protected onBlurCore(event: any): void { - this.dropdownListModel?.onBlur(event); + this.dropdownListModel.onBlur(event); super.onBlurCore(event); } protected onFocusCore(event: any): void { - this.dropdownListModel?.onFocus(event); + this.dropdownListModel.onFocus(event); super.onFocusCore(event); } protected allElementsSelected(): boolean { @@ -255,13 +264,14 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { public dispose(): void { super.dispose(); - if(!!this.dropdownListModel) { - this.dropdownListModel.dispose(); + if(!!this.dropdownListModelValue) { + this.dropdownListModelValue.dispose(); + this.dropdownListModelValue = undefined; } } public clearValue(keepComment?: boolean): void { super.clearValue(keepComment); - this.dropdownListModel?.clear(); + this.dropdownListModelValue?.clear(); } public get showClearButton(): boolean { return this.allowClear && !this.isEmpty() && (!this.isDesignMode || settings.supportCreatorV2); diff --git a/packages/survey-core/src/question_text.ts b/packages/survey-core/src/question_text.ts index 0eb1145ad4..aab4b7028e 100644 --- a/packages/survey-core/src/question_text.ts +++ b/packages/survey-core/src/question_text.ts @@ -134,7 +134,7 @@ export class QuestionTextModel extends QuestionTextBase { ); this.registerPropertyChangedHandlers(["inputType", "size"], () => { this.updateInputSize(); - this.calcRenderedPlaceholder(); + this.resetRenderedPlaceholder(); }); } protected isTextValue(): boolean { diff --git a/packages/survey-core/src/question_textbase.ts b/packages/survey-core/src/question_textbase.ts index bdbaf29c87..65c61ed8ee 100644 --- a/packages/survey-core/src/question_textbase.ts +++ b/packages/survey-core/src/question_textbase.ts @@ -46,7 +46,7 @@ export class QuestionTextBase extends Question { /** * A placeholder for the input field. */ - @property({ localizable: true, onSet: (val, target) => target.calcRenderedPlaceholder() }) + @property({ localizable: true, onSet: (val, target) => target.resetRenderedPlaceholder() }) public placeholder: string; public get placeHolder(): string { return this.placeholder; } public set placeHolder(val: string) { this.placeholder = val; } @@ -80,34 +80,22 @@ export class QuestionTextBase extends Question { return this.textUpdateMode == "onTyping"; } public get renderedPlaceholder(): string { - return this.getPropertyValue("renderedPlaceholder"); - } - protected setRenderedPlaceholder(val: string): void { - this.setPropertyValue("renderedPlaceholder", val); + const func = (): string => { + return this.hasPlaceholder() ? this.placeHolder : undefined; + }; + return this.getPropertyValue("renderedPlaceholder", undefined, func); } protected onReadOnlyChanged(): void { super.onReadOnlyChanged(); - this.calcRenderedPlaceholder(); - } - public onSurveyLoad(): void { - this.calcRenderedPlaceholder(); - super.onSurveyLoad(); + this.resetRenderedPlaceholder(); } public localeChanged(): void { super.localeChanged(); - this.calcRenderedPlaceholder(); - } - public setSurveyImpl(value: ISurveyImpl, isLight?: boolean): void { - super.setSurveyImpl(value, isLight); - this.calcRenderedPlaceholder(); + this.resetRenderedPlaceholder(); } protected supportEmptyValidation(): boolean { return true; } - protected calcRenderedPlaceholder() { - let res = this.placeHolder; - if(!!res && !this.hasPlaceholder()) { - res = undefined; - } - this.setRenderedPlaceholder(res); + protected resetRenderedPlaceholder(): void { + this.resetPropertyValue("renderedPlaceholder"); } protected hasPlaceholder(): boolean { return !this.isReadOnly; diff --git a/packages/survey-core/src/survey-element.ts b/packages/survey-core/src/survey-element.ts index d23de3961d..092fe3d23f 100644 --- a/packages/survey-core/src/survey-element.ts +++ b/packages/survey-core/src/survey-element.ts @@ -522,7 +522,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey this.textProcessorValue = this.surveyImplValue.getTextProcessor(); this.onSetData(); } - if (!!this.survey) { + if (!!this.survey/* && !this.isLoadingFromJson*/) { this.updateDescriptionVisibility(this.description); this.clearCssClasses(); } @@ -625,18 +625,27 @@ export class SurveyElement extends SurveyElementCore implements ISurvey private get css(): any { return !!this.survey ? this.survey.getCss() : {}; } + private isCssValueCalculating: boolean; public get cssClassesValue(): any { - return this.getPropertyValueWithoutDefault("cssClassesValue"); - } - public set cssClassesValue(val: any) { - this.setPropertyValue("cssClassesValue", val); + let res = this.getPropertyValueWithoutDefault("cssClassesValue"); + if(!res && !this.isCssValueCalculating) { + this.isCssValueCalculating = true; + res = this.createCssClassesValue(); + this.isCssValueCalculating = false; + } + return res; } - private ensureCssClassesValue() { + private ensureCssClassesValue(): void { if (!this.cssClassesValue) { - this.cssClassesValue = this.calcCssClasses(this.css); - this.updateElementCssCore(this.cssClassesValue); + this.createCssClassesValue(); } } + private createCssClassesValue(): any { + const res = this.calcCssClasses(this.css); + this.setPropertyValue("cssClassesValue", res); + this.updateElementCssCore(this.cssClassesValue); + return res; + } /** * Returns an object in which keys are UI elements and values are CSS classes applied to them. * @@ -667,13 +676,13 @@ export class SurveyElement extends SurveyElementCore implements ISurvey return this.cssClasses.titleExpandableSvg; } protected calcCssClasses(css: any): any { return undefined; } - protected updateElementCssCore(cssClasses: any) { } + protected updateElementCssCore(cssClasses: any): void { } public get cssError(): string { return ""; } - public updateElementCss(reNew?: boolean) { + public updateElementCss(reNew?: boolean): void { this.clearCssClasses(); } - protected clearCssClasses() { - this.cssClassesValue = undefined; + protected clearCssClasses(): void { + this.resetPropertyValue("cssClassesValue"); } protected getIsLoadingFromJson(): boolean { if (super.getIsLoadingFromJson()) return true; @@ -751,7 +760,12 @@ export class SurveyElement extends SurveyElementCore implements ISurvey private wasRenderedValue: boolean; public get wasRendered(): boolean { return !!this.wasRenderedValue; } public onFirstRendering(): void { - this.wasRenderedValue = true; + if(!this.wasRendered) { + this.wasRenderedValue = true; + this.onFirstRenderingCore(); + } + } + protected onFirstRenderingCore(): void { this.ensureCssClassesValue(); } endLoadingFromJson(): void { @@ -829,7 +843,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey if (!html || !this.textProcessor) return html; return this.textProcessor.processText(html, true); } - protected onSetData() { } + protected onSetData(): void { } public get parent(): IPanel { return this.getPropertyValue("parent", null); } @@ -1006,16 +1020,20 @@ export class SurveyElement extends SurveyElementCore implements ISurvey return style; } get paddingLeft(): string { - return this.getPropertyValue("paddingLeft", ""); + return this.getPropertyValue("paddingLeft", undefined, () => this.calcPaddingLeft()); } - set paddingLeft(val: string) { - this.setPropertyValue("paddingLeft", val); + protected calcPaddingLeft(): string { + return ""; } get paddingRight(): string { - return this.getPropertyValue("paddingRight", ""); + return this.getPropertyValue("paddingRight", undefined, () => this.calcPaddingRight()); + } + protected calcPaddingRight(): string { + return ""; } - set paddingRight(val: string) { - this.setPropertyValue("paddingRight", val); + protected resetIndents(): void { + this.resetPropertyValue("paddingLeft"); + this.resetPropertyValue("paddingRight"); } @property({ defaultValue: true }) allowRootStyle: boolean; diff --git a/packages/survey-core/src/utils/utils.ts b/packages/survey-core/src/utils/utils.ts index 4bfd103474..bacadb9d89 100644 --- a/packages/survey-core/src/utils/utils.ts +++ b/packages/survey-core/src/utils/utils.ts @@ -573,7 +573,7 @@ export function sanitizeEditableContent(element: any, cleanLineBreaks: boolean = range.setStart(range.endContainer, range.endOffset); } } -function mergeValues(src: any, dest: any) { +function mergeValues(src: any, dest: any): void { if (!dest || !src) return; if (typeof dest !== "object") return; for (var key in src) { @@ -586,6 +586,12 @@ function mergeValues(src: any, dest: any) { } } } +function updateListCssValues(res: any, css: any): void { + const listCssClasses = {}; + mergeValues(css.list, listCssClasses); + mergeValues(res.list, listCssClasses); + res["list"] = listCssClasses; +} export class Logger { private _result = ""; @@ -803,6 +809,7 @@ export function roundTo2Decimals(number: number): number { export { mergeValues, + updateListCssValues, getElementWidth, isContainerVisible, classesToSelector, diff --git a/packages/survey-core/tests/dropdown_list_model_test.ts b/packages/survey-core/tests/dropdown_list_model_test.ts index 36dc71fe79..927067f38d 100644 --- a/packages/survey-core/tests/dropdown_list_model_test.ts +++ b/packages/survey-core/tests/dropdown_list_model_test.ts @@ -281,7 +281,7 @@ QUnit.test("Check list classes with onUpdateQuestionCssClasses", function (asser const question = survey.getAllQuestions()[0]; const dropdownListModel = question.dropdownListModel; question.dropdownListModel = dropdownListModel; - question.onFirstRendering(); + assert.ok(question.cssClassesValue); const list: ListModel = dropdownListModel.popupModel.contentComponentData.model as ListModel; assert.equal(list.cssClasses.item, "original-class custom-class"); assert.equal(list.cssClasses.itemSelected, "original-class-selected custom-class-selected"); diff --git a/packages/survey-core/tests/listModelTests.ts b/packages/survey-core/tests/listModelTests.ts index bbdfe2b953..1bd1ab1438 100644 --- a/packages/survey-core/tests/listModelTests.ts +++ b/packages/survey-core/tests/listModelTests.ts @@ -131,6 +131,23 @@ QUnit.test("ListModel custom onFilter", assert => { ListModel.MINELEMENTCOUNT = oldValueMINELEMENTCOUNT; }); +QUnit.test("ListModel: refresh & isEmpty", assert => { + const items = [ + new Action({ id: "test1", title: "test1" }), + new Action({ id: "test2", title: "test2" }) + ]; + const myObject = new MyObject(items); + const list = new ListModel({ items: items, onSelectionChanged: () => { }, allowSelection: true } as any); + assert.equal(list.isEmpty, false, "#1"); + list.actions[0].setVisible(false); + list.actions[1].setVisible(false); + list.refresh(); + assert.equal(list.isEmpty, true, "#2"); + list.actions[1].setVisible(true); + list.refresh(); + assert.equal(list.isEmpty, false, "#3"); +}); + QUnit.test("ListModel custom onFilter: item is not found when a search string contains a white space", assert => { ListModel.MINELEMENTCOUNT = 5; const items = [ diff --git a/packages/survey-core/tests/questionDropdownTests.ts b/packages/survey-core/tests/questionDropdownTests.ts index d9b077df43..f9478655d9 100644 --- a/packages/survey-core/tests/questionDropdownTests.ts +++ b/packages/survey-core/tests/questionDropdownTests.ts @@ -9,6 +9,7 @@ import { PopupBaseViewModel } from "../src/popup-view-model"; import { PopupDropdownViewModel } from "../src/popup-dropdown-view-model"; import { PopupModalViewModel } from "../src/popup-modal-view-model"; import { QuestionMatrixDynamicModel } from "../src/question_matrixdynamic"; +import { IAction } from "../src/actions/action"; export default QUnit.module("Dropdown question"); @@ -1051,6 +1052,25 @@ QUnit.test("itemsSettings property", assert => { done1(); }, onChoicesLazyLoadCallbackTimeOut + callbackTimeOutDelta); }); +QUnit.test("rendering actions id", assert => { + const json = { + questions: [{ + type: "dropdown", + name: "q1", + choices: ["Item1", "Item2"] + }] + }; + const survey = new SurveyModel(json); + const question = survey.getQuestionByName("q1"); + question.id = "el1"; + const listModel = question.popupModel.contentComponentData.model as ListModel; + const actions = listModel.renderedActions; + assert.equal(actions.length, 2, "two actions"); + assert.equal((actions[0]).elementId, "el1i_listItem1", "elementId, action1"); + assert.equal((actions[1]).elementId, "el1i_listItem2", "elementId, action2"); + assert.equal((actions[0]).disableTabStop, true, "disableTabStop, action1"); + assert.equal((actions[1]).disableTabStop, true, "disableTabStop, action2"); +}); QUnit.test("Test dropdown choices change should update strings", function (assert) { const json = { diff --git a/packages/survey-core/tests/questionFileTests.ts b/packages/survey-core/tests/questionFileTests.ts index 457b580d5a..342341e1c4 100644 --- a/packages/survey-core/tests/questionFileTests.ts +++ b/packages/survey-core/tests/questionFileTests.ts @@ -1416,10 +1416,10 @@ QUnit.test("QuestionFile current mode property, camera is not available", functi { type: "file", name: "q4", sourceType: "file-camera" }, ] }); - assert.equal(survey.getQuestionByName("q1").currentMode, "file"); - assert.equal(survey.getQuestionByName("q2").currentMode, "file"); - assert.equal(survey.getQuestionByName("q3").currentMode, "camera"); - assert.equal(survey.getQuestionByName("q4").currentMode, "file-camera"); + assert.equal(survey.getQuestionByName("q1").currentMode, "file", "#1.1"); + assert.equal(survey.getQuestionByName("q2").currentMode, "file", "#1.2"); + assert.equal(survey.getQuestionByName("q3").currentMode, "camera", "#1.3"); + assert.equal(survey.getQuestionByName("q4").currentMode, "file-camera", "#1.4"); assert.ok(callbacks.length > 0, "callbacks are set"); callbacks.forEach(cb => cb([])); assert.equal(survey.getQuestionByName("q1").currentMode, "file", "#1"); @@ -1459,10 +1459,10 @@ QUnit.test("QuestionFile current mode property, camera is available", function ( { type: "file", name: "q4", sourceType: "file-camera" }, ] }); - assert.equal(survey.getQuestionByName("q1").currentMode, "file"); - assert.equal(survey.getQuestionByName("q2").currentMode, "file"); - assert.equal(survey.getQuestionByName("q3").currentMode, "camera"); - assert.equal(survey.getQuestionByName("q4").currentMode, "file-camera"); + assert.equal(survey.getQuestionByName("q1").currentMode, "file", "1.1"); + assert.equal(survey.getQuestionByName("q2").currentMode, "file", "1.2"); + assert.equal(survey.getQuestionByName("q3").currentMode, "camera", "1.3"); + assert.equal(survey.getQuestionByName("q4").currentMode, "file-camera", "1.4"); assert.ok(callbacks.length > 0, "callbacks are set"); callbacks.forEach(cb => cb(devices)); diff --git a/packages/survey-core/tests/question_customtests.ts b/packages/survey-core/tests/question_customtests.ts index bf08c25d64..50f00b6616 100644 --- a/packages/survey-core/tests/question_customtests.ts +++ b/packages/survey-core/tests/question_customtests.ts @@ -1902,9 +1902,13 @@ QUnit.test("Check updateElementCss for custom question", function (assert) { elements: [{ type: "newquestion", name: "q1" }], }); const question = survey.getAllQuestions()[0]; + const css1 = question.cssClassesValue; + const css2 = question.contentQuestion.cssClassesValue; + assert.ok(css1); + assert.ok(css2); question.updateElementCss(); - assert.equal(question.cssClassesValue, undefined); - assert.equal(question.contentQuestion.cssClassesValue, undefined); + assert.notStrictEqual(question.cssClassesValue, css1); + assert.notStrictEqual(question.contentQuestion.cssClassesValue, css2); ComponentCollection.Instance.clear(); }); QUnit.test("onvalueChanging/Changed events", function (assert) { diff --git a/packages/survey-core/tests/question_paneldynamic_tests.ts b/packages/survey-core/tests/question_paneldynamic_tests.ts index 166f42bbdf..e3035085fb 100644 --- a/packages/survey-core/tests/question_paneldynamic_tests.ts +++ b/packages/survey-core/tests/question_paneldynamic_tests.ts @@ -4851,6 +4851,7 @@ QUnit.test("cssClasses for a question in nested panel dynamic, #2", function (as const nestedPanel = rootPanel.panels[0].getQuestionByName("panel2"); const panel = nestedPanel.panels[0]; const question = panel.getQuestionByName("q1"); + assert.equal(question.wasRendered, true, "question.onFirstRendering was called"); assert.ok(question.cssClassesValue.mainRoot, "Main root style is set"); assert.equal(panel.rows.length, 1, "There is one row"); const row = panel.rows[0]; diff --git a/packages/survey-core/tests/question_ratingtests.ts b/packages/survey-core/tests/question_ratingtests.ts index fd7edca97a..3fc7a9f4fb 100644 --- a/packages/survey-core/tests/question_ratingtests.ts +++ b/packages/survey-core/tests/question_ratingtests.ts @@ -343,7 +343,6 @@ QUnit.test("Check cssClasses update when dropdownListModel is set", (assert) => const dropdownListModel = new DropdownListModel(q1); const list: ListModel = dropdownListModel.popupModel.contentComponentData.model as ListModel; q1.dropdownListModel = dropdownListModel; - q1.cssClasses; assert.ok(dropdownListModel.popupModel.cssClass.includes("custom-popup-class")); assert.equal(list.cssClasses.item, "original-class custom-class"); assert.equal(list.cssClasses.itemSelected, "original-class-selected custom-class-selected"); diff --git a/packages/survey-core/tests/question_tagbox_tests.ts b/packages/survey-core/tests/question_tagbox_tests.ts index 5b8dd9ead4..430a66da9b 100644 --- a/packages/survey-core/tests/question_tagbox_tests.ts +++ b/packages/survey-core/tests/question_tagbox_tests.ts @@ -7,6 +7,7 @@ import { settings } from "../src/settings"; import { QuestionMatrixDynamicModel } from "../src/question_matrixdynamic"; import { ListModel } from "../src/list"; import { PageModel } from "../src/page"; +import { IAction } from "../src/actions/action"; export default QUnit.module("Tagbox question"); @@ -1622,9 +1623,10 @@ QUnit.test("Create tag box in the code, dropdownListModel instance", (assert) => const question = new QuestionTagboxModel("q1"); const page = new PageModel("page1"); page.addQuestion(question); - assert.notOk(question.dropdownListModel, "It is not created yet"); + assert.notOk(question["dropdownListModelValue"], "It is not created yet"); survey.addPage(page); - assert.ok(question.dropdownListModel, "It is created"); + assert.notOk(question["dropdownListModelValue"], "It is not created yet"); + assert.ok(question.dropdownListModel, "It is created on demand"); }); QUnit.test("Create tag box from json, dropdownListModel instance", (assert) => { const survey = new SurveyModel({ @@ -1818,4 +1820,43 @@ QUnit.test("The new selected value is always replaced with the the first selecte }, callbackTimeOutDelta); done1(); }, onChoicesLazyLoadCallbackTimeOut + callbackTimeOutDelta); -}); \ No newline at end of file +}); +QUnit.test("rendering actions id", assert => { + const json = { + questions: [{ + type: "tagbox", + name: "q1", + searchEnabled: false, + choices: ["Item1", "Item2"] + }] + }; + const survey = new SurveyModel(json); + const question = survey.getQuestionByName("q1"); + assert.notOk(question["dropdownListModelValue"], "It is not created yet"); + question.id = "el1"; + const listModel = question.popupModel.contentComponentData.model as ListModel; + const actions = listModel.renderedActions; + assert.equal(actions.length, 2, "two actions"); + assert.equal((actions[0]).elementId, "el1i_listItem1", "elementId, action1"); + assert.equal((actions[1]).elementId, "el1i_listItem2", "elementId, action2"); + assert.equal((actions[0]).disableTabStop, true, "disableTabStop, action1"); + assert.equal((actions[1]).disableTabStop, true, "disableTabStop, action2"); +}); +QUnit.test("List actions disableTabStop", assert => { + const json = { + questions: [{ + type: "tagbox", + name: "q1", + searchEnabled: true, + choices: ["Item1", "Item2"] + }] + }; + const survey = new SurveyModel(json); + const question = survey.getQuestionByName("q1"); + question.dropdownListModel.inputStringRendered = "o"; + const listModel = question.popupModel.contentComponentData.model as ListModel; + const actions = listModel.renderedActions; + assert.equal(actions.length, 2, "two actions"); + assert.equal((actions[0]).disableTabStop, true, "disableTabStop, action1"); + assert.equal((actions[1]).disableTabStop, true, "disableTabStop, action2"); +}); diff --git a/tests/markup/snapshots/dropdown-input-string.snap.html b/tests/markup/snapshots/dropdown-input-string.snap.html index 1b2b7dc017..608654e7cd 100644 --- a/tests/markup/snapshots/dropdown-input-string.snap.html +++ b/tests/markup/snapshots/dropdown-input-string.snap.html @@ -30,17 +30,17 @@
No data to display
    -
  • +
  • Ford
  • - -
  • +
  • Volkswagen
    diff --git a/tests/markup/snapshots/matrixdropdown-readonly.snap.html b/tests/markup/snapshots/matrixdropdown-readonly.snap.html index 103ece95ce..ba26a0c14b 100644 --- a/tests/markup/snapshots/matrixdropdown-readonly.snap.html +++ b/tests/markup/snapshots/matrixdropdown-readonly.snap.html @@ -21,12 +21,12 @@
    - +
    - +
    @@ -36,12 +36,12 @@
    - +
    - +
    diff --git a/tests/markup/snapshots/multipletext-readonly.snap.html b/tests/markup/snapshots/multipletext-readonly.snap.html index d9b936c3a6..e9fd445dda 100644 --- a/tests/markup/snapshots/multipletext-readonly.snap.html +++ b/tests/markup/snapshots/multipletext-readonly.snap.html @@ -7,7 +7,7 @@ Text 1
    - +
    @@ -17,7 +17,7 @@ Text 2
    - +
    @@ -29,7 +29,7 @@ Text 3
    - +
    @@ -39,7 +39,7 @@ Text 4
    - +
    diff --git a/tests/markup/snapshots/text-readonly.snap.html b/tests/markup/snapshots/text-readonly.snap.html index fefc6c25c7..c4ec1b8db3 100644 --- a/tests/markup/snapshots/text-readonly.snap.html +++ b/tests/markup/snapshots/text-readonly.snap.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file