diff --git a/docs/handle-survey-results-continue-incomplete.md b/docs/handle-survey-results-continue-incomplete.md index 03a3317dbf..fb50899716 100644 --- a/docs/handle-survey-results-continue-incomplete.md +++ b/docs/handle-survey-results-continue-incomplete.md @@ -1,8 +1,8 @@ # Continue an Incomplete Survey -Your respondents may not complete your survey in a single session. In this case, you can restore their answers from the previous session next time they get to the survey. Incomplete results can be loaded from your database or the browser's [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +Respondents may not complete your survey in a single session. In this case, you can restore their answers from the previous session next time they get to the survey. Incomplete results can be loaded from your database or the browser's [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). -To save incomplete results, enable the Survey's [`sendResultOnPageNext`](/Documentation/Library?id=surveymodel#sendResultOnPageNext) property. With this setting, the Survey raises the [`onPartialSend`](/Documentation/Library?id=surveymodel#onPartialSend) event each time a respondent navigates to the next survey page. Handle this event to send incomplete results to your database or `localStorage`: +To save incomplete results, implement a function that sends them to your server or saves them in the `localStorage`. Call this function within `SurveyModel`'s [`onValueChanged`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#onValueChanged) and [`onCurrentPageChanged`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#onCurrentPageChanged) event handlers to save survey results when users change a question value or switch between pages. If you use the `localStorage`, you also need to delete survey results from it when the survey is completed. Handle the [`onComplete`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#onComplete) event for this purpose. ```js import { Model } from "survey-core"; @@ -10,8 +10,6 @@ import { Model } from "survey-core"; const surveyJson = { ... }; const survey = new Model(surveyJson); -survey.sendResultOnPageNext = true; - const storageItemKey = "my-survey"; function saveSurveyData (survey) { @@ -20,13 +18,9 @@ function saveSurveyData (survey) { window.localStorage.setItem(storageItemKey, JSON.stringify(data)); } -// Save survey results -survey.onPartialSend.add((survey) => { - saveSurveyData(survey); -}); -survey.onComplete.add((survey) => { - saveSurveyData(survey); -}); +// Save survey results to the local storage +survey.onValueChanged.add(saveSurveyData); +survey.onCurrentPageChanged.add(saveSurveyData); // Restore survey results const prevData = window.localStorage.getItem(storageItemKey) || null; @@ -37,9 +31,14 @@ if (prevData) { survey.currentPageNo = data.pageNo; } } + +// Empty the local storage after the survey is completed +survey.onComplete.add(() => { + window.localStorage.setItem(storageItemKey, ""); +}); ``` -[View Demo](https://surveyjs.io/Examples/Library?id=real-patient-history (linkStyle)) +[View Demo](/form-library/examples/survey-editprevious/ (linkStyle)) ## See Also diff --git a/package.json b/package.json index fd89df41a7..214d1a0861 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.0", "ace-builds": "1.2.2", - "ajv": "4.11.2", + "ajv": "6.12.3", "axe-core": "^3.5.6", "axe-testcafe": "^3.0.0", "babel-loader": "8.2.5", diff --git a/packages/survey-angular-ui/example/src/testCafe/countriesMock.json b/packages/survey-angular-ui/example/src/testCafe/countriesMock.json index 441f17a454..f3a78ab119 100644 --- a/packages/survey-angular-ui/example/src/testCafe/countriesMock.json +++ b/packages/survey-angular-ui/example/src/testCafe/countriesMock.json @@ -1,9 +1,18 @@ { "RestResponse": { - "result": [ - { "alpha2_code": "US", "name": "Unated States" }, - { "alpha2_code": "CU", "name": "Cuba" }, - { "alpha2_code": "RO","name":"Romania" } - ] + "result": [ + { + "alpha2_code": "US", + "name": "United States" + }, + { + "alpha2_code": "CU", + "name": "Cuba" + }, + { + "alpha2_code": "RO", + "name": "Romania" + } + ] } } \ No newline at end of file diff --git a/packages/survey-angular-ui/src/angular-ui.module.ts b/packages/survey-angular-ui/src/angular-ui.module.ts index ddaae152cc..0c0259182c 100644 --- a/packages/survey-angular-ui/src/angular-ui.module.ts +++ b/packages/survey-angular-ui/src/angular-ui.module.ts @@ -110,6 +110,7 @@ import { PaneldynamicRemoveButtonComponent } from "./components/paneldynamic-act import { TimerPanelComponent } from "./components/timer-panel/timer-panel.component"; import { NotifierComponent } from "./components/notifier/notifier.component"; import { ComponentsContainerComponent } from "./components-container.component"; +import { MultipleTextRowComponent } from "./questions/multipletextrow.component"; @NgModule({ declarations: [ VisibleDirective, Key2ClickDirective, PanelDynamicAddBtn, PanelDynamicNextBtn, PanelDynamicPrevBtn, PanelDynamicProgressText, ElementComponent, TemplateRendererComponent, @@ -126,7 +127,7 @@ import { ComponentsContainerComponent } from "./components-container.component"; MultipleTextComponent, MultipleTextItemComponent, DynamicComponentDirective, RankingQuestionComponent, RankingItemComponent, PanelDynamicQuestionComponent, EmbeddedViewContentComponent, CustomWidgetComponent, MatrixCellComponent, MatrixTableComponent, MatrixDropdownComponent, MatrixDynamicComponent, MatrixDetailButtonComponent, MatrixDynamicRemoveButtonComponent, MatrixDynamicDragDropIconComponent, MatrixRequiredHeader, ExpressionComponent, SafeResourceUrlPipe, BrandInfoComponent, CustomQuestionComponent, CompositeQuestionComponent, ButtonGroupItemComponent, ButtonGroupQuestionComponent, MatrixRowComponent, ModalComponent, LogoImageComponent, SkeletonComponent, TimerPanelComponent, PaneldynamicRemoveButtonComponent, - NotifierComponent, ComponentsContainerComponent + NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent ], imports: [ CommonModule, FormsModule @@ -147,7 +148,7 @@ import { ComponentsContainerComponent } from "./components-container.component"; MultipleTextComponent, MultipleTextItemComponent, DynamicComponentDirective, RankingQuestionComponent, RankingItemComponent, PanelDynamicQuestionComponent, EmbeddedViewContentComponent, CustomWidgetComponent, MatrixCellComponent, MatrixTableComponent, MatrixDropdownComponent, MatrixDynamicComponent, MatrixDetailButtonComponent, MatrixDynamicRemoveButtonComponent, MatrixDynamicDragDropIconComponent, MatrixRequiredHeader, ExpressionComponent, SafeResourceUrlPipe, CustomQuestionComponent, CompositeQuestionComponent, ButtonGroupQuestionComponent, ModalComponent, LogoImageComponent, SkeletonComponent, TimerPanelComponent, PaneldynamicRemoveButtonComponent, - NotifierComponent, ComponentsContainerComponent + NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent ], providers: [PopupService], }) diff --git a/packages/survey-angular-ui/src/angular-ui.ts b/packages/survey-angular-ui/src/angular-ui.ts index 8afb655c7d..be7dd4a86c 100644 --- a/packages/survey-angular-ui/src/angular-ui.ts +++ b/packages/survey-angular-ui/src/angular-ui.ts @@ -75,8 +75,9 @@ export * from "./utils/safe-html.pipe"; export * from "./questions/comment.component"; export * from "./questions/signature.component"; export * from "./questions/multipletext.component"; -export * from "./errors.component"; export * from "./questions/multipletextitem.component"; +export * from "./questions/multipletextrow.component"; +export * from "./errors.component"; export * from "./utils/dynamic.directive"; export * from "./questions/ranking.component"; export * from "./questions/ranking-item.component"; diff --git a/packages/survey-angular-ui/src/errors.component.ts b/packages/survey-angular-ui/src/errors.component.ts index bbc9995f93..bd75abb063 100644 --- a/packages/survey-angular-ui/src/errors.component.ts +++ b/packages/survey-angular-ui/src/errors.component.ts @@ -1,26 +1,14 @@ -import { Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { Base, SurveyElement, TooltipManager } from "survey-core"; +import { Component, HostBinding, Input } from "@angular/core"; +import { SurveyElement } from "survey-core"; @Component({ templateUrl: "./errors.component.html", selector: "'[sv-ng-errors]'" }) -export class ErrorsComponent implements OnDestroy, OnInit { +export class ErrorsComponent { @Input() element!: SurveyElement | any; @Input() location?: String; - @ViewChild("errorsContainer", { static: true }) errorsContainerRef!: ElementRef; - private tooltipManager!: TooltipManager; - constructor(private viewContainerRef: ViewContainerRef) {} - ngOnInit(): void { - if (this.location == "tooltip") { - this.tooltipManager = new TooltipManager(this.viewContainerRef.element.nativeElement); - } - } - ngOnDestroy() { - if (!!this.tooltipManager) { - this.tooltipManager.dispose(); - } - } + constructor() {} @HostBinding("attr.role") get role (): string { return "alert"; } diff --git a/packages/survey-angular-ui/src/question.component.html b/packages/survey-angular-ui/src/question.component.html index 0d6b949527..e4ef63a0d7 100644 --- a/packages/survey-angular-ui/src/question.component.html +++ b/packages/survey-angular-ui/src/question.component.html @@ -11,7 +11,6 @@
-
diff --git a/packages/survey-angular-ui/src/questions/multipletext.component.html b/packages/survey-angular-ui/src/questions/multipletext.component.html index 4e3fb5ba5f..ffe31507fd 100644 --- a/packages/survey-angular-ui/src/questions/multipletext.component.html +++ b/packages/survey-angular-ui/src/questions/multipletext.component.html @@ -1,12 +1,7 @@ - - - - - + + + -
+ \ No newline at end of file diff --git a/packages/survey-angular-ui/src/questions/multipletext.component.ts b/packages/survey-angular-ui/src/questions/multipletext.component.ts index 5082edfcf3..51537b7582 100644 --- a/packages/survey-angular-ui/src/questions/multipletext.component.ts +++ b/packages/survey-angular-ui/src/questions/multipletext.component.ts @@ -11,9 +11,6 @@ export class MultipleTextComponent extends QuestionAngular { return this.model.inputId + "rowkey" + index; } - trackItemBy (_: number, item: MultipleTextItemModel): string { - return "item" + item.editor.id; - } } AngularComponentFactory.Instance.registerComponent("multipletext-question", MultipleTextComponent); \ No newline at end of file diff --git a/packages/survey-angular-ui/src/questions/multipletextitem.component.ts b/packages/survey-angular-ui/src/questions/multipletextitem.component.ts index b7546a1411..debd5a594b 100644 --- a/packages/survey-angular-ui/src/questions/multipletextitem.component.ts +++ b/packages/survey-angular-ui/src/questions/multipletextitem.component.ts @@ -1,15 +1,38 @@ -import { MultipleTextItemModel, QuestionMultipleTextModel, QuestionTextModel } from "survey-core"; -import { Component, Input } from "@angular/core"; +import { MultipleTextCell, MultipleTextItemModel, QuestionMultipleTextModel, QuestionTextModel } from "survey-core"; +import { Component, DoCheck, Input, OnDestroy } from "@angular/core"; import { BaseAngular } from "../base-angular"; @Component({ selector: "'[sv-ng-multipletext-item]'", templateUrl: "./mutlipletextitem.component.html" }) -export class MultipleTextItemComponent extends BaseAngular { +export class MultipleTextItemComponent extends BaseAngular implements DoCheck, OnDestroy { @Input() question!: QuestionMultipleTextModel; - @Input() model!: MultipleTextItemModel; + @Input() model!: MultipleTextCell; protected getModel(): QuestionTextModel { - return this.model.editor; + if(!this.model.isErrorsCell) { + return this.model.item.editor; + } + return null as any; + } + public get item(): MultipleTextItemModel { + return this.model.item; + } + public get editor(): QuestionTextModel { + return this.model.item.editor; + } + override ngDoCheck(): void { + super.ngDoCheck(); + if(this.model.isErrorsCell) { + this.editor.registerFunctionOnPropertyValueChanged("errors", () => { + this.update(); + }, "__ngSubscription") + } + } + override ngOnDestroy(): void { + super.ngOnDestroy(); + if(this.model.isErrorsCell) { + this.editor.unRegisterFunctionOnPropertyValueChanged("errors", "__ngSubscription") + } } } diff --git a/packages/survey-angular-ui/src/questions/multipletextrow.component.html b/packages/survey-angular-ui/src/questions/multipletextrow.component.html new file mode 100644 index 0000000000..559f5d9f53 --- /dev/null +++ b/packages/survey-angular-ui/src/questions/multipletextrow.component.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/survey-angular-ui/src/questions/multipletextrow.component.ts b/packages/survey-angular-ui/src/questions/multipletextrow.component.ts new file mode 100644 index 0000000000..60635d0af7 --- /dev/null +++ b/packages/survey-angular-ui/src/questions/multipletextrow.component.ts @@ -0,0 +1,19 @@ +import { MultipleTextCell, MultipleTextItemModel, MutlipleTextRow, QuestionMultipleTextModel, QuestionTextModel } from "survey-core"; +import { Component, Input } from "@angular/core"; +import { BaseAngular } from "../base-angular"; + +@Component({ + selector: "sv-ng-multipletext-row", + templateUrl: "./multipletextrow.component.html", + styleUrls: ["../hide-host.scss"] +}) +export class MultipleTextRowComponent extends BaseAngular { + @Input() question!: QuestionMultipleTextModel; + @Input() model!: MutlipleTextRow; + protected getModel(): MutlipleTextRow { + return this.model; + } + trackItemBy (_: number, cell: MultipleTextCell): string { + return "item" + cell.item.editor.id; + } +} diff --git a/packages/survey-angular-ui/src/questions/mutlipletextitem.component.html b/packages/survey-angular-ui/src/questions/mutlipletextitem.component.html index 87e792d974..f0b384279d 100644 --- a/packages/survey-angular-ui/src/questions/mutlipletextitem.component.html +++ b/packages/survey-angular-ui/src/questions/mutlipletextitem.component.html @@ -1,18 +1,18 @@ - \ No newline at end of file + + + + +
+
\ No newline at end of file diff --git a/packages/survey-angular-ui/src/survey-content.component.html b/packages/survey-angular-ui/src/survey-content.component.html index 7d2953f2bf..88f29440a1 100644 --- a/packages/survey-angular-ui/src/survey-content.component.html +++ b/packages/survey-angular-ui/src/survey-content.component.html @@ -26,9 +26,9 @@
-
-
+
{{model.emptySurveyText}}
diff --git a/packages/survey-vue3-ui/example/public/testCafe/countriesMock.json b/packages/survey-vue3-ui/example/public/testCafe/countriesMock.json index 441f17a454..f3a78ab119 100644 --- a/packages/survey-vue3-ui/example/public/testCafe/countriesMock.json +++ b/packages/survey-vue3-ui/example/public/testCafe/countriesMock.json @@ -1,9 +1,18 @@ { "RestResponse": { - "result": [ - { "alpha2_code": "US", "name": "Unated States" }, - { "alpha2_code": "CU", "name": "Cuba" }, - { "alpha2_code": "RO","name":"Romania" } - ] + "result": [ + { + "alpha2_code": "US", + "name": "United States" + }, + { + "alpha2_code": "CU", + "name": "Cuba" + }, + { + "alpha2_code": "RO", + "name": "Romania" + } + ] } } \ No newline at end of file diff --git a/packages/survey-vue3-ui/src/Element.vue b/packages/survey-vue3-ui/src/Element.vue index 78e9a58dc1..c752652329 100644 --- a/packages/survey-vue3-ui/src/Element.vue +++ b/packages/survey-vue3-ui/src/Element.vue @@ -53,11 +53,6 @@ :element="element" :location="'bottom'" /> -
diff --git a/packages/survey-vue3-ui/src/Multipletext.vue b/packages/survey-vue3-ui/src/Multipletext.vue index 9d92a175d9..43166b732c 100644 --- a/packages/survey-vue3-ui/src/Multipletext.vue +++ b/packages/survey-vue3-ui/src/Multipletext.vue @@ -1,15 +1,23 @@ diff --git a/packages/survey-vue3-ui/src/MultipletextItem.vue b/packages/survey-vue3-ui/src/MultipletextItem.vue index fbf9f8013d..0fd67ed3d6 100644 --- a/packages/survey-vue3-ui/src/MultipletextItem.vue +++ b/packages/survey-vue3-ui/src/MultipletextItem.vue @@ -1,5 +1,5 @@ diff --git a/packages/survey-vue3-ui/src/Survey.vue b/packages/survey-vue3-ui/src/Survey.vue index f770331a4b..ef3e733300 100644 --- a/packages/survey-vue3-ui/src/Survey.vue +++ b/packages/survey-vue3-ui/src/Survey.vue @@ -74,12 +74,12 @@
diff --git a/src/actions/action.ts b/src/actions/action.ts index 72cace2bd1..e38b93a266 100644 --- a/src/actions/action.ts +++ b/src/actions/action.ts @@ -163,7 +163,7 @@ export function createDropdownActionModelAdvanced(actionOptions: IAction, listOp const listModel: ListModel = new ListModel( listOptions.items, (item: Action) => { - listOptions.onSelectionChanged(item), + listOptions.onSelectionChanged(item); innerPopupModel.toggleVisibility(); }, listOptions.allowSelection, @@ -171,13 +171,15 @@ export function createDropdownActionModelAdvanced(actionOptions: IAction, listOp listOptions.onFilterStringChangedCallback ); listModel.locOwner = locOwner; - const innerPopupModel: PopupModel = new PopupModel("sv-list", { model: listModel }, popupOptions?.verticalPosition, popupOptions?.horizontalPosition, popupOptions?.showPointer, popupOptions?.isModal, popupOptions?.onCancel, popupOptions?.onApply, popupOptions?.onHide, popupOptions?.onShow, popupOptions?.cssClass, popupOptions?.title); + const innerPopupModel: PopupModel = new PopupModel("sv-list", { model: listModel }, popupOptions?.verticalPosition, popupOptions?.horizontalPosition, popupOptions?.showPointer, popupOptions?.isModal, popupOptions?.onCancel, popupOptions?.onApply, popupOptions?.onHide, popupOptions?.onShow, popupOptions?.cssClass, popupOptions?.title, () => { + listModel.dispose(); + }); innerPopupModel.displayMode = popupOptions?.displayMode as any; const newActionOptions = Object.assign({}, actionOptions, { component: "sv-action-bar-item-dropdown", popupModel: innerPopupModel, - action: (action:IAction, isUserAction: boolean) => { + action: (action: IAction, isUserAction: boolean) => { !!(actionOptions.action) && actionOptions.action(); innerPopupModel.isFocusedContent = !isUserAction || listModel.showFilter; innerPopupModel.toggleVisibility(); @@ -303,7 +305,7 @@ export abstract class BaseAction extends Base implements IAction { return this.tooltip || this.title; } public getIsTrusted(args: any): boolean { - if(!!args.originalEvent) { + if (!!args.originalEvent) { return args.originalEvent.isTrusted; } return args.isTrusted; @@ -445,6 +447,17 @@ export class Action extends BaseAction implements IAction, ILocalizableOwner { public getComponent(): string { return this._component; } + public dispose(): void { + this.action = undefined; + super.dispose(); + if (this.popupModel) { + this.popupModel.dispose(); + } + if (!!this.locTitleValue) { + this.locTitleValue.onStringChanged.remove(this.locTitleChanged); + this.locTitleChanged = undefined; + } + } } export class ActionDropdownViewModel { diff --git a/src/actions/adaptive-container.ts b/src/actions/adaptive-container.ts index 6391c8cc8f..25c88b0dc0 100644 --- a/src/actions/adaptive-container.ts +++ b/src/actions/adaptive-container.ts @@ -160,6 +160,8 @@ export class AdaptiveActionContainer extends ActionCo } public dispose(): void { super.dispose(); + this.dotsItem.data.dispose(); + this.dotsItem.dispose(); this.resetResponsivityManager(); } } \ No newline at end of file diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index 4b17406f7a..dd7831a90c 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -41,7 +41,10 @@ export interface ITextProcessor { export interface ISurveyErrorOwner extends ILocalizableOwner { getErrorCustomText(text: string, error: SurveyError): string; } - +export interface IValueItemCustomPropValues { + propertyName: string; + values: Array; +} export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { getSkeletonComponentName(element: ISurveyElement): string; currentPage: IPage; @@ -186,7 +189,7 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { canChangeChoiceItemsVisibility(): boolean; getChoiceItemVisibility(question: IQuestion, item: any, val: boolean): boolean; loadQuestionChoices(options: { question: IQuestion, filter: string, skip: number, take: number, setItems: (items: Array, totalCount: number) => void }): void; - getChoiceDisplayValue(options: { question: IQuestion, values: Array, setItems: (displayValues: Array) => void }): void; + getChoiceDisplayValue(options: { question: IQuestion, values: Array, setItems: (displayValues: Array, ...customValues: Array) => void }): void; matrixRowAdded(question: IQuestion, row: any): any; matrixColumnAdded(question: IQuestion, column: any): void; matrixBeforeRowAdded(options: { @@ -213,6 +216,7 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { dynamicPanelRemoved(question: IQuestion, panelIndex: number, panel: IPanel): void; dynamicPanelRemoving(question: IQuestion, panelIndex: number, panel: IPanel): boolean; dynamicPanelItemValueChanged(question: IQuestion, options: any): any; + dynamicPanelGetTabTitle(question: IQuestion, options: any): any; dragAndDropAllow(options: DragDropAllowEvent): boolean; diff --git a/src/base.ts b/src/base.ts index 30cd543ead..852280c7ea 100644 --- a/src/base.ts +++ b/src/base.ts @@ -141,6 +141,7 @@ export class Dependencies { this.dependencies.forEach(dependency => { dependency.obj.unregisterPropertyChangedHandlers([dependency.prop], dependency.id); }); + // this.currentDependency = undefined; } } @@ -191,6 +192,7 @@ export class Base { if (Base.currentDependencis === undefined) return; Base.currentDependencis.addDependency(target, property); } + public dependencies: {[key: string]: ComputedUpdater } = {}; public static get commentSuffix(): string { return settings.commentSuffix; } @@ -307,6 +309,7 @@ export class Base { } this.onPropertyValueChangedCallback = undefined; this.isDisposedValue = true; + Object.values(this.dependencies).forEach(dependencies => dependencies.dispose()); } public get isDisposed() { return this.isDisposedValue === true; @@ -490,7 +493,13 @@ export class Base { return this.getDefaultPropertyValue(name) !== undefined; } public resetPropertyValue(name: string): void { - this.setPropertyValue(name, undefined); + const locStr = this.localizableStrings ? this.localizableStrings[name] : undefined; + if(locStr) { + locStr.clearLocale(); + } + else { + this.setPropertyValue(name, undefined); + } } protected getPropertyValueWithoutDefault(name: string): any { return this.getPropertyValueCore(this.propertyHash, name); diff --git a/src/common-styles/sv-ranking.scss b/src/common-styles/sv-ranking.scss index cd8138b38e..7081249d7f 100644 --- a/src/common-styles/sv-ranking.scss +++ b/src/common-styles/sv-ranking.scss @@ -56,6 +56,16 @@ height: calcSize(4); } +.sv-ranking-item--disabled.sv-ranking-item--disabled { + cursor: initial; + + .sv-ranking-item__icon-container.sv-ranking-item__icon-container { + .sv-ranking-item__icon.sv-ranking-item__icon { + visibility: hidden; + } + } +} + .sv-ranking-item__icon.sv-ranking-item__icon { visibility: hidden; fill: $primary; diff --git a/src/defaultCss/defaultV2Css.ts b/src/defaultCss/defaultV2Css.ts index ef0e7e5026..a742cfc38b 100644 --- a/src/defaultCss/defaultV2Css.ts +++ b/src/defaultCss/defaultV2Css.ts @@ -35,6 +35,7 @@ export var defaultV2Css = { clockTimerMinorText: "sd-timer__text--minor", clockTimerMajorText: "sd-timer__text--major", bodyEmpty: "sd-body sd-body--empty", + bodyLoading: "sd-body--loading", footer: "sd-footer sd-body__navigation sd-clearfix", title: "sd-title", description: "sd-description", @@ -45,6 +46,7 @@ export var defaultV2Css = { navigationButton: "", bodyNavigationButton: "sd-btn", completedPage: "sd-completedpage", + completedBeforePage: "sd-completed-before-page", timerRoot: "sd-body__timer", navigation: { complete: "sd-btn--action sd-navigation__complete-btn", @@ -322,7 +324,10 @@ export var defaultV2Css = { itemTitle: "sd-multipletext__item-title", content: "sd-multipletext__content sd-question__content", row: "sd-multipletext__row", - cell: "sd-multipletext__cell" + cell: "sd-multipletext__cell", + cellError: "sd-multipletext__cell--error", + cellErrorTop: "sd-multipletext__cell--error-top", + cellErrorBottom: "sd-multipletext__cell--error-bottom" }, dropdown: { root: "sd-selectbase", diff --git a/src/defaultV2-theme/blocks/sd-body.scss b/src/defaultV2-theme/blocks/sd-body.scss index fc14a80354..adf25fe501 100644 --- a/src/defaultV2-theme/blocks/sd-body.scss +++ b/src/defaultV2-theme/blocks/sd-body.scss @@ -2,7 +2,7 @@ width: 100%; .sd-body__page { - min-width: calc(300px + 6 * #{$base-unit}); + min-width: 300px; } .sd-body__timer { @@ -110,6 +110,73 @@ box-sizing: border-box; } +.sd-body--empty, +.sd-body--loading { + h1 { + @include articleXXLargeFont; + } + + h2 { + @include articleXLargeFont; + } + + h3 { + @include articleLargeFont; + } + + h4, + h5, + h6 { + @include articleMediumFont; + } + + td, + span, + div, + p { + @include articleDefaultFont; + } + + a { + color: $primary; + } + + button { + display: flex; + align-items: center; + padding: calcSize(1.5) calcSize(4); + vertical-align: baseline; + text-align: center; + background-color: $background; + box-shadow: $shadow-small; + border: none; + border-radius: calcSize(0.5); + cursor: pointer; + user-select: none; + outline: solid calcSize(0.25) transparent; + + &:hover { + background-color: $background-dark; + } + &:focus { + box-shadow: 0 0 0 2px $primary; + } + + span { + display: flex; + align-items: center; + flex-grow: 1; + justify-content: center; + color: $primary; + font-weight: 600; + font-style: normal; + font-family: var(--font-family, $font-family); + font-size: $font-questiontitle-size; + line-height: multiply(1.5, $font-questiontitle-size); + } + } +} + .sd-root_background-image { background-position-x: center; position: absolute; diff --git a/src/defaultV2-theme/blocks/sd-completedpage.scss b/src/defaultV2-theme/blocks/sd-completedpage.scss index 2ae078a484..39491874f3 100644 --- a/src/defaultV2-theme/blocks/sd-completedpage.scss +++ b/src/defaultV2-theme/blocks/sd-completedpage.scss @@ -4,7 +4,10 @@ box-sizing: border-box; text-align: center; height: auto; +} +.sd-completedpage, +.sd-completed-before-page { // h3 { // font-size: calcFontSize(1.5); // } @@ -21,15 +24,13 @@ @include articleLargeFont; } - h4 { - @include articleMediumFont; - } - + h4, h5, h6 { - color: $foreground; + @include articleMediumFont; } + td, span, div, p { diff --git a/src/defaultV2-theme/blocks/sd-html.scss b/src/defaultV2-theme/blocks/sd-html.scss index de0f42e0e2..e7358b1801 100644 --- a/src/defaultV2-theme/blocks/sd-html.scss +++ b/src/defaultV2-theme/blocks/sd-html.scss @@ -15,15 +15,13 @@ @include articleLargeFont; } - h4 { - @include articleMediumFont; - } - + h4, h5, h6 { - color: $foreground; + @include articleMediumFont; } + td, span, div, p { diff --git a/src/defaultV2-theme/blocks/sd-multipletext.scss b/src/defaultV2-theme/blocks/sd-multipletext.scss index 70690b1961..5d85e4a3dc 100644 --- a/src/defaultV2-theme/blocks/sd-multipletext.scss +++ b/src/defaultV2-theme/blocks/sd-multipletext.scss @@ -1,25 +1,31 @@ @import "../variables.scss"; + .sd-multipletext { width: 100%; table-layout: fixed; border-spacing: 0; height: 1px; } + .sd-multipletext__cell { height: 100%; } -.sd-multipletext__cell:not(:last-child) { - padding-right: calcSize(2); + +.sd-multipletext__cell:not(:first-of-type) { + padding-left: calcSize(2); } + .sd-multipletext__item-container.sd-input:focus-within { box-shadow: 0 0 0 2px $primary; } + .sd-multipletext__item-container { display: flex; align-items: flex-start; height: 100%; padding-top: 0; padding-bottom: 0; + .sd-input { padding-top: 0; padding-right: 0; @@ -28,11 +34,23 @@ box-shadow: none; } } + .sd-multipletext__item-container .sd-input, .sd-multipletext__item-title { margin-top: calcSize(1.5); margin-bottom: calcSize(1.5); } + +.sd-multipletext__item-title { + font-size: 0; + line-height: 0; + + span { + font-size: $font-editorfont-size; + line-height: multiply(1.5, $font-editorfont-size); + } +} + .sd-multipletext__item-title { height: calc(100% - #{$base-unit} * 3); max-width: 30%; @@ -42,24 +60,50 @@ white-space: normal; color: $foreground-light; } + .sd-multipletext__item { flex-grow: 1; } + .sd-multipletext__content .sd-multipletext__item-container { position: relative; } + .sd-multipletext__item-container--error { background-color: $red-light; + .sd-input--error { background-color: transparent; } } -.sd-multipletext__item-container:hover:not(:focus-within) > .sd-question__erbox--tooltip { + +.sd-multipletext__item-container:hover:not(:focus-within)>.sd-question__erbox--tooltip { display: inline-block; } -.sd-multipletext tr:not(:last-child) .sd-multipletext__cell { - padding-bottom: calcSize(2); + +.sd-multipletext__cell { + padding-left: 0; + padding-right: 0; + padding-bottom: calcSize(1); + padding-top: calcSize(1); +} + +.sd-multipletext__cell--error-bottom, +.sd-multipletext__row:first-of-type .sd-multipletext__cell { + padding-top: 0; +} + +.sd-multipletext__cell--error-top, +.sd-multipletext__row:last-of-type .sd-multipletext__cell { + padding-bottom: 0; } + +.sd-multipletext__cell--error { + .sd-question__erbox--outside-question { + margin: 0; + } +} + .sd-multipletext .sd-input .sd-input { background: transparent; -} +} \ No newline at end of file diff --git a/src/defaultV2-theme/blocks/sd-row.scss b/src/defaultV2-theme/blocks/sd-row.scss index 6589d246c7..613105af9e 100644 --- a/src/defaultV2-theme/blocks/sd-row.scss +++ b/src/defaultV2-theme/blocks/sd-row.scss @@ -61,6 +61,13 @@ margin-left: calc(-1 * var(--sd-base-padding)); width: calc(100% + var(--sd-base-padding)); } + .sd-row__panel { + height: 100% + } + + .sd-row__question { + height: 100% + } } .sd-row__panel { diff --git a/src/defaultV2-theme/blocks/sd-table.scss b/src/defaultV2-theme/blocks/sd-table.scss index 0eb898e9e9..6e7fbd403f 100644 --- a/src/defaultV2-theme/blocks/sd-table.scss +++ b/src/defaultV2-theme/blocks/sd-table.scss @@ -17,6 +17,12 @@ background-clip: padding-box; } +.sd-root-modern:not(.sd-root-modern--mobile) { + .sd-table__cell:not(.sd-table__cell--actions):not(.sd-table__cell--action):not(.sd-table__cell--empty.sd-table__cell--error) { + width: 10000px; + } +} + .sd-table__row:first-of-type>.sd-table__cell { border-top: 0; } @@ -35,6 +41,45 @@ padding-top: calcSize(3); } +.sd-panel__content { + .sd-table--no-header { + padding-top: 0; + } + + .sd-question--table { + .sd-question__content { + padding-top: calcSize(0); + } + + &>.sd-question__header { + &+.sd-question__content { + padding-top: calcSize(2); + + .sd-table--no-header { + padding-top: calcSize(4); + } + } + } + } + + + .sd-question--table>.sd-question__content .sd-table-wrapper .sd-table:not(.sd-table--no-header) { + margin-top: calcSize(-3); + } + + &>.sd-row:not(:first-of-type) .sd-question--table>.sd-question__content .sd-table-wrapper .sd-table:not(.sd-table--no-header) { + margin-top: calcSize(-2); + } + + .sd-question--table>.sd-question__header+.sd-question__content .sd-table-wrapper .sd-table:not(.sd-table--no-header) { + margin-top: calcSize(1); + } + + &>.sd-row:not(:first-of-type) .sd-question--table>.sd-question__header+.sd-question__content .sd-table-wrapper .sd-table:not(.sd-table--no-header) { + margin-top: calcSize(1); + } +} + .sd-table--alternate-rows { margin-left: calcSize(1); margin-right: calcSize(1); @@ -328,9 +373,6 @@ } &.sd-element--nested { - &>.sd-question__header--location-top { - margin-top: calc(-1 * 0.25 * var(--sd-base-vertical-padding)); - } &>.sd-question__erbox--above-question { margin-bottom: var(--sd-base-vertical-padding); @@ -340,6 +382,13 @@ min-width: auto; overflow-x: auto; } + + &>.sd-question__content { + padding-bottom: calcSize(0.25); + margin-bottom: calcSize(-0.25); + padding-top: calcSize(1); + margin-top: calcSize(-1); + } } &.sd-element--nested>.sd-question__content, diff --git a/src/defaultV2-theme/defaultV2.m600.scss b/src/defaultV2-theme/defaultV2.m600.scss index f9e50c3495..a79a1e85be 100644 --- a/src/defaultV2-theme/defaultV2.m600.scss +++ b/src/defaultV2-theme/defaultV2.m600.scss @@ -32,9 +32,13 @@ .sd-multipletext__cell { display: block; - &:not(:last-child) { - padding-right: 0; - padding-bottom: calcSize(2); + &:not(:first-of-type) { + padding-left: 0; + padding-top: calcSize(1); + } + + :not(:last-of-type) { + padding-bottom: calcSize(1); } } @@ -43,8 +47,6 @@ } .sd-multipletext__item-title { - font-size: calcFontSize(0.75); - line-height: calcLineHeight(1); height: calcSize(2); padding-right: 0; border-right: none; @@ -52,6 +54,11 @@ margin-top: calcSize(1); width: 100%; max-width: none; + + span { + font-size: calcFontSize(0.75); + line-height: calcLineHeight(1); + } } .sd-multipletext__item { diff --git a/src/defaultV2-theme/variables.scss b/src/defaultV2-theme/variables.scss index 5898174ae6..2da36fc5e7 100644 --- a/src/defaultV2-theme/variables.scss +++ b/src/defaultV2-theme/variables.scss @@ -158,7 +158,7 @@ $base-unit: var(--sjs-base-unit, var(--base-unit, 8px)); } @mixin articleXXLargeFont { - color: $foreground; + color: $font-questiontitle-color; font-size: var(--sjs-article-font-xx-large-fontSize, 64px); text-decoration: var(--sjs-article-font-xx-large-textDecoration, "none"); font-family: $font-family; @@ -172,7 +172,7 @@ $base-unit: var(--sjs-base-unit, var(--base-unit, 8px)); } @mixin articleXLargeFont { - color: $foreground; + color: $font-questiontitle-color; font-size: var(--sjs-article-font-x-large-fontSize, 48px); text-decoration: var(--sjs-article-font-x-large-textDecoration, "none"); font-family: $font-family; @@ -186,7 +186,7 @@ $base-unit: var(--sjs-base-unit, var(--base-unit, 8px)); } @mixin articleLargeFont { - color: $foreground; + color: $font-questiontitle-color; font-size: var(--sjs-article-font-large-fontSize, 32px); text-decoration: var(--sjs-article-font-large-textDecoration, "none"); font-family: $font-family; @@ -200,7 +200,7 @@ $base-unit: var(--sjs-base-unit, var(--base-unit, 8px)); } @mixin articleMediumFont { - color: $foreground; + color: $font-questiontitle-color; font-size: var(--sjs-article-font-medium-fontSize, 24px); text-decoration: var(--sjs-article-font-medium-textDecoration, "none"); font-family: $font-family; @@ -214,7 +214,7 @@ $base-unit: var(--sjs-base-unit, var(--base-unit, 8px)); } @mixin articleDefaultFont { - color: $foreground; + color: $font-questiontitle-color; font-size: var(--sjs-article-font-default-fontSize, 16px); text-decoration: var(--sjs-article-font-default-textDecoration, "none"); font-family: $font-family; diff --git a/src/dragdrop/dom-adapter.ts b/src/dragdrop/dom-adapter.ts index 5b409f3870..d5e1dba25a 100644 --- a/src/dragdrop/dom-adapter.ts +++ b/src/dragdrop/dom-adapter.ts @@ -36,6 +36,7 @@ export class DragDropDOMAdapter implements IDragDropDOMAdapter { private currentY: number; // save event.target node from the frameworks update. See https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed private savedTargetNode: any; + private savedTargetNodeParent: any; private scrollIntervalId: number = null; constructor(private dd: IDragDropEngine, private longTap: boolean = true) {} @@ -101,6 +102,7 @@ export class DragDropDOMAdapter implements IDragDropDOMAdapter { clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); `; + this.savedTargetNodeParent = this.savedTargetNode.parentElement; this.rootElement.appendChild(this.savedTargetNode); } @@ -268,9 +270,14 @@ export class DragDropDOMAdapter implements IDragDropDOMAdapter { this.scrollIntervalId = null; if (IsTouch) { + this.savedTargetNode.style.cssText = null; this.savedTargetNode && this.savedTargetNode.parentElement.removeChild(this.savedTargetNode); + this.savedTargetNodeParent.appendChild(this.savedTargetNode); DragDropDOMAdapter.PreventScrolling = false; } + this.savedTargetNode = null; + this.savedTargetNodeParent = null; + document.body.style.setProperty("touch-action", ""); document.body.style.setProperty("user-select", ""); document.body.style.setProperty("-webkit-user-select", ""); diff --git a/src/dropdownListModel.ts b/src/dropdownListModel.ts index 23249b4330..fb6a4c0436 100644 --- a/src/dropdownListModel.ts +++ b/src/dropdownListModel.ts @@ -320,11 +320,12 @@ export class DropdownListModel extends Base { if (start == -1) return null; return this.hintString.substring(start, start + this.inputStringLC.length); } + private qustionPropertyChangedHandler = (sender: any, options: any) => { + this.onPropertyChangedHandler(sender, options); + }; constructor(protected question: Question, protected onSelectionChanged?: (item: IAction, ...params: any[]) => void) { super(); - question.onPropertyChanged.add((sender: any, options: any) => { - this.onPropertyChangedHandler(sender, options); - }); + question.onPropertyChanged.add(this.qustionPropertyChangedHandler); this.showInputFieldComponent = this.question.showInputFieldComponent; this.listModel = this.createListModel(); @@ -530,6 +531,8 @@ export class DropdownListModel extends Base { dispose(): void { super.dispose(); + this.question && this.question.onPropertyChanged.remove(this.qustionPropertyChangedHandler); + this.qustionPropertyChangedHandler = undefined; if (!!this.listModel) { this.listModel.dispose(); } diff --git a/src/dropdownMultiSelectListModel.ts b/src/dropdownMultiSelectListModel.ts index 00945ae97d..5495e93d25 100644 --- a/src/dropdownMultiSelectListModel.ts +++ b/src/dropdownMultiSelectListModel.ts @@ -14,6 +14,10 @@ export class DropdownMultiSelectListModel extends DropdownListModel { @property({ defaultValue: "" }) filterStringPlaceholder: string; @property({ defaultValue: true }) closeOnSelect: boolean; + public locStrsChanged(): void { + super.locStrsChanged(); + this.syncFilterStringPlaceholder(); + } private updateListState() { (>this.listModel).updateState(); this.syncFilterStringPlaceholder(); diff --git a/src/entries/chunks/model.ts b/src/entries/chunks/model.ts index 2f340eb8e1..7b0ef1a8e2 100644 --- a/src/entries/chunks/model.ts +++ b/src/entries/chunks/model.ts @@ -146,6 +146,10 @@ export { export { QuestionMatrixBaseModel } from "../../martixBase"; export { MultipleTextItemModel, + MultipleTextCell, + MultipleTextErrorCell, + MutlipleTextErrorRow, + MutlipleTextRow, QuestionMultipleTextModel, MultipleTextEditorModel } from "../../question_multipletext"; diff --git a/src/entries/core-wo-model.ts b/src/entries/core-wo-model.ts index 6a473ef95e..66a5cf10d0 100644 --- a/src/entries/core-wo-model.ts +++ b/src/entries/core-wo-model.ts @@ -16,5 +16,4 @@ export { unwrap, getOriginalEvent, getElement } from "../utils/utils"; export * from "../actions/action"; export * from "../actions/adaptive-container"; export * from "../actions/container"; -export * from "../utils/tooltip"; export * from "../utils/dragOrClickHelper"; diff --git a/src/entries/knockout-ui-model.ts b/src/entries/knockout-ui-model.ts index a7df837b83..6fc0439866 100644 --- a/src/entries/knockout-ui-model.ts +++ b/src/entries/knockout-ui-model.ts @@ -74,7 +74,6 @@ export { SurveyQuestionMatrixDynamicRemoveButton } from "../knockout/components/ export { SurveyQuestionMatrixDetailButton } from "../knockout/components/matrix-actions/detail-button/detail-button"; export { SurveyQuestionMatrixDynamicDragDropIcon } from "../knockout/components/matrix-actions/drag-drop-icon/drag-drop-icon"; export { ButtonGroupItemViewModel } from "../knockout/components/button-group/button-group-item"; -export { TooltipErrorViewModel } from "../knockout/components/tooltip-error/tooltip-error"; export { SurveyNavigationButton } from "../knockout/components/survey-actions/survey-nav-button"; export * from "../knockout/components/paneldynamic-actions/paneldynamic-actions"; export * from "../knockout/components/brand-info/brand-info"; diff --git a/src/jsonobject.ts b/src/jsonobject.ts index d2b7ff9afb..371725ec36 100644 --- a/src/jsonobject.ts +++ b/src/jsonobject.ts @@ -43,14 +43,19 @@ function getLocStringValue( return ""; } -export function property(options?: IPropertyDecoratorOptions) { - return function (target: any, key: string) { +export function property(options: IPropertyDecoratorOptions = {}) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + return function (target: any, key: string): void { let processComputedUpdater = (obj: any, val: any) => { if (!!val && typeof val === "object" && val.type === ComputedUpdater.ComputedUpdaterType) { Base.startCollectDependencies(() => obj[key] = val.updater(), obj, key); const result = val.updater(); const dependencies = Base.finishCollectDependencies(); val.setDependencies(dependencies); + if(obj.dependencies[key]) { + obj.dependencies[key].dispose(); + } + obj.dependencies[key] = val; return result; } return val; @@ -322,10 +327,10 @@ export class JsonObjectProperty implements IObject { if (!Helpers.isValueEmpty(this.defaultValue)) { return Helpers.isTwoValueEquals(value, this.defaultValue, false, true, false); } + if(this.isLocalizable) return value === null || value === undefined; return ( (value === false && (this.type == "boolean" || this.type == "switch")) || - value === "" || - Helpers.isValueEmpty(value) + value === "" || Helpers.isValueEmpty(value) ); } public getValue(obj: any): any { @@ -349,7 +354,7 @@ export class JsonObjectProperty implements IObject { if (!this.onSettingValue || obj.isLoadingFromJson) return value; return this.onSettingValue(obj, value); } - public setValue(obj: any, value: any, jsonConv: JsonObject) { + public setValue(obj: any, value: any, jsonConv: JsonObject): void { if (this.onSetValue) { this.onSetValue(obj, value, jsonConv); } else { @@ -1603,7 +1608,7 @@ export class JsonObject { } } public valueToObj(value: any, obj: any, property: JsonObjectProperty) { - if (value == null) return; + if (value === null || value === undefined) return; this.removePos(property, value); if (property != null && property.hasToUseSetValue) { property.setValue(obj, value, this); diff --git a/src/knockout/components/list/list.ts b/src/knockout/components/list/list.ts index b8caa09748..47a3cea221 100644 --- a/src/knockout/components/list/list.ts +++ b/src/knockout/components/list/list.ts @@ -17,7 +17,10 @@ ko.components.register("sv-list", { model.initListContainerHtmlElement(componentInfo.element); return { model: model, - dispose: () => { _implementor.dispose(); }, + dispose: () => { + _implementor.dispose(); + model.initListContainerHtmlElement(undefined as any); + }, afterItemRender: (_: any, action: Action) => { !!ko.tasks && ko.tasks.runEarly(); model.onLastItemRended(action); diff --git a/src/knockout/components/popup/popup.ts b/src/knockout/components/popup/popup.ts index 35c853f102..7a262ca375 100644 --- a/src/knockout/components/popup/popup.ts +++ b/src/knockout/components/popup/popup.ts @@ -4,13 +4,22 @@ import { ImplementorBase } from "../../kobase"; const template = require("html-loader?interpolate!val-loader!./popup.html"); export class PopupViewModel { + private _popupImplementor: ImplementorBase; + private _popupModelImplementor: ImplementorBase; constructor(public popupViewModel: PopupBaseViewModel) { - new ImplementorBase(popupViewModel.model); - new ImplementorBase(popupViewModel); + this._popupModelImplementor = new ImplementorBase(popupViewModel.model); + this._popupImplementor = new ImplementorBase(popupViewModel); popupViewModel.model.onVisibilityChanged.add(this.visibilityChangedHandler); } dispose() { + this._popupModelImplementor.dispose(); + this._popupModelImplementor = undefined; + this._popupImplementor.dispose(); + this._popupImplementor = undefined; + this.popupViewModel.resetComponentElement(); this.popupViewModel.model.onVisibilityChanged.remove(this.visibilityChangedHandler); + this.popupViewModel.dispose(); + this.visibilityChangedHandler = undefined; } visibilityChangedHandler = (s: any, option: { isVisible: boolean }) => { if (option.isVisible) { @@ -46,10 +55,10 @@ export function showModal( } export function showDialog(dialogOptions: IDialogOptions, rootElement?: HTMLElement): PopupBaseViewModel { dialogOptions.onHide = () => { - viewModel.dispose(); ko.cleanNode(popupViewModel.container); popupViewModel.container.remove(); popupViewModel.dispose(); + viewModel.dispose(); }; const popupViewModel: PopupBaseViewModel = createPopupModalViewModel(dialogOptions, rootElement); var viewModel = new PopupViewModel(popupViewModel); diff --git a/src/knockout/components/tooltip-error/tooltip-error.html b/src/knockout/components/tooltip-error/tooltip-error.html deleted file mode 100644 index e8ed59a73a..0000000000 --- a/src/knockout/components/tooltip-error/tooltip-error.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/knockout/components/tooltip-error/tooltip-error.ts b/src/knockout/components/tooltip-error/tooltip-error.ts deleted file mode 100644 index b747242362..0000000000 --- a/src/knockout/components/tooltip-error/tooltip-error.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as ko from "knockout"; -import { Question, TooltipManager } from "survey-core"; -const template = require("html-loader?interpolate!val-loader!./tooltip-error.html"); - -export class TooltipErrorViewModel { - private tooltipManager: TooltipManager; - constructor(public question: Question) { - } - public afterRender = (elements: HTMLElement[]): void => { - const tooltipElement = elements.filter((el)=> el instanceof HTMLElement)[0]; - this.tooltipManager = new TooltipManager(tooltipElement); - ko.utils.domNodeDisposal.addDisposeCallback(elements[1], ()=> { - this.tooltipManager.dispose(); - }); - } -} - -ko.components.register("sv-tooltip-error", { - viewModel: { - createViewModel: (params: any, componentInfo: any) => { - return new TooltipErrorViewModel(params.question); - }, - }, - template: template, -}); diff --git a/src/knockout/koquestion_multipletext.ts b/src/knockout/koquestion_multipletext.ts index 5f62eff640..46c34e38ca 100644 --- a/src/knockout/koquestion_multipletext.ts +++ b/src/knockout/koquestion_multipletext.ts @@ -2,11 +2,12 @@ import * as ko from "knockout"; import { QuestionMultipleTextModel, MultipleTextItemModel } from "survey-core"; import { QuestionTextModel } from "survey-core"; import { QuestionImplementor } from "./koquestion"; -import { QuestionText, QuestionTextImplementor } from "./koquestion_text"; -import { Question } from "survey-core"; +import { QuestionTextImplementor } from "./koquestion_text"; import { Serializer } from "survey-core"; import { QuestionFactory } from "survey-core"; import { MultipleTextEditorModel } from "survey-core"; +import { MutlipleTextRow } from "survey-core"; +import { ImplementorBase } from "./kobase"; export class koMultipleTextEditorModel extends MultipleTextEditorModel { private _implementor: QuestionTextImplementor; @@ -60,18 +61,14 @@ export class QuestionMultipleText extends QuestionMultipleTextModel { koRows: any; constructor(name: string) { super(name); - this.koRows = ko.observableArray(this.getRows()); - this.colCountChangedCallback = () => { - this.onColCountChanged(); - }; - this.onColCountChanged(); } protected onBaseCreating() { super.onBaseCreating(); this._implementor = new QuestionMultipleTextImplementor(this); } - protected onColCountChanged() { - this.koRows(this.getRows()); + protected onRowCreated(row: MutlipleTextRow): MutlipleTextRow { + new ImplementorBase(row); + return row; } protected createTextItem(name: string, title: string): MultipleTextItemModel { return new MultipleTextItem(name, title); diff --git a/src/knockout/kosurvey.ts b/src/knockout/kosurvey.ts index 8cb79b615c..4d1cecaa6d 100644 --- a/src/knockout/kosurvey.ts +++ b/src/knockout/kosurvey.ts @@ -100,6 +100,7 @@ export class SurveyImplementor extends ImplementorBase { if (!!this.renderedElement) { ko.cleanNode(this.renderedElement); this.renderedElement.innerHTML = ""; + this.renderedElement = undefined; } this.survey["koAfterRenderPage"] = undefined; this.survey["koAfterRenderHeader"] = undefined; @@ -125,7 +126,7 @@ export class Survey extends SurveyModel { super(jsonObj, renderedElement); this.implementor = new SurveyImplementor(this); } - render(element: any = null): void { + public render(element: any = null): void { this.implementor.render(element); } public getHtmlTemplate(): string { @@ -134,6 +135,13 @@ export class Survey extends SurveyModel { public makeReactive(obj: Base): void { new ImplementorBase(obj); } + public dispose() { + super.dispose(); + if(this.implementor) { + this.implementor.dispose(); + this.implementor = undefined; + } + } } function ensureSurvey(survey: Survey) { diff --git a/src/knockout/templates/index.html b/src/knockout/templates/index.html index 0c693a2218..6be70d2f7a 100644 --- a/src/knockout/templates/index.html +++ b/src/knockout/templates/index.html @@ -38,10 +38,10 @@
-
+
-
+
diff --git a/src/knockout/templates/question-multipletext.html b/src/knockout/templates/question-multipletext.html index 0fe7ed6a4f..5674af0848 100644 --- a/src/knockout/templates/question-multipletext.html +++ b/src/knockout/templates/question-multipletext.html @@ -1,31 +1,27 @@  diff --git a/src/knockout/templates/questioncontent.html b/src/knockout/templates/questioncontent.html index a803d2353f..51467398cf 100644 --- a/src/knockout/templates/questioncontent.html +++ b/src/knockout/templates/questioncontent.html @@ -26,10 +26,6 @@ - - - -
diff --git a/src/list.ts b/src/list.ts index 17cc64a74e..affeacd2d0 100644 --- a/src/list.ts +++ b/src/list.ts @@ -320,5 +320,6 @@ export class ListModel extends ActionContainer if(!!this.loadingIndicatorValue) { this.loadingIndicatorValue.dispose(); } + this.listContainerHtmlElement = undefined; } } diff --git a/src/localizablestring.ts b/src/localizablestring.ts index 67799c4991..c394b05fc5 100644 --- a/src/localizablestring.ts +++ b/src/localizablestring.ts @@ -105,19 +105,19 @@ export class LocalizableString implements ILocalizableString { var loc = this.locale; if (!loc) loc = this.defaultLoc; var res = this.getValue(loc); - if (!res && loc === this.defaultLoc) { + if (this.isValueEmpty(res) && loc === this.defaultLoc) { res = this.getValue(surveyLocalization.defaultLocale); } - if(!res) { + if(this.isValueEmpty(res)) { const dialect = this.getRootDialect(loc); if(!!dialect) { res = this.getValue(dialect); } } - if (!res && loc !== this.defaultLoc) { + if (this.isValueEmpty(res) && loc !== this.defaultLoc) { res = this.getValue(this.defaultLoc); } - if (!res && !!this.getLocalizationName()) { + if (this.isValueEmpty(res) && !!this.getLocalizationName()) { res = this.getLocalizationStr(); if(!!this.onGetLocalizationTextCallback) { res = this.onGetLocalizationTextCallback(res); @@ -141,47 +141,55 @@ export class LocalizableString implements ILocalizableString { public get hasHtml(): boolean { return this.hasHtmlValue(); } - public get html() { + public get html(): string { if (!this.hasHtml) return ""; return this.getHtmlValue(); } public get isEmpty(): boolean { return this.getValuesKeys().length == 0; } - public get textOrHtml() { + public get textOrHtml(): string { return this.hasHtml ? this.getHtmlValue() : this.calculatedText; } - public get renderedHtml() { + public get renderedHtml(): string { return this.textOrHtml; } public getLocaleText(loc: string): string { - if (!loc) loc = this.defaultLoc; - var res = this.getValue(loc); + const res = this.getLocaleTextCore(loc); return res ? res : ""; } - private getLocaleTextWithDefault(loc: string): string { - let res = this.getLocaleText(loc); + private getLocaleTextCore(loc: string): string { + if (!loc) loc = this.defaultLoc; + return this.getValue(loc); + } + private isLocaleTextEqualsWithDefault(loc: string, val: string): boolean { + let res = this.getLocaleTextCore(loc); if(!res && this.onGetDefaultTextCallback) { - return this.onGetDefaultTextCallback(); + res = this.onGetDefaultTextCallback(); } - return res; + if(res === val) return true; + return this.isValueEmpty(res) && this.isValueEmpty(val); + } + public clear(): void { + this.setJson(undefined); + } + public clearLocale(loc?: string): void { + this.setLocaleText(loc, undefined); } public setLocaleText(loc: string, value: string): void { loc = this.getValueLoc(loc); - if (!this.storeDefaultText && value == this.getLocaleTextWithDefault(loc)) { - if(!!value || !!loc && loc !== this.defaultLoc) return; + if (!this.storeDefaultText && this.isLocaleTextEqualsWithDefault(loc, value)) { + if(!this.isValueEmpty(value) || !!loc && loc !== this.defaultLoc) return; let dl = surveyLocalization.defaultLocale; let oldValue = this.getValue(dl); - if(!!dl && !!oldValue) { + if(!!dl && !this.isValueEmpty(oldValue)) { this.setValue(dl, value); this.fireStrChanged(dl, oldValue); } return; } if (!settings.localization.storeDuplicatedTranslations && - value && - loc && - loc != this.defaultLoc && + !this.isValueEmpty(value) && loc && loc != this.defaultLoc && !this.getValue(loc) && value == this.getLocaleText(this.defaultLoc) ) @@ -190,7 +198,7 @@ export class LocalizableString implements ILocalizableString { if (!loc) loc = this.defaultLoc; var oldValue = this.onStrChanged && loc === curLoc ? this.pureText : undefined; delete (this).htmlValues[loc]; - if (!value) { + if (this.isValueEmpty(value)) { if (this.getValue(loc)) this.deleteValue(loc); } else { if (typeof value === "string") { @@ -206,6 +214,11 @@ export class LocalizableString implements ILocalizableString { } this.fireStrChanged(loc, oldValue); } + private isValueEmpty(val: string): boolean { + if(val === undefined || val === null) return true; + if(this.localizationName) return false; + return val === ""; + } private get curLocale(): string { return !!this.locale ? this.locale : this.defaultLoc; } @@ -258,7 +271,7 @@ export class LocalizableString implements ILocalizableString { } this.values = {}; this.htmlValues = {}; - if (!value) return; + if (value === null || value === undefined) return; if (typeof value === "string") { this.setLocaleText(null, value); } else { diff --git a/src/localization/latvian.ts b/src/localization/latvian.ts index e50068b4c5..2a52260746 100644 --- a/src/localization/latvian.ts +++ b/src/localization/latvian.ts @@ -1,8 +1,8 @@ import { surveyLocalization } from "survey-core"; export var latvianSurveyStrings = { - pagePrevText: "Atpakaļ", - pageNextText: "Tālāk", + pagePrevText: "Iepriekšēja lapa", + pageNextText: "Nākamā lapa", completeText: "Iesniegt", previewText: "Priekšskatījums", editText: "Rediģēt", diff --git a/src/popup-dropdown-view-model.ts b/src/popup-dropdown-view-model.ts index 003bafda8e..108878865c 100644 --- a/src/popup-dropdown-view-model.ts +++ b/src/popup-dropdown-view-model.ts @@ -8,7 +8,7 @@ import { settings } from "./settings"; export class PopupDropdownViewModel extends PopupBaseViewModel { private scrollEventCallBack = (event: any) => { - if(this.isOverlay && IsTouch) { + if (this.isOverlay && IsTouch) { event.stopPropagation(); event.preventDefault(); return; @@ -16,7 +16,7 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { this.hidePopup(); } private static readonly tabletSizeBreakpoint = 600; - private calculateIsTablet (windowWidth: number, windowHeight: number) { + private calculateIsTablet(windowWidth: number, windowHeight: number) { const width = Math.min(windowWidth, windowHeight); this.isTablet = width >= PopupDropdownViewModel.tabletSizeBreakpoint; } @@ -25,7 +25,7 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { document.documentElement.style.setProperty("--sv-popup-overlay-height", `${visualViewport.height * visualViewport.scale}px`); } private resizeWindowCallback = () => { - if(!this.isOverlay) { + if (!this.isOverlay) { this.updatePosition(true, false); } }; @@ -39,10 +39,10 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { } private _updatePosition() { - if(!this.targetElement) return; + if (!this.targetElement) return; const targetElementRect = this.targetElement.getBoundingClientRect(); const popupContainer = this.container?.querySelector(this.containerSelector); - if(!popupContainer) return; + if (!popupContainer) return; const fixedPopupContainer = this.container?.querySelector(this.fixedPopupContainer) as HTMLElement; const scrollContent = popupContainer.querySelector(this.scrollingContentSelector); const popupComputedStyle = window.getComputedStyle(popupContainer); @@ -109,7 +109,7 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { } } } - if(!!fixedPopupContainer) { + if (!!fixedPopupContainer) { const rect = fixedPopupContainer.getBoundingClientRect(); pos.top -= rect.top; pos.left -= rect.left; @@ -135,10 +135,10 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { protected getActualHorizontalPosition(): "left" | "center" | "right" { let actualHorizontalPosition = this.model.horizontalPosition; let isRtl = !!document && document.defaultView.getComputedStyle(document.body).direction == "rtl"; - if(isRtl) { - if(this.model.horizontalPosition === "left") { + if (isRtl) { + if (this.model.horizontalPosition === "left") { actualHorizontalPosition = "right"; - } else if(this.model.horizontalPosition === "right") { + } else if (this.model.horizontalPosition === "right") { actualHorizontalPosition = "left"; } } @@ -162,21 +162,23 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { @property({ defaultValue: "left" }) popupDirection: string; @property({ defaultValue: { left: "0px", top: "0px" } }) pointerTarget: IPosition; + private recalculatePositionHandler: (_: any, options: { isResetHeight: boolean }) => void; + constructor(model: PopupModel, public targetElement?: HTMLElement) { super(model); - this.model.onRecalculatePosition.add((_, options: { isResetHeight: boolean }) => { - if(!this.isOverlay) { - this.updatePosition(options.isResetHeight); - } - }); + this.model.onRecalculatePosition.add(this.recalculatePositionHandler); } public setComponentElement(componentRoot: HTMLElement, targetElement?: HTMLElement | null): void { super.setComponentElement(componentRoot); - if(!!componentRoot && !!componentRoot.parentElement && !this.isModal) { + if (!!componentRoot && !!componentRoot.parentElement && !this.isModal) { this.targetElement = targetElement || componentRoot.parentElement; } } + public resetComponentElement() { + super.resetComponentElement(); + this.targetElement = undefined; + } public updateOnShowing(): void { const { root } = settings.environment; this.prevActiveElement = root.activeElement; @@ -189,9 +191,9 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { this.switchFocus(); window.addEventListener("resize", this.resizeWindowCallback); - if(this.shouldCreateResizeCallback) { + if (this.shouldCreateResizeCallback) { window.visualViewport.addEventListener("resize", this.resizeEventCallback); - if(this.container) { + if (this.container) { this.container.addEventListener("touchstart", this.touchStartEventCallback); this.container.addEventListener("touchmove", this.touchMoveEventCallback); } @@ -205,11 +207,11 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { } public updatePosition(isResetHeight: boolean, isDelayUpdating = true): void { - if(isResetHeight) { + if (isResetHeight) { this.height = "auto"; } - if(isDelayUpdating) { + if (isDelayUpdating) { setTimeout(() => { this._updatePosition(); }, 1); @@ -221,16 +223,16 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { public updateOnHiding(): void { super.updateOnHiding(); window.removeEventListener("resize", this.resizeWindowCallback); - if(this.shouldCreateResizeCallback) { + if (this.shouldCreateResizeCallback) { window.visualViewport.removeEventListener("resize", this.resizeEventCallback); - if(this.container) { + if (this.container) { this.container.removeEventListener("touchstart", this.touchStartEventCallback); this.container.removeEventListener("touchmove", this.touchMoveEventCallback); } } window.removeEventListener("scroll", this.scrollEventCallBack); - if(!this.isDisposed) { + if (!this.isDisposed) { this.top = undefined; this.left = undefined; this.height = undefined; @@ -238,4 +240,29 @@ export class PopupDropdownViewModel extends PopupBaseViewModel { this.minWidth = undefined; } } + + protected onModelChanging(newModel: PopupModel) { + if (!!this.model) { + this.model.onRecalculatePosition.remove(this.recalculatePositionHandler); + } + if (!this.recalculatePositionHandler) { + this.recalculatePositionHandler = (_: any, options: { isResetHeight: boolean }) => { + if (!this.isOverlay) { + this.updatePosition(options.isResetHeight); + } + }; + } + super.onModelChanging(newModel); + newModel.onRecalculatePosition.add(this.recalculatePositionHandler); + } + + public dispose(): void { + super.dispose(); + this.updateOnHiding(); + if (!!this.model) { + this.model.onRecalculatePosition.remove(this.recalculatePositionHandler); + this.recalculatePositionHandler = undefined; + } + this.resetComponentElement(); + } } \ No newline at end of file diff --git a/src/popup-survey.ts b/src/popup-survey.ts index afc0301cc7..4206121631 100644 --- a/src/popup-survey.ts +++ b/src/popup-survey.ts @@ -40,15 +40,6 @@ export class PopupSurveyModel extends Base { this.width = this.survey.width; this.updateCss(); this.onCreating(); - this.survey.onPopupVisibleChanged.add((_, opt) => { - if(opt.visible) { - this.onScrollCallback = () => { - opt.popup.toggleVisibility(); - }; - } else { - this.onScrollCallback = undefined; - } - }); } protected onCreating(): void { } public getType(): string { @@ -223,11 +214,8 @@ export class PopupSurveyModel extends Base { : 0; } } - private onScrollCallback: () => void; public onScroll(): void { - if(this.onScrollCallback) { - this.onScrollCallback(); - } + this.survey.onScroll(); } } /** diff --git a/src/popup-view-model.ts b/src/popup-view-model.ts index ede16d0fe1..18127af3a0 100644 --- a/src/popup-view-model.ts +++ b/src/popup-view-model.ts @@ -10,6 +10,8 @@ import { getElement } from "./utils/utils"; export const FOCUS_INPUT_SELECTOR = "input:not(:disabled):not([readonly]):not([type=hidden]),select:not(:disabled):not([readonly]),textarea:not(:disabled):not([readonly]), button:not(:disabled):not([readonly]), [tabindex]:not([tabindex^=\"-\"])"; export class PopupBaseViewModel extends Base { + private static SubscriptionId = 0; + private subscriptionId = PopupBaseViewModel.SubscriptionId++; protected popupSelector = ".sv-popup"; protected fixedPopupContainer = ".sv-popup"; protected containerSelector = ".sv-popup__container"; @@ -32,7 +34,7 @@ export class PopupBaseViewModel extends Base { private createdContainer: HTMLElement; public getLocale(): string { - if(!!this.locale) return this.locale; + if (!!this.locale) return this.locale; return super.getLocale(); } protected hidePopup(): void { @@ -79,10 +81,14 @@ export class PopupBaseViewModel extends Base { this.minWidth = nullableValue; } + protected onModelChanging(newModel: PopupModel) { + } + private setupModel(model: PopupModel) { if (!!this.model) { - this.model.unregisterPropertyChangedHandlers(["isVisible"], "PopupBaseViewModel"); + this.model.unregisterPropertyChangedHandlers(["isVisible"], "PopupBaseViewModel" + this.subscriptionId); } + this.onModelChanging(model); this._model = model; const onIsVisibleChangedHandler = () => { if (!model.isVisible) { @@ -90,7 +96,7 @@ export class PopupBaseViewModel extends Base { } this.isVisible = model.isVisible; }; - model.registerPropertyChangedHandlers(["isVisible"], onIsVisibleChangedHandler, "PopupBaseViewModel"); + model.registerPropertyChangedHandlers(["isVisible"], onIsVisibleChangedHandler, "PopupBaseViewModel" + this.subscriptionId); onIsVisibleChangedHandler(); } @@ -144,7 +150,7 @@ export class PopupBaseViewModel extends Base { return this.getLocalizationString("modalCancelButtonText"); } public get footerToolbar(): ActionContainer { - if(!this.footerToolbarValue) { + if (!this.footerToolbarValue) { this.createFooterActionBar(); } return this.footerToolbarValue; @@ -175,9 +181,9 @@ export class PopupBaseViewModel extends Base { } public switchFocus(): void { - if(this.isFocusedContent) { + if (this.isFocusedContent) { this.focusFirstInput(); - } else if(this.isFocusedContainer) { + } else if (this.isFocusedContainer) { this.focusContainer(); } } @@ -220,13 +226,17 @@ export class PopupBaseViewModel extends Base { } public dispose(): void { super.dispose(); - if(!!this.createdContainer) { + if (this.model) { + this.model.unregisterPropertyChangedHandlers(["isVisible"], "PopupBaseViewModel" + this.subscriptionId); + } + if (!!this.createdContainer) { this.createdContainer.remove(); this.createdContainer = undefined; } - if(!!this.footerToolbarValue) { + if (!!this.footerToolbarValue) { this.footerToolbarValue.dispose(); } + this.resetComponentElement(); } public initializePopupContainer(): void { if (!this.container) { @@ -236,12 +246,13 @@ export class PopupBaseViewModel extends Base { } } public setComponentElement(componentRoot: HTMLElement, targetElement?: HTMLElement | null): void { - if(!!componentRoot) { + if (!!componentRoot) { this.containerElement = componentRoot; } } public resetComponentElement(): void { this.containerElement = undefined; + this.prevActiveElement = undefined; } protected preventScrollOuside(event: any, deltaY: number): void { let currentElement = event.target; diff --git a/src/popup.ts b/src/popup.ts index ddc5fd56cc..42b33bfcee 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -68,7 +68,8 @@ export class PopupModel extends Base { onHide = () => { }, onShow = () => { }, cssClass: string = "", - title: string = "" + title: string = "", + private onDispose = () => { } ) { super(); this.contentComponentName = contentComponentName; @@ -111,6 +112,10 @@ export class PopupModel extends Base { this.onFooterActionsCreated.fire(this, options); return options.actions; } + public dispose(): void { + super.dispose(); + this.onDispose(); + } } export function createDialogOptions( diff --git a/src/question.ts b/src/question.ts index 227cdec66a..dd9c025ca9 100644 --- a/src/question.ts +++ b/src/question.ts @@ -901,12 +901,8 @@ export class Question extends SurveyElement .append(cssClasses.descriptionUnderInput, this.hasDescriptionUnderInput) .toString(); } - protected getIsErrorsModeTooltip() { - return super.getIsErrorsModeTooltip() && !this.customWidget; - } - public showErrorOnCore(location: string): boolean { - return !this.isErrorsModeTooltip && !this.showErrorsAboveQuestion && !this.showErrorsBelowQuestion && this.getErrorLocation() === location; + return !this.showErrorsAboveQuestion && !this.showErrorsBelowQuestion && this.getErrorLocation() === location; } public get showErrorOnTop(): boolean { @@ -915,15 +911,8 @@ export class Question extends SurveyElement public get showErrorOnBottom(): boolean { return this.showErrorOnCore("bottom"); } - protected getIsTooltipErrorSupportedByParent(): boolean { - if (this.parentQuestion) { - return this.parentQuestion.getIsTooltipErrorInsideSupported(); - } else { - return super.getIsTooltipErrorSupportedByParent(); - } - } private get showErrorsOutsideQuestion(): boolean { - return this.isDefaultV2Theme && !(this.hasParent && this.getIsTooltipErrorSupportedByParent()); + return this.isDefaultV2Theme; } public get showErrorsAboveQuestion(): boolean { return this.showErrorsOutsideQuestion && this.getErrorLocation() === "top"; @@ -945,7 +934,6 @@ export class Question extends SurveyElement .append(cssClasses.error.outsideQuestion, this.showErrorsBelowQuestion || this.showErrorsAboveQuestion) .append(cssClasses.error.belowQuestion, this.showErrorsBelowQuestion) .append(cssClasses.error.aboveQuestion, this.showErrorsAboveQuestion) - .append(cssClasses.error.tooltip, this.isErrorsModeTooltip) .append(cssClasses.error.locationTop, this.showErrorOnTop) .append(cssClasses.error.locationBottom, this.showErrorOnBottom) .toString(); @@ -1356,6 +1344,8 @@ export class Question extends SurveyElement public set value(newValue: any) { this.setNewValue(newValue); } + public get hasFilteredValue(): boolean { return false; } + public getFilteredValue(): any { return this.value; } public get valueForSurvey(): any { if (!!this.valueToDataCallback) { return this.valueToDataCallback(this.value); diff --git a/src/question_baseselect.ts b/src/question_baseselect.ts index 94a27dfc2a..fd93812644 100644 --- a/src/question_baseselect.ts +++ b/src/question_baseselect.ts @@ -1,6 +1,6 @@ import { property, Serializer } from "./jsonobject"; import { SurveyError } from "./survey-error"; -import { ISurveyImpl, ISurvey, ISurveyData, IPlainDataOptions } from "./base-interfaces"; +import { ISurveyImpl, ISurvey, ISurveyData, IPlainDataOptions, IValueItemCustomPropValues } from "./base-interfaces"; import { SurveyModel } from "./survey"; import { IQuestionPlainData, Question } from "./question"; import { ItemValue } from "./itemvalue"; @@ -573,49 +573,43 @@ export class QuestionSelectBase extends Question { return this.hasUnknownValue(val, true, false); } protected updateSelectedItemValues(): void { - if(this.waitingGetChoiceDisplayValueResponse) return; - - const IsMultipleValue = this.getIsMultipleValue(); - if(IsMultipleValue) { - this.updateMultipleSelectedItemValues(); - } else { - this.updateSingleSelectedItemValues(); - } - } - protected updateSingleSelectedItemValues(): void { - if (!!this.survey && !this.isEmpty() && !ItemValue.getItemByValue(this.choices, this.value)) { + if(this.waitingGetChoiceDisplayValueResponse || !this.survey || this.isEmpty()) return; + const value = this.value; + const valueArray: Array = Array.isArray(value) ? value : [value]; + const hasItemWithoutValues = valueArray.some(val => !ItemValue.getItemByValue(this.choices, val)); + if (hasItemWithoutValues) { this.waitingGetChoiceDisplayValueResponse = true; this.isReady = !this.waitingAcyncOperations; this.survey.getChoiceDisplayValue({ question: this, - values: [this.value], - setItems: (displayValues: Array) => { + values: valueArray, + setItems: (displayValues: Array, ...customValues: Array) => { this.waitingGetChoiceDisplayValueResponse = false; if (!displayValues || !displayValues.length) return; - this.selectedItemValues = this.createItemValue(this.value, displayValues[0]); + const items = displayValues.map((displayValue, index) => this.createItemValue(valueArray[index], displayValue)); + this.setCustomValuesIntoItems(items, customValues); + if(Array.isArray(value)) { + this.selectedItemValues = items; + } + else { + this.selectedItemValues = items[0]; + } this.isReady = !this.waitingAcyncOperations; } }); } } - protected updateMultipleSelectedItemValues(): void { - const valueArray: Array = this.value; - const hasItemWithValues = valueArray.some(val => !ItemValue.getItemByValue(this.choices, val)); - - if (!!this.survey && !this.isEmpty() && hasItemWithValues) { - this.waitingGetChoiceDisplayValueResponse = true; - this.isReady = this.waitingAcyncOperations; - this.survey.getChoiceDisplayValue({ - question: this, - values: valueArray, - setItems: (displayValues: Array) => { - this.waitingGetChoiceDisplayValueResponse = false; - if (!displayValues || !displayValues.length) return; - this.selectedItemValues = displayValues.map((displayValue, index) => this.createItemValue(this.value[index], displayValue)); - this.isReady = !this.waitingAcyncOperations; + private setCustomValuesIntoItems(items: Array, customValues: Array): void { + if(!Array.isArray(customValues) || customValues.length === 0) return; + customValues.forEach(customValue => { + const vals = customValue.values; + const propName = customValue.propertyName; + if(Array.isArray(vals)) { + for(let i = 0; i < items.length && i < vals.length; i ++) { + items[i][propName] = vals[i]; } - }); - } + } + }); } protected hasUnknownValue( val: any, diff --git a/src/question_checkbox.ts b/src/question_checkbox.ts index 8ac0c4cb5e..7a5aabba11 100644 --- a/src/question_checkbox.ts +++ b/src/question_checkbox.ts @@ -219,6 +219,11 @@ export class QuestionCheckboxModel extends QuestionCheckboxBase { return this.validateItemValues(itemValues); } public get selectedItems(): Array { return this.selectedChoices; } + public get hasFilteredValue(): boolean { return !!this.valuePropertyName; } + public getFilteredValue(): any { + if(this.hasFilteredValue) return this.renderedValue; + return super.getFilteredValue(); + } protected getMultipleSelectedItems(): Array { return this.selectedChoices; } diff --git a/src/question_matrixdropdownbase.ts b/src/question_matrixdropdownbase.ts index d2e95b6964..a40c4a9135 100644 --- a/src/question_matrixdropdownbase.ts +++ b/src/question_matrixdropdownbase.ts @@ -2402,9 +2402,6 @@ export class QuestionMatrixDropdownModelBase extends QuestionMatrixBaseModeldata); } } - public focusIn(): void { + public focusIn = (): void => { this.editor.focusIn(); } /** @@ -309,7 +309,6 @@ export class QuestionMultipleTextModel extends Question for (var i = 0; i < names.length; i++) question.addItem(names[i]); } - colCountChangedCallback: () => void; constructor(name: string) { super(name); this.createNewArray("items", (item: any) => { @@ -318,8 +317,8 @@ export class QuestionMultipleTextModel extends Question this.survey.multipleTextItemAdded(this, item); } }); - this.registerPropertyChangedHandlers(["items", "colCount"], () => { - this.fireCallback(this.colCountChangedCallback); + this.registerPropertyChangedHandlers(["items", "colCount", "itemErrorLocation"], () => { + this.calcVisibleRows(); }); this.registerPropertyChangedHandlers(["itemSize"], () => { this.updateItemsSize(); }); } @@ -347,7 +346,6 @@ export class QuestionMultipleTextModel extends Question onSurveyLoad() { this.editorsOnSurveyLoad(); super.onSurveyLoad(); - this.fireCallback(this.colCountChangedCallback); } setQuestionValue(newValue: any, updateIsAnswered: boolean = true) { super.setQuestionValue(newValue, updateIsAnswered); @@ -470,6 +468,12 @@ export class QuestionMultipleTextModel extends Question if(this.itemErrorLocation !== "default") return this.itemErrorLocation; return this.getErrorLocation(); } + public get showItemErrorOnTop(): boolean { + return this.getQuestionErrorLocation() == "top"; + } + public get showItemErrorOnBottom(): boolean { + return this.getQuestionErrorLocation() == "bottom"; + } public getChildErrorLocation(child: Question): string { return this.getQuestionErrorLocation(); } @@ -503,22 +507,48 @@ export class QuestionMultipleTextModel extends Question public set itemSize(val: number) { this.setPropertyValue("itemSize", val); } - public getRows(): Array { - var colCount = this.colCount; - var items = this.items; - var rows = []; - var index = 0; + @propertyArray() rows: Array; + + protected onRowCreated(row: MutlipleTextRow) { + return row; + } + + private calcVisibleRows() { + const colCount = this.colCount; + const items = this.items; + let index = 0; + let row: MutlipleTextRow; + let errorRow: MutlipleTextErrorRow; + let rows: Array = []; for (var i = 0; i < items.length; i++) { - if (index == 0) { - rows.push([]); + if(index == 0) { + row = this.onRowCreated(new MutlipleTextRow()); + errorRow = this.onRowCreated(new MutlipleTextErrorRow()); + if(this.showItemErrorOnTop) { + rows.push(errorRow); + rows.push(row); + } + else { + rows.push(row); + rows.push(errorRow); + } } - rows[rows.length - 1].push(items[i]); + row.cells.push(new MultipleTextCell(items[i], this)); + errorRow.cells.push(new MultipleTextErrorCell(items[i], this)); index++; - if (index >= colCount) { + if (index >= colCount || i == items.length - 1) { index = 0; + errorRow.onAfterCreated(); } } - return rows; + this.rows = rows; + } + + public getRows(): Array { + if(Helpers.isValueEmpty(this.rows)) { + this.calcVisibleRows(); + } + return this.rows; } private isMultipleItemValueChanging = false; protected onValueChanged(): void { @@ -684,8 +714,45 @@ export class QuestionMultipleTextModel extends Question public getItemTitleCss(): string { return new CssClassBuilder().append(this.cssClasses.itemTitle).toString(); } - protected getIsTooltipErrorInsideSupported(): boolean { - return true; +} + +export class MutlipleTextRow extends Base { + @property() public isVisible: boolean = true; + @propertyArray() public cells: Array = [] +} +export class MutlipleTextErrorRow extends MutlipleTextRow { + public onAfterCreated(): void { + const callback = () => { + this.isVisible = this.cells.some((cell) => cell.item?.editor && cell.item?.editor.hasVisibleErrors); + }; + this.cells.forEach((cell) => { + if(cell.item?.editor) { + cell.item?.editor.registerFunctionOnPropertyValueChanged("hasVisibleErrors", callback); + } + }); + callback(); + } +} +export class MultipleTextCell { + constructor(public item: MultipleTextItemModel, protected question: QuestionMultipleTextModel) {} + public isErrorsCell: boolean = false; + protected getClassName(): string { + return new CssClassBuilder().append(this.question.cssClasses.cell).toString(); + } + public get className(): string { + return this.getClassName(); + } +} + +export class MultipleTextErrorCell extends MultipleTextCell { + public isErrorsCell: boolean = true; + protected getClassName(): string { + return new CssClassBuilder() + .append(super.getClassName()) + .append(this.question.cssClasses.cellError) + .append(this.question.cssClasses.cellErrorTop, this.question.showItemErrorOnTop) + .append(this.question.cssClasses.cellErrorBottom, this.question.showItemErrorOnBottom) + .toString(); } } diff --git a/src/question_paneldynamic.ts b/src/question_paneldynamic.ts index 96372a42fc..bd22eb6feb 100644 --- a/src/question_paneldynamic.ts +++ b/src/question_paneldynamic.ts @@ -1518,6 +1518,7 @@ export class QuestionPanelDynamicModel extends Question if (!!this.parentQuestion && !!this.parent) { cachedValues[QuestionPanelDynamicItem.ParentItemVariableName] = (this.parent).getValue(); } + this.isValueChangingInternally = true; for (var i = 0; i < this.panels.length; i++) { const panel = this.panels[i]; var panelValues = this.getPanelItemData(panel.data); @@ -1530,6 +1531,7 @@ export class QuestionPanelDynamicModel extends Question newProps[panelName] = panel; panel.runCondition(newValues, newProps); } + this.isValueChangingInternally = false; } onAnyValueChanged(name: string) { super.onAnyValueChanged(name); @@ -2150,10 +2152,20 @@ export class QuestionPanelDynamicModel extends Question this.updateFooterActionsCallback(); this.footerToolbarValue.setItems(items); } - private createTabByPanel(panel: PanelModel) { + private createTabByPanel(panel: PanelModel, visPanelIndex: number) { if(!this.isRenderModeTab) return; const locTitle = new LocalizableString(panel, true); + locTitle.onGetTextCallback = (str: string): string => { + if(!this.survey) return str; + const options = { + title: str, + panel: panel, + visiblePanelIndex: visPanelIndex + }; + this.survey.dynamicPanelGetTabTitle(this, options); + return options.title; + }; locTitle.sharedData = this.locTemplateTabTitle; const isActive = this.getPanelIndexById(panel.id) === this.currentIndex; const newItem = new Action({ @@ -2194,13 +2206,16 @@ export class QuestionPanelDynamicModel extends Question if(!this.isRenderModeTab) return; const items: Array = []; - this.visiblePanels.forEach(panel => items.push(this.createTabByPanel(panel))); + const visPanels = this.visiblePanels; + for(let i = 0; i < visPanels.length; i ++) { + this.visiblePanels.forEach(panel => items.push(this.createTabByPanel(visPanels[i], i))); + } this.additionalTitleToolbar.setItems(items); } private addTabFromToolbar(panel: PanelModel, index: number) { if(!this.isRenderModeTab) return; - const newItem = this.createTabByPanel(panel); + const newItem = this.createTabByPanel(panel, index); this.additionalTitleToolbar.actions.splice(index, 0, newItem); this.updateTabToolbarItemsPressedState(); } diff --git a/src/question_ranking.ts b/src/question_ranking.ts index b91eaecd44..7f56185c2f 100644 --- a/src/question_ranking.ts +++ b/src/question_ranking.ts @@ -37,7 +37,8 @@ export class QuestionRankingModel extends QuestionCheckboxModel { } public getItemTabIndex(item: ItemValue) { - return this.isDesignMode ? undefined : 0; + if (this.isDesignMode || item.disabled) return undefined; + return 0; } public get rootClass(): string { @@ -287,7 +288,12 @@ export class QuestionRankingModel extends QuestionCheckboxModel { if (!this.isDragStartNodeValid(target)) return; - if (this.allowStartDrag && this.canStartDragDueMaxSelectedChoices(target)) { + if ( + this.allowStartDrag && + this.canStartDragDueMaxSelectedChoices(target) && + this.canStartDragDueItemEnabled(choice) + ) + { this.dragDropRankingChoices.startDrag(event, choice, this, node); } }; @@ -314,6 +320,10 @@ export class QuestionRankingModel extends QuestionCheckboxModel { return true; } + private canStartDragDueItemEnabled(item: ItemValue): boolean { + return item.enabled; + } + public checkMaxSelectedChoicesUnreached() { if (this.maxSelectedChoices < 1) return true; var val = this.value; diff --git a/src/question_tagbox.ts b/src/question_tagbox.ts index 192ea1f599..f2632fa495 100644 --- a/src/question_tagbox.ts +++ b/src/question_tagbox.ts @@ -22,16 +22,23 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { super(name); this.createLocalizableString("placeholder", this, false, true); this.createLocalizableString("clearCaption", this, false, true); + this.registerPropertyChangedHandlers(["value", "renderAs", "showOtherItem", "otherText", "placeholder", "choices", "visibleChoices"], () => { + this.updateReadOnlyText(); + }); + this.updateReadOnlyText(); + } + public locStrsChanged(): void { + super.locStrsChanged(); + this.updateReadOnlyText(); + this.dropdownListModel?.locStrsChanged(); + } + @property({ defaultValue: "" }) readOnlyText: string; + private updateReadOnlyText(): void { + this.readOnlyText = this.displayValue || this.placeholder; } - protected getDefaultItemComponent(): string { return ""; } - - public get readOnlyText() { - return this.displayValue || this.placeholder; - } - public onSurveyLoad() { super.onSurveyLoad(); if (!this.dropdownListModel) { diff --git a/src/react/components/list/list.tsx b/src/react/components/list/list.tsx index c54456dd75..6aab007d7b 100644 --- a/src/react/components/list/list.tsx +++ b/src/react/components/list/list.tsx @@ -37,6 +37,12 @@ export class List extends SurveyElementBase { this.model.initListContainerHtmlElement(this.listContainerRef.current); } } + componentWillUnmount(): void { + super.componentWillUnmount(); + if(!!this.model) { + this.model.initListContainerHtmlElement(undefined as any); + } + } renderElement() { return (
diff --git a/src/react/components/popup/popup.tsx b/src/react/components/popup/popup.tsx index 6fee561223..d5146bd437 100644 --- a/src/react/components/popup/popup.tsx +++ b/src/react/components/popup/popup.tsx @@ -39,6 +39,10 @@ export class Popup extends SurveyElementBase { super.componentDidUpdate(prevProps, prevState); this.setTargetElement(); } + componentWillUnmount(): void { + super.componentWillUnmount(); + this.popup.resetComponentElement(); + } shouldComponentUpdate(nextProps: IPopupProps, nextState: any) { if (!super.shouldComponentUpdate(nextProps, nextState)) return false; const isNeedUpdate = nextProps.model !== this.popup.model; diff --git a/src/react/reactSurvey.tsx b/src/react/reactSurvey.tsx index 582897f0f2..2370e059fe 100644 --- a/src/react/reactSurvey.tsx +++ b/src/react/reactSurvey.tsx @@ -71,6 +71,8 @@ export class Survey extends SurveyElementBase } destroySurvey() { if (this.survey) { + this.survey.renderCallback = undefined as any; + this.survey.onPartialSend.clear(); this.survey.stopTimer(); this.survey.destroyResizeObserver(); } @@ -149,13 +151,13 @@ export class Survey extends SurveyElementBase protected renderCompletedBefore(): JSX.Element { var htmlValue = { __html: this.survey.processedCompletedBeforeHtml }; return ( -
+
); } protected renderLoading(): JSX.Element { var htmlValue = { __html: this.survey.processedLoadingHtml }; return ( -
+
); } protected renderSurvey(): JSX.Element { diff --git a/src/react/reactquestion.tsx b/src/react/reactquestion.tsx index b0387eff94..ee736af64b 100644 --- a/src/react/reactquestion.tsx +++ b/src/react/reactquestion.tsx @@ -5,7 +5,6 @@ import { SurveyError, Question, QuestionMatrixDropdownRenderedCell, - TooltipManager, SurveyModel } from "survey-core"; import { ReactSurveyElementsWrapper } from "./reactsurveymodel"; @@ -112,10 +111,6 @@ export class SurveyQuestion extends SurveyElementBase { : null; var comment = question && question.hasComment ? this.renderComment(cssClasses) : null; - const errorsTooltip = - this.question.isErrorsModeTooltip - ? this.renderErrors(cssClasses, "tooltip") - : null; var descriptionUnderInput = question.hasDescriptionUnderInput ? this.renderDescription() : null; @@ -129,7 +124,6 @@ export class SurveyQuestion extends SurveyElementBase { {questionRender} {comment} {errorsBottom} - {errorsTooltip} {descriptionUnderInput}
); @@ -236,7 +230,6 @@ export class SurveyElementErrors extends ReactSurveyElement { constructor(props: any) { super(props); this.state = this.getState(); - this.tooltipRef = React.createRef(); } protected get id(): string { return this.props.element.id + "_errors"; @@ -256,27 +249,7 @@ export class SurveyElementErrors extends ReactSurveyElement { protected canRender(): boolean { return !!this.element && this.element.hasVisibleErrors; } - private tooltipManager: TooltipManager | undefined; - private tooltipRef: React.RefObject; - componentDidUpdate(prevProps: any, prevState: any) { - super.componentDidUpdate(prevProps, prevState); - if (this.props.location == "tooltip") { - if (this.tooltipRef.current && !this.tooltipManager) { - this.tooltipManager = new TooltipManager(this.tooltipRef.current); - } - if (!!this.tooltipManager && !this.tooltipRef.current) { - this.disposeTooltipManager(); - } - } - } componentWillUnmount() { - if (!!this.tooltipManager) { - this.disposeTooltipManager(); - } - } - private disposeTooltipManager() { - this.tooltipManager?.dispose(); - this.tooltipManager = undefined; } protected renderElement(): JSX.Element { const errors: Array = []; @@ -293,7 +266,6 @@ export class SurveyElementErrors extends ReactSurveyElement { aria-live="polite" className={this.element.cssError} id={this.id} - ref={this.tooltipRef} > {errors}
@@ -333,31 +305,11 @@ export abstract class SurveyQuestionAndErrorsWrapped extends ReactSurveyElement protected canRender(): boolean { return !!this.question; } - protected renderErrors(errorsLocation: string) { - return this.getShowErrors() ? ( - - ) : null; - } protected renderContent(): JSX.Element { - var errorsLocation = this.creator.questionErrorLocation(); - var errors = this.renderErrors(errorsLocation); - var errorsTop = this.question.showErrorOnTop - ? errors - : null; - var errorsBottom = this.question.showErrorOnBottom - ? errors - : null; var renderedQuestion = this.renderQuestion(); return ( <> - {errorsTop} {renderedQuestion} - {errorsBottom} ); } @@ -430,3 +382,45 @@ export class SurveyQuestionAndErrorsCell extends SurveyQuestionAndErrorsWrapped return wrapper ?? element; } } + +export class SurveyQuestionErrorCell extends React.Component { + constructor(props: any) { + super(props); + this.state = { + changed: 0 + }; + if(this.question) { + this.registerCallback(this.question); + } + } + private get question(): Question { + return this.props.question; + } + private update() { + this.setState({ changed: this.state.changed + 1 }); + } + private registerCallback(question: Question) { + question.registerFunctionOnPropertyValueChanged("errors", () => { + this.update(); + }, "__reactSubscription"); + } + private unRegisterCallback(question: Question) { + question.unRegisterFunctionOnPropertyValueChanged("errors", "__reactSubscription"); + } + componentDidUpdate(prevProps: Readonly): void { + if(prevProps.question && prevProps.question !== this.question) { + this.unRegisterCallback(prevProps.cell); + } + if(this.question) { + this.registerCallback(this.question); + } + } + componentWillUnmount(): void { + if(this.question) { + this.unRegisterCallback(this.question); + } + } + render(): JSX.Element { + return ; + } +} diff --git a/src/react/reactquestion_matrixdropdownbase.tsx b/src/react/reactquestion_matrixdropdownbase.tsx index 846f1f89cb..83d7cfa458 100644 --- a/src/react/reactquestion_matrixdropdownbase.tsx +++ b/src/react/reactquestion_matrixdropdownbase.tsx @@ -3,7 +3,7 @@ import { ReactSurveyElement, SurveyQuestionElementBase } from "./reactquestion_element"; -import { SurveyElementErrors, SurveyQuestion, SurveyQuestionAndErrorsCell } from "./reactquestion"; +import { SurveyElementErrors, SurveyQuestion, SurveyQuestionAndErrorsCell, SurveyQuestionErrorCell } from "./reactquestion"; import { QuestionMatrixDropdownModelBase, QuestionMatrixDropdownRenderedRow, @@ -238,11 +238,11 @@ export class SurveyQuestionMatrixDropdownBase extends SurveyQuestionElementBase if(cell.isErrorsCell) { if (cell.isErrorsCell) { return ( - - + ); } } @@ -399,46 +399,4 @@ export class SurveyQuestionMatrixDropdownCell extends SurveyQuestionAndErrorsCel /> ); } -} - -export class SurveyQuestionMatrixDropdownErrorCell extends React.Component { - constructor(props: any) { - super(props); - this.state = { - changed: 0 - }; - if(this.cell) { - this.registerCallback(this.cell); - } - } - private get cell(): QuestionMatrixDropdownRenderedCell { - return this.props.cell; - } - private update() { - this.setState({ changed: this.state.changed + 1 }); - } - private registerCallback(cell: QuestionMatrixDropdownRenderedCell) { - cell.question.registerFunctionOnPropertyValueChanged("errors", () => { - this.update(); - }, "__reactSubscription"); - } - private unRegisterCallback(cell: QuestionMatrixDropdownRenderedCell) { - cell.question.unRegisterFunctionOnPropertyValueChanged("errors", "__reactSubscription"); - } - componentDidUpdate(prevProps: Readonly): void { - if(prevProps.cell && prevProps.cell !== this.cell) { - this.unRegisterCallback(prevProps.cell); - } - if(this.cell) { - this.registerCallback(this.cell); - } - } - componentWillUnmount(): void { - if(this.cell) { - this.unRegisterCallback(this.cell); - } - } - render(): JSX.Element { - return ; - } -} +} \ No newline at end of file diff --git a/src/react/reactquestion_multipletext.tsx b/src/react/reactquestion_multipletext.tsx index 6513df6832..a8da57a5dd 100644 --- a/src/react/reactquestion_multipletext.tsx +++ b/src/react/reactquestion_multipletext.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { SurveyQuestionElementBase } from "./reactquestion_element"; -import { ISurveyCreator, SurveyElementErrors, SurveyQuestionAndErrorsWrapped } from "./reactquestion"; -import { QuestionMultipleTextModel, MultipleTextItemModel } from "survey-core"; +import { ISurveyCreator, SurveyQuestionAndErrorsWrapped, SurveyQuestionErrorCell } from "./reactquestion"; +import { QuestionMultipleTextModel, MultipleTextItemModel, MultipleTextCell } from "survey-core"; import { ReactQuestionFactory } from "./reactquestion_factory"; import { ReactSurveyElement } from "./reactquestion_element"; import { TitleContent } from "./components/title/title-content"; @@ -18,7 +18,9 @@ export class SurveyQuestionMultipleText extends SurveyQuestionElementBase { var tableRows = this.question.getRows(); var rows:Array = []; for (var i = 0; i < tableRows.length; i++) { - rows.push(this.renderRow(i, tableRows[i], cssClasses)); + if(tableRows[i].isVisible) { + rows.push(this.renderRow(i, tableRows[i].cells, cssClasses)); + } } return ( @@ -26,20 +28,29 @@ export class SurveyQuestionMultipleText extends SurveyQuestionElementBase {
); } + + protected renderCell(cell: MultipleTextCell, cssClasses: any, index: number): JSX.Element { + let cellContent: JSX.Element; + const focusIn = () => { cell.item.focusIn(); }; + if(cell.isErrorsCell) { + cellContent = ; + } else { + cellContent = ; + } + return ({cellContent}); + } + protected renderRow( rowIndex: number, - items: Array, + cells: Array, cssClasses: any ): JSX.Element { const key: string = "item" + rowIndex; const tds:Array = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const focusIn = () => { item.focusIn(); }; + for (let i = 0; i < cells.length; i++) { + const cell = cells[i]; tds.push( - - - + this.renderCell(cell, cssClasses, i) ); } return ( @@ -77,20 +88,8 @@ export class SurveyMultipleTextItem extends ReactSurveyElement { question={item.editor} creator={this.creator} /> - {this.renderItemTooltipError(item, cssClasses)} ); } - - protected renderItemTooltipError(item: MultipleTextItemModel, cssClasses: any): JSX.Element | null { - return this.item.editor.isErrorsModeTooltip ? ( - - ): null; - } } export class SurveyMultipleTextItemEditor extends SurveyQuestionAndErrorsWrapped { diff --git a/src/survey-element.ts b/src/survey-element.ts index bdb3f17a24..fef71e940a 100644 --- a/src/survey-element.ts +++ b/src/survey-element.ts @@ -783,19 +783,6 @@ export class SurveyElement extends SurveyElementCore implements ISurvey return this.survey && this.survey.getCss().root == "sd-root-modern"; } - public get isErrorsModeTooltip(): boolean { - return this.getIsErrorsModeTooltip(); - } - protected getIsErrorsModeTooltip() { - return this.isDefaultV2Theme && this.hasParent && this.getIsTooltipErrorSupportedByParent(); - } - protected getIsTooltipErrorSupportedByParent(): boolean { - return (this.parent)?.getIsTooltipErrorInsideSupported(); - } - protected getIsTooltipErrorInsideSupported(): boolean { - return false; - } - public get hasParent() { return (this.parent && !this.parent.isPage && (!(this.parent).originalPage || (this.survey).isShowingPreview)) || (this.parent === undefined); } @@ -975,4 +962,10 @@ export class SurveyElement extends SurveyElementCore implements ISurvey }); } } + public dispose() { + super.dispose(); + if(this.titleToolbarValue) { + this.titleToolbarValue.dispose(); + } + } } diff --git a/src/survey-events-api.ts b/src/survey-events-api.ts index 669a7f1e48..aa0de435aa 100644 --- a/src/survey-events-api.ts +++ b/src/survey-events-api.ts @@ -1,6 +1,6 @@ import { IAction } from "./actions/action"; import { Base } from "./base"; -import { IElement, ISurveyElement } from "./base-interfaces"; +import { IElement, ISurveyElement, IValueItemCustomPropValues } from "./base-interfaces"; import { ItemValue } from "./itemvalue"; import { PageModel } from "./page"; import { PanelModel, PanelModelBase } from "./panel"; @@ -618,7 +618,7 @@ export interface GetChoiceDisplayValueEvent extends QuestionEventMixin { /** * A method that you should call to assign display texts to the question. */ - setItems: (displayValues: Array) => void; + setItems: (displayValues: Array, ...customValues: Array) => void; /** * An array of one (in Dropdown) or more (in Tag Box) default values. */ @@ -814,6 +814,20 @@ export interface DynamicPanelItemValueChangedEvent extends PanelDynamicQuestionE */ panel: PanelModel; } +export interface DynamicPanelGetTabTitleEvent extends PanelDynamicQuestionEventMixin { + /** + * A panel whose tab title is being rendered. + */ + panel: PanelModel; + /** + * The panel's index in the [`visiblePanels`](https://surveyjs.io/form-library/documentation/api-reference/dynamic-panel-model#visiblePanels) array of the Dynamic Panel. + */ + visiblePanelIndex: number; + /** + * A tab title. You can change this parameter's value. + */ + title: string; +} export interface IsAnswerCorrectEvent extends QuestionEventMixin { /** * you may change the default number of correct or incorrect answers in the question, for example for matrix, where each row is a quiz question diff --git a/src/survey.ts b/src/survey.ts index f1dac094c0..cbb9c9f786 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -16,7 +16,8 @@ import { IFindElement, ISurveyLayoutElement, IPlainDataOptions, - LayoutElementContainer + LayoutElementContainer, + IValueItemCustomPropValues } from "./base-interfaces"; import { SurveyElementCore, SurveyElement } from "./survey-element"; import { surveyCss } from "./defaultCss/defaultV2Css"; @@ -49,7 +50,19 @@ import { ActionContainer, defaultActionBarCss } from "./actions/container"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { QuestionPanelDynamicModel } from "./question_paneldynamic"; import { Notifier } from "./notifier"; -import { TriggerExecutedEvent, CompletingEvent, CompleteEvent, ShowingPreviewEvent, NavigateToUrlEvent, CurrentPageChangingEvent, CurrentPageChangedEvent, ValueChangingEvent, ValueChangedEvent, VariableChangedEvent, QuestionVisibleChangedEvent, PageVisibleChangedEvent, PanelVisibleChangedEvent, QuestionCreatedEvent, QuestionAddedEvent, QuestionRemovedEvent, PanelAddedEvent, PanelRemovedEvent, PageAddedEvent, ValidateQuestionEvent, SettingQuestionErrorsEvent, ValidatePanelEvent, ErrorCustomTextEvent, ValidatedErrorsOnCurrentPageEvent, ProcessHtmlEvent, GetQuestionTitleEvent, GetTitleTagNameEvent, GetQuestionNoEvent, ProgressTextEvent, TextMarkdownEvent, TextRenderAsEvent, SendResultEvent, GetResultEvent, UploadFilesEvent, DownloadFileEvent, ClearFilesEvent, LoadChoicesFromServerEvent, ProcessTextValueEvent, UpdateQuestionCssClassesEvent, UpdatePanelCssClassesEvent, UpdatePageCssClassesEvent, UpdateChoiceItemCssEvent, AfterRenderSurveyEvent, AfterRenderHeaderEvent, AfterRenderPageEvent, AfterRenderQuestionEvent, AfterRenderQuestionInputEvent, AfterRenderPanelEvent, FocusInQuestionEvent, FocusInPanelEvent, ShowingChoiceItemEvent, ChoicesLazyLoadEvent, GetChoiceDisplayValueEvent, MatrixRowAddedEvent, MatrixBeforeRowAddedEvent, MatrixRowRemovingEvent, MatrixRowRemovedEvent, MatrixAllowRemoveRowEvent, MatrixCellCreatingEvent, MatrixCellCreatedEvent, MatrixAfterCellRenderEvent, MatrixCellValueChangedEvent, MatrixCellValueChangingEvent, MatrixCellValidateEvent, DynamicPanelModifiedEvent, DynamicPanelRemovingEvent, TimerPanelInfoTextEvent, DynamicPanelItemValueChangedEvent, IsAnswerCorrectEvent, DragDropAllowEvent, ScrollingElementToTopEvent, GetQuestionTitleActionsEvent, GetPanelTitleActionsEvent, GetPageTitleActionsEvent, GetPanelFooterActionsEvent, GetMatrixRowActionsEvent, ElementContentVisibilityChangedEvent, GetExpressionDisplayValueEvent, ServerValidateQuestionsEvent, MultipleTextItemAddedEvent, MatrixColumnAddedEvent, GetQuestionDisplayValueEvent, PopupVisibleChangedEvent } from "./survey-events-api"; +import { TriggerExecutedEvent, CompletingEvent, CompleteEvent, ShowingPreviewEvent, NavigateToUrlEvent, CurrentPageChangingEvent, CurrentPageChangedEvent, + ValueChangingEvent, ValueChangedEvent, VariableChangedEvent, QuestionVisibleChangedEvent, PageVisibleChangedEvent, PanelVisibleChangedEvent, QuestionCreatedEvent, + QuestionAddedEvent, QuestionRemovedEvent, PanelAddedEvent, PanelRemovedEvent, PageAddedEvent, ValidateQuestionEvent, SettingQuestionErrorsEvent, ValidatePanelEvent, + ErrorCustomTextEvent, ValidatedErrorsOnCurrentPageEvent, ProcessHtmlEvent, GetQuestionTitleEvent, GetTitleTagNameEvent, GetQuestionNoEvent, ProgressTextEvent, + TextMarkdownEvent, TextRenderAsEvent, SendResultEvent, GetResultEvent, UploadFilesEvent, DownloadFileEvent, ClearFilesEvent, LoadChoicesFromServerEvent, + ProcessTextValueEvent, UpdateQuestionCssClassesEvent, UpdatePanelCssClassesEvent, UpdatePageCssClassesEvent, UpdateChoiceItemCssEvent, AfterRenderSurveyEvent, + AfterRenderHeaderEvent, AfterRenderPageEvent, AfterRenderQuestionEvent, AfterRenderQuestionInputEvent, AfterRenderPanelEvent, FocusInQuestionEvent, FocusInPanelEvent, + ShowingChoiceItemEvent, ChoicesLazyLoadEvent, GetChoiceDisplayValueEvent, MatrixRowAddedEvent, MatrixBeforeRowAddedEvent, MatrixRowRemovingEvent, MatrixRowRemovedEvent, + MatrixAllowRemoveRowEvent, MatrixCellCreatingEvent, MatrixCellCreatedEvent, MatrixAfterCellRenderEvent, MatrixCellValueChangedEvent, MatrixCellValueChangingEvent, + MatrixCellValidateEvent, DynamicPanelModifiedEvent, DynamicPanelRemovingEvent, TimerPanelInfoTextEvent, DynamicPanelItemValueChangedEvent, DynamicPanelGetTabTitleEvent, + IsAnswerCorrectEvent, DragDropAllowEvent, ScrollingElementToTopEvent, GetQuestionTitleActionsEvent, GetPanelTitleActionsEvent, GetPageTitleActionsEvent, + GetPanelFooterActionsEvent, GetMatrixRowActionsEvent, ElementContentVisibilityChangedEvent, GetExpressionDisplayValueEvent, ServerValidateQuestionsEvent, + MultipleTextItemAddedEvent, MatrixColumnAddedEvent, GetQuestionDisplayValueEvent, PopupVisibleChangedEvent } from "./survey-events-api"; import { QuestionMatrixDropdownModelBase } from "./question_matrixdropdownbase"; import { QuestionMatrixDynamicModel } from "./question_matrixdynamic"; import { QuestionFileModel } from "./question_file"; @@ -179,7 +192,7 @@ export class SurveyModel extends SurveyElementCore * * For information on event handler parameters, refer to descriptions within the interface. * - * [Continue an Incomplete Survey](https://surveyjs.io/form-library/documentation/handle-survey-results-continue-incomplete (linkStyle)) + * Alternatively, you can handle the [`onCurrentPageChanged`](#onCurrentPageChanged) and [`onValueChanged`](#onValueChanged) events, as shown in the following demo: [Continue an Incomplete Survey](https://surveyjs.io/form-library/examples/survey-editprevious/). */ public onPartialSend: EventBase = this.addEvent(); /** @@ -204,7 +217,7 @@ export class SurveyModel extends SurveyElementCore */ public onValueChanging: EventBase = this.addEvent(); /** - * An event that is raised after a question value is changed + * An event that is raised after a question value is changed. * * For information on event handler parameters, refer to descriptions within the interface. * @@ -724,6 +737,11 @@ export class SurveyModel extends SurveyElementCore */ public onDynamicPanelItemValueChanged: EventBase = this.addEvent(); + /** + * An event that is raised before a [Dynamic Panel](https://surveyjs.io/form-library/examples/questiontype-paneldynamic/) renders [tab titles](https://surveyjs.io/form-library/documentation/api-reference/dynamic-panel-model#templateTabTitle). Use this event to change individual tab titles. + */ + public onGetDynamicPanelTabTitle: EventBase = this.addEvent(); + /** * Use this event to define, whether an answer to a question is correct or not. * @see Question.value @@ -927,6 +945,16 @@ export class SurveyModel extends SurveyElementCore this.notifier = new Notifier(this.css.saveData); this.notifier.addAction(this.createTryAgainAction(), "error"); + this.onPopupVisibleChanged.add((_, opt) => { + if(opt.visible) { + this.onScrollCallback = () => { + opt.popup.toggleVisibility(); + }; + } else { + this.onScrollCallback = undefined; + } + }); + this.layoutElements.push({ id: "timerpanel", template: "survey-timerpanel", @@ -1049,6 +1077,14 @@ export class SurveyModel extends SurveyElementCore this.containerCss = this.css.container; this.completedCss = new CssClassBuilder().append(this.css.body) .append(this.css.completedPage).toString(); // for completed page + this.completedBeforeCss = new CssClassBuilder() + .append(this.css.body) + .append(this.css.completedBeforePage) + .toString(); + this.loadingBodyCss = new CssClassBuilder() + .append(this.css.body) + .append(this.css.bodyLoading) + .toString(); } private updateCss() { this.rootCss = this.getRootCss(); @@ -1134,6 +1170,8 @@ export class SurveyModel extends SurveyElementCore return this.css.bodyContainer; } @property() completedCss: string; + @property() completedBeforeCss: string; + @property() loadingBodyCss: string; @property() containerCss: string; @property({ onSet: (newValue, target: SurveyModel) => { target.updateCss(); } }) fitToContainer: boolean; @@ -1247,7 +1285,7 @@ export class SurveyModel extends SurveyElementCore /** * Specifies whether to save survey results when respondents switch between pages. Handle the [`onPartialSend`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#onPartialSend) event to implement the save operation. * - * [Continue an Incomplete Survey](https://surveyjs.io/form-library/documentation/handle-survey-results-continue-incomplete (linkStyle)) + * Alternatively, you can handle the [`onCurrentPageChanged`](#onCurrentPageChanged) and [`onValueChanged`](#onValueChanged) events, as shown in the following demo: [Continue an Incomplete Survey](https://surveyjs.io/form-library/examples/survey-editprevious/). */ public get sendResultOnPageNext(): boolean { return this.getPropertyValue("sendResultOnPageNext"); @@ -2824,6 +2862,12 @@ export class SurveyModel extends SurveyElementCore var key = keys[i]; values[key] = this.getDataValueCore(this.valuesHash, key); } + this.getAllQuestions().forEach(q => { + if(q.hasFilteredValue) { + values[q.getValueName()] = q.getFilteredValue(); + } + }); + return values; } private addCalculatedValuesIntoFilteredValues(values: { @@ -4537,6 +4581,7 @@ export class SurveyModel extends SurveyElementCore htmlElement: htmlElement, }); this.rootElement = htmlElement; + this.addScrollEventListener(); } private processResponsiveness(width: number, mobileWidth: number): boolean { const isMobile = width < mobileWidth; @@ -4654,7 +4699,7 @@ export class SurveyModel extends SurveyElementCore loadQuestionChoices(options: { question: Question, filter: string, skip: number, take: number, setItems: (items: Array, totalCount: number) => void }): void { this.onChoicesLazyLoad.fire(this, options); } - getChoiceDisplayValue(options: { question: Question, values: Array, setItems: (displayValues: Array) => void }): void { + getChoiceDisplayValue(options: { question: Question, values: Array, setItems: (displayValues: Array, ...customValues: Array) => void }): void { if(this.onGetChoiceDisplayValue.isEmpty) { options.setItems(null); } else { @@ -4785,12 +4830,16 @@ export class SurveyModel extends SurveyElementCore this.onDynamicPanelRemoving.fire(this, options); return options.allow; } - dynamicPanelItemValueChanged(question: IQuestion, options: any) { + dynamicPanelItemValueChanged(question: IQuestion, options: any): void { options.question = question; options.panelIndex = options.itemIndex; options.panelData = options.itemValue; this.onDynamicPanelItemValueChanged.fire(this, options); } + dynamicPanelGetTabTitle(question: IQuestion, options: any): void { + options.question = question; + this.onGetDynamicPanelTabTitle.fire(this, options); + } dragAndDropAllow(options: DragDropAllowEvent): boolean { this.onDragDropAllow.fire(this, options); return options.allow; @@ -5097,9 +5146,11 @@ export class SurveyModel extends SurveyElementCore } } /** - * Returns a question by its name. - * @param name a question name - * @param caseInsensitive + * Returns a question with a specified [`name`](https://surveyjs.io/form-library/documentation/api-reference/question#name). + * @param name A question name + * @param caseInsensitive (Optional) A Boolean value that specifies case sensitivity when searching for the question. Default value: `false` (uppercase and lowercase letters are treated as distinct). + * @returns A question with a specified name. + * @see getAllQuestions * @see getQuestionByValueName */ public getQuestionByName( @@ -5121,27 +5172,29 @@ export class SurveyModel extends SurveyElementCore return this.getQuestionByName(name); } /** - * Returns a question by its value name - * @param valueName a question name - * @param caseInsensitive + * Returns a question with a specified [`valueName`](https://surveyjs.io/form-library/documentation/api-reference/question#valueName). + * + * > Since `valueName` does not have to be unique, multiple questions can have the same `valueName` value. In this case, the `getQuestionByValueName()` method returns the first such question. If you need to get all questions with the same `valueName`, call the `getQuestionsByValueName()` method. + * @param valueName A question's `valueName` property value. + * @param caseInsensitive (Optional) A Boolean value that specifies case sensitivity when searching for the question. Default value: `false` (uppercase and lowercase letters are treated as distinct). + * @returns A question with a specified `valueName`. + * @see getAllQuestions * @see getQuestionByName - * @see getQuestionsByValueName - * @see Question.valueName */ public getQuestionByValueName( valueName: string, caseInsensitive: boolean = false - ): IQuestion { + ): Question { var res = this.getQuestionsByValueName(valueName, caseInsensitive); return !!res ? res[0] : null; } /** - * Returns all questions by their valueName. name property is used if valueName property is empty. - * @param valueName a question name - * @param caseInsensitive + * Returns all questions with a specified [`valueName`](https://surveyjs.io/form-library/documentation/api-reference/question#valueName). If a question's `valueName` is undefined, its [`name`](https://surveyjs.io/form-library/documentation/api-reference/question#name) property is used. + * @param valueName A question's `valueName` property value. + * @param caseInsensitive (Optional) A Boolean value that specifies case sensitivity when searching for the questions. Default value: `false` (uppercase and lowercase letters are treated as distinct). + * @returns An array of questions with a specified `valueName`. + * @see getAllQuestions * @see getQuestionByName - * @see getQuestionByValueName - * @see Question.valueName */ public getQuestionsByValueName( valueName: string, @@ -5162,9 +5215,11 @@ export class SurveyModel extends SurveyElementCore return null; } /** - * Gets a list of questions by their names. - * @param names an array of question names - * @param caseInsensitive + * Returns an array of questions with specified [names](https://surveyjs.io/form-library/documentation/api-reference/question#name). + * @param names An array of question names. + * @param caseInsensitive (Optional) A Boolean value that specifies case sensitivity when searching for the questions. Default value: `false` (uppercase and lowercase letters are treated as distinct). + * @returns An array of questions with specified names + * @see getAllQuestions */ public getQuestionsByNames( names: string[], @@ -5222,10 +5277,12 @@ export class SurveyModel extends SurveyElementCore return result; } /** - * Returns a list of all questions in the survey. + * Returns a list of all [questions](https://surveyjs.io/form-library/documentation/api-reference/question) in the survey. * @param visibleOnly A Boolean value that specifies whether to include only visible questions. * @param includeDesignTime For internal use. * @param includeNested A Boolean value that specifies whether to include nested questions, such as questions within matrix cells. + * @returns An array of questions. + * @see getQuestionByName */ public getAllQuestions( visibleOnly: boolean = false, @@ -5250,7 +5307,10 @@ export class SurveyModel extends SurveyElementCore return res2; } /** - * Returns quiz questions. All visible questions that has input(s) widgets. + * Returns an array of quiz questions. A question counts if it is visible, has an input field, and specifies [`correctAnswer`](https://surveyjs.io/form-library/documentation/api-reference/checkbox-question-model#correctAnswer). + * + * For more information about quizzes, refer to the following tutorial: [Create a Quiz](https://surveyjs.io/form-library/documentation/design-survey/create-a-quiz). + * @returns An array of quiz questions. * @see getQuizQuestionCount */ public getQuizQuestions(): Array { @@ -5269,10 +5329,11 @@ export class SurveyModel extends SurveyElementCore return result; } /** - * Returns a panel by its name. - * @param name a panel name - * @param caseInsensitive - * @see getQuestionByName + * Returns a [panel](https://surveyjs.io/form-library/documentation/api-reference/panel-model) with a specified [`name`](https://surveyjs.io/form-library/documentation/api-reference/panel-model#name). + * @param name A panel name. + * @param caseInsensitive (Optional) A Boolean value that specifies case sensitivity when searching for the panel. Default value: `false` (uppercase and lowercase letters are treated as distinct). + * @returns A panel with a specified name. + * @see getAllPanels */ public getPanelByName( name: string, @@ -5288,7 +5349,11 @@ export class SurveyModel extends SurveyElementCore return null; } /** - * Returns a list of all survey's panels. + * Returns a list of all [panels](https://surveyjs.io/form-library/documentation/api-reference/panel-model) in the survey. + * @param visibleOnly A Boolean value that specifies whether to include only visible panels. + * @param includeDesignTime For internal use. + * @returns An array of panels. + * @see getPanelByName */ public getAllPanels( visibleOnly: boolean = false, @@ -5986,7 +6051,7 @@ export class SurveyModel extends SurveyElementCore textValue.value = processor.getValue(textValue.name, data); textValue.isExists = processor.hasValue(textValue.name, data); } - private getFirstName(name: string): IQuestion { + private getFirstName(name: string): Question { name = name.toLowerCase(); var question; do { @@ -6030,9 +6095,13 @@ export class SurveyModel extends SurveyElementCore } } /** - * Returns a variable value. Variable, unlike values, are not stored in the survey results. - * @param name A variable name - * @see SetVariable + * Returns a variable value. + * + * [Variables help topic](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#variables (linkStyle)) + * @param name A variable name. + * @return A variable value. + * @see setVariable + * @see getVariableNames */ public getVariable(name: string): any { if (!name) return null; @@ -6046,10 +6115,13 @@ export class SurveyModel extends SurveyElementCore return res; } /** - * Sets a variable value. Variable, unlike values, are not stored in the survey results. - * @param name A variable name - * @param newValue A variable new value - * @see GetVariable + * Sets a variable value. + * + * [Variables help topic](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#variables (linkStyle)) + * @param name A variable name. + * @param newValue A new variable value. + * @see getVariable + * @see getVariableNames */ public setVariable(name: string, newValue: any): void { if (!name) return; @@ -6063,7 +6135,10 @@ export class SurveyModel extends SurveyElementCore this.onVariableChanged.fire(this, { name: name, value: newValue }); } /** - * Returns all variables in the survey. Use setVariable function to create a new variable. + * Returns the names of all variables in the survey. + * + * [Variables help topic](https://surveyjs.io/form-library/documentation/design-survey/conditional-logic#variables (linkStyle)) + * @returns An array of variable names. * @see getVariable * @see setVariable */ @@ -6080,8 +6155,9 @@ export class SurveyModel extends SurveyElementCore return Helpers.getUnbindValue(value); } /** - * Returns a question value (answer) by a question's name. - * @param name A question name + * Returns a value (answer) for a question with a specified `name`. + * @param name A question name. + * @returns A question value (answer). * @see data * @see setValue */ @@ -6091,16 +6167,15 @@ export class SurveyModel extends SurveyElementCore return this.getUnbindValue(value); } /** - * Sets a question value (answer). It runs all triggers and conditions (`visibleIf` properties). + * Sets a question value (answer). * - * Goes to the next page if `goNextPageAutomatic` is `true` and all questions on the current page are answered correctly. - * @param name A question name - * @param newValue A new question value + * > This method executes all triggers and reevaluates conditions (`visibleIf`, `requiredId`, and others). It also switches the survey to the next page if the [`goNextPageAutomatic`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#goNextPageAutomatic) property is enabled and all questions on the current page have correct answers. + * @param name A question name. + * @param newValue A new question value. + * @param locNotification For internal use. + * @param allowNotifyValueChanged For internal use. * @see data * @see getValue - * @see PageModel.visibleIf - * @see Question.visibleIf - * @see goNextPageAutomatic */ public setValue( name: string, @@ -6124,7 +6199,7 @@ export class SurveyModel extends SurveyElementCore ) return; var oldValue = this.getValue(name); - if (this.isValueEmpty(newValue, false)) { + if (this.isValueEmpyOnSetValue(name, newValue)) { this.deleteDataValueCore(this.valuesHash, name); } else { newValue = this.getUnbindValue(newValue); @@ -6138,6 +6213,11 @@ export class SurveyModel extends SurveyElementCore allowNotifyValueChanged ); } + private isValueEmpyOnSetValue(name: string, val: any): boolean { + if(!this.isValueEmpty(val, false)) return false; + if(!this.editingObj || val === null || val === undefined) return true; + return this.editingObj.getDefaultPropertyValue(name) === val; + } private updateOnSetValue( name: string, newValue: any, @@ -6232,8 +6312,9 @@ export class SurveyModel extends SurveyElementCore } } /** - * Returns the comment value. - * @param name A comment's name. + * Returns a comment value from a question with a specified `name`. + * @param name A question name. + * @returns A comment. * @see setComment */ public getComment(name: string): string { @@ -6241,9 +6322,10 @@ export class SurveyModel extends SurveyElementCore return res || ""; } /** - * Sets a comment value. - * @param name A comment name. + * Sets a comment value to a question with a specified `name`. + * @param name A question name. * @param newValue A new comment value. + * @param locNotification For internal use. * @see getComment */ public setComment( @@ -6566,13 +6648,23 @@ export class SurveyModel extends SurveyElementCore return this.getCorrectedAnswerCountCore(true); } /** - * Returns an amount of corrected quiz answers. + * Returns the number of correct answers in a quiz. + * + * For more information about quizzes, refer to the following tutorial: [Create a Quiz](https://surveyjs.io/form-library/documentation/design-survey/create-a-quiz). + * @returns The number of correct answers in a quiz. + * @see getQuizQuestionCount + * @see getInCorrectAnswerCount */ public getCorrectAnswerCount(): number { return this.getCorrectedAnswerCountCore(true); } /** - * Returns quiz question number. It may be different from `getQuizQuestions.length` because some widgets like matrix may have several questions. + * Returns the number of quiz questions. A question counts if it is visible, has an input field, and specifies [`correctAnswer`](https://surveyjs.io/form-library/documentation/api-reference/checkbox-question-model#correctAnswer). + * + * This number may be different from `getQuizQuestions().length` because certain question types (for instance, matrix-like types) include more than one question. + * + * For more information about quizzes, refer to the following tutorial: [Create a Quiz](https://surveyjs.io/form-library/documentation/design-survey/create-a-quiz). + * @returns The number of quiz questions. * @see getQuizQuestions */ public getQuizQuestionCount(): number { @@ -6587,7 +6679,11 @@ export class SurveyModel extends SurveyElementCore return this.getCorrectedAnswerCountCore(false); } /** - * Returns an amount of incorrect quiz answers. + * Returns the number of incorrect answers in a quiz. + * + * For more information about quizzes, refer to the following tutorial: [Create a Quiz](https://surveyjs.io/form-library/documentation/design-survey/create-a-quiz). + * @returns The number of incorrect answers in a quiz. + * @see getCorrectAnswerCount */ public getInCorrectAnswerCount(): number { return this.getCorrectedAnswerCountCore(false); @@ -7227,6 +7323,7 @@ export class SurveyModel extends SurveyElementCore * Use this method to dispose survey model properly. */ public dispose() { + this.rootElement = undefined; this.currentPage = null; this.destroyResizeObserver(); super.dispose(); @@ -7240,8 +7337,25 @@ export class SurveyModel extends SurveyElementCore if (this.disposeCallback) { this.disposeCallback(); } + this.removeScrollEventListener(); } disposeCallback: () => void; + + private onScrollCallback: () => void; + public onScroll(): void { + if(this.onScrollCallback) { + this.onScrollCallback(); + } + } + public addScrollEventListener(): void { + this.scrollHandler = () => { this.onScroll(); }; + this.rootElement.addEventListener("scroll", this.scrollHandler); + } + public removeScrollEventListener(): void { + if (!!this.rootElement && !!this.scrollHandler) { + this.rootElement.removeEventListener("scroll", this.scrollHandler); + } + } } function isStrCiEqual(a: string, b: string) { diff --git a/src/utils/tooltip.ts b/src/utils/tooltip.ts deleted file mode 100644 index 951a850f0c..0000000000 --- a/src/utils/tooltip.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class TooltipManager { - private targetElement: HTMLElement; - constructor(public tooltipElement: HTMLElement) { - this.targetElement = tooltipElement.parentElement; - this.targetElement.addEventListener("mousemove", this.onMouseMoveCallback); - } - dispose() { - this.targetElement.removeEventListener("mousemove", this.onMouseMoveCallback); - } - - private onMouseMoveCallback = (e: any) => { - this.tooltipElement.style.left = e.clientX + 12 + "px"; - this.tooltipElement.style.top = e.clientY + 12 + "px"; - } -} \ No newline at end of file diff --git a/src/vue/element.vue b/src/vue/element.vue index af590aefe5..8bce6f9ff4 100644 --- a/src/vue/element.vue +++ b/src/vue/element.vue @@ -45,11 +45,6 @@ :element="element" :location="'bottom'" /> -
this.$el); - } - } - if (!(this.$el instanceof HTMLElement) && !!this.tooltipManager) { - this.tooltipManager.dispose(); - } - } - destroyed() { - if (!!this.tooltipManager) { - this.tooltipManager.dispose(); - } - } } Vue.component("survey-errors", Errors); export default Errors; diff --git a/src/vue/multipletext.vue b/src/vue/multipletext.vue index 21e41eaef6..6bf913852a 100644 --- a/src/vue/multipletext.vue +++ b/src/vue/multipletext.vue @@ -1,18 +1,22 @@ diff --git a/src/vue/multipletextitem.vue b/src/vue/multipletextitem.vue index ac2b6ab4a8..6e297db8c1 100644 --- a/src/vue/multipletextitem.vue +++ b/src/vue/multipletextitem.vue @@ -1,5 +1,5 @@