From ef70ae8cbddabc96a08ab25bc2db729ce7fb7d16 Mon Sep 17 00:00:00 2001 From: Andrew Telnov Date: Thu, 17 Aug 2023 14:11:06 +0300 Subject: [PATCH] confirmActionFunc is not supporting a compromised async function fix #6736 --- src/entries/chunks/model.ts | 1 + src/knockout/koquestion_file.ts | 10 +------- src/question_file.ts | 20 ++++++++------- src/question_matrixdynamic.ts | 10 ++++++-- src/question_paneldynamic.ts | 12 ++++----- src/settings.ts | 10 ++++++++ src/utils/utils.ts | 19 +++++++++++--- tests/question_matrixdynamictests.ts | 31 ++++++++++++++++++++++- tests/surveypaneldynamictests.ts | 37 ++++++++++++++++++++++++++++ 9 files changed, 119 insertions(+), 31 deletions(-) diff --git a/src/entries/chunks/model.ts b/src/entries/chunks/model.ts index 4dcce3f71c..336f80eb8c 100644 --- a/src/entries/chunks/model.ts +++ b/src/entries/chunks/model.ts @@ -239,6 +239,7 @@ export { export { IsMobile, IsTouch, _setIsTouch } from "../../utils/devices"; export { confirmAction, + confirmActionAsync, detectIEOrEdge, doKey2ClickUp, doKey2ClickDown, diff --git a/src/knockout/koquestion_file.ts b/src/knockout/koquestion_file.ts index ea165e54fb..f2139d8f0c 100644 --- a/src/knockout/koquestion_file.ts +++ b/src/knockout/koquestion_file.ts @@ -1,13 +1,5 @@ import * as ko from "knockout"; -import { Serializer, - Question, - QuestionFactory, - QuestionFileModel, - getOriginalEvent, - confirmAction, - detectIEOrEdge, - loadFileFromBase64 -} from "survey-core"; +import { Serializer, QuestionFactory, QuestionFileModel, getOriginalEvent } from "survey-core"; import { QuestionImplementor } from "./koquestion"; class QuestionFileImplementor extends QuestionImplementor { diff --git a/src/question_file.ts b/src/question_file.ts index 306c7d709e..b2cb652ea2 100644 --- a/src/question_file.ts +++ b/src/question_file.ts @@ -3,10 +3,9 @@ import { property, propertyArray, Serializer } from "./jsonobject"; import { QuestionFactory } from "./questionfactory"; import { EventBase } from "./base"; import { UploadingFileError, ExceedSizeError } from "./error"; -import { surveyLocalization } from "./surveyStrings"; import { SurveyError } from "./survey-error"; import { CssClassBuilder } from "./utils/cssClassBuilder"; -import { confirmAction, detectIEOrEdge, loadFileFromBase64 } from "./utils/utils"; +import { confirmActionAsync, detectIEOrEdge, loadFileFromBase64 } from "./utils/utils"; import { ActionContainer } from "./actions/container"; import { Action } from "./actions/action"; import { Helpers } from "./helpers"; @@ -582,11 +581,13 @@ export class QuestionFileModel extends Question { this.onChange(src); } doClean = (event: any) => { - var src = event.currentTarget || event.srcElement; if (this.needConfirmRemoveFile) { - var isConfirmed = confirmAction(this.confirmRemoveAllMessage); - if (!isConfirmed) return; + confirmActionAsync(this.confirmRemoveAllMessage, () => { this.clearFilesCore(); }); + return; } + this.clearFilesCore(); + } + private clearFilesCore(): void { if(this.rootElement) { this.rootElement.querySelectorAll("input")[0].value = ""; } @@ -594,11 +595,12 @@ export class QuestionFileModel extends Question { } doRemoveFile(data: any) { if (this.needConfirmRemoveFile) { - var isConfirmed = confirmAction( - this.getConfirmRemoveMessage(data.name) - ); - if (!isConfirmed) return; + confirmActionAsync(this.getConfirmRemoveMessage(data.name), () => { this.removeFileCore(data); }); + return; } + this.removeFileCore(data); + } + private removeFileCore(data: any): void { const previewIndex = this.previewValue.indexOf(data); this.removeFileByContent(previewIndex === -1 ? data : this.value[previewIndex]); } diff --git a/src/question_matrixdynamic.ts b/src/question_matrixdynamic.ts index 4b539937d9..d22ecf1a13 100644 --- a/src/question_matrixdynamic.ts +++ b/src/question_matrixdynamic.ts @@ -12,7 +12,7 @@ import { SurveyError } from "./survey-error"; import { MinRowCountError } from "./error"; import { IAction } from "./actions/action"; import { settings } from "./settings"; -import { confirmAction } from "./utils/utils"; +import { confirmActionAsync } from "./utils/utils"; import { DragDropMatrixRows } from "./dragdrop/matrix-rows"; import { IShortcutText, ISurveyImpl, IProgressInfo } from "./base-interfaces"; import { CssClassBuilder } from "./utils/cssClassBuilder"; @@ -566,7 +566,13 @@ export class QuestionMatrixDynamicModel extends QuestionMatrixDropdownModelBase if(confirmDelete === undefined) { confirmDelete = this.isRequireConfirmOnRowDelete(index); } - if (confirmDelete && !confirmAction(this.confirmDeleteText)) return; + if (confirmDelete) { + confirmActionAsync(this.confirmDeleteText, () => { this.removeRowAsync(index, row); }); + return; + } + this.removeRowAsync(index, row); + } + private removeRowAsync(index: number, row: MatrixDropdownRowModelBase): void { if (!!row && !!this.survey && !this.survey.matrixRowRemoving(this, index, row)) return; this.onStartRowAddingRemoving(); this.removeRowCore(index); diff --git a/src/question_paneldynamic.ts b/src/question_paneldynamic.ts index cc19a6bd04..66bceca172 100644 --- a/src/question_paneldynamic.ts +++ b/src/question_paneldynamic.ts @@ -1,5 +1,4 @@ import { HashTable, Helpers } from "./helpers"; - import { IElement, IQuestion, @@ -11,8 +10,7 @@ import { IProgressInfo, } from "./base-interfaces"; import { SurveyElement } from "./survey-element"; -import { surveyLocalization } from "./surveyStrings"; -import { ILocalizableOwner, LocalizableString } from "./localizablestring"; +import { LocalizableString } from "./localizablestring"; import { TextPreProcessorValue, QuestionTextProcessor, @@ -23,7 +21,7 @@ import { JsonObject, property, Serializer } from "./jsonobject"; import { QuestionFactory } from "./questionfactory"; import { KeyDuplicationError } from "./error"; import { settings } from "./settings"; -import { confirmAction, mergeValues } from "./utils/utils"; +import { confirmActionAsync } from "./utils/utils"; import { SurveyError } from "./survey-error"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { ActionContainer } from "./actions/container"; @@ -1236,9 +1234,11 @@ export class QuestionPanelDynamicModel extends Question * @see canRemovePanel * */ - public removePanelUI(value: any) { + public removePanelUI(value: any): void { if (!this.canRemovePanel) return; - if (!this.confirmDelete || confirmAction(this.confirmDeleteText)) { + if(this.confirmDelete) { + confirmActionAsync(this.confirmDeleteText, () => { this.removePanel(value); }); + } else { this.removePanel(value); } } diff --git a/src/settings.ts b/src/settings.ts index 489e000d88..1f6a890874 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -488,6 +488,16 @@ export var settings = { confirmActionFunc: function (message: string): boolean { return confirm(message); }, + /** + * A property that allows you to display a custom confirm dialog in async mode instead of the standard browser dialog. Set this property to a function that renders your custom dialog window in async mode. + * @param message A message to be displayed in the confirm dialog window. + * @param callback A callback function that should be called with res paramter equals to true if action is confirmed and equals to false otherwise. + */ + confirmActionAsyncFunc: function (message: string, resFunc: (res: boolean) => void): boolean { + //when you finish with displaying your dialog, call the resFunc as resFunc(true) or resFunc(false). + //You should return true to tell that you use this function + return false; + }, /** * A minimum width value for all survey elements. * diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8047a5f37b..3b5b1ede29 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -19,14 +19,24 @@ function confirmAction(message: string): boolean { return settings.confirmActionFunc(message); return confirm(message); } -function detectIEBrowser() { +function confirmActionAsync(message: string, funcOnYes: () => void, funcOnNo?: () => void): void { + const callbackFunc = (res: boolean): void => { + if(res) funcOnYes(); + else if(!!funcOnNo) funcOnNo(); + }; + if(!!settings && !!settings.confirmActionAsyncFunc) { + if(settings.confirmActionAsyncFunc(message, callbackFunc)) return; + } + callbackFunc(confirmAction(message)); +} +function detectIEBrowser(): boolean { if (typeof window === "undefined") return false; const ua: string = window.navigator.userAgent; const oldIe: number = ua.indexOf("MSIE "); const elevenIe: number = ua.indexOf("Trident/"); return oldIe > -1 || elevenIe > -1; } -function detectIEOrEdge() { +function detectIEOrEdge(): boolean { if (typeof window === "undefined") return false; if (typeof (detectIEOrEdge).isIEOrEdge === "undefined") { const ua: string = window.navigator.userAgent; @@ -37,7 +47,7 @@ function detectIEOrEdge() { } return (detectIEOrEdge).isIEOrEdge; } -function loadFileFromBase64(b64Data: string, fileName: string) { +function loadFileFromBase64(b64Data: string, fileName: string): void { try { const byteString: string = atob(b64Data.split(",")[1]); @@ -64,7 +74,7 @@ function loadFileFromBase64(b64Data: string, fileName: string) { } } catch (err) { } } -function isMobile() { +function isMobile(): boolean { return ( typeof window !== "undefined" && typeof window.orientation !== "undefined" ); @@ -392,6 +402,7 @@ export { classesToSelector, compareVersions, confirmAction, + confirmActionAsync, detectIEOrEdge, detectIEBrowser, loadFileFromBase64, diff --git a/tests/question_matrixdynamictests.ts b/tests/question_matrixdynamictests.ts index 03f235455b..27b8c6cca3 100644 --- a/tests/question_matrixdynamictests.ts +++ b/tests/question_matrixdynamictests.ts @@ -8709,4 +8709,33 @@ QUnit.test("Errors: matrixdropdown + mobile mode", function (assert) { assert.equal(table.rows[0].cells[2].hasQuestion, true); assert.equal(table.rows[0].cells[3].isErrorsCell, true); assert.equal(table.rows[0].cells[4].hasQuestion, true); -}); \ No newline at end of file +}); +QUnit.test("matrixdynamic.removeRow & confirmActionAsyncFunc, #6736", function (assert) { + const prevAsync = settings.confirmActionAsyncFunc; + + const survey = new SurveyModel({ + elements: [ + { type: "matrixdynamic", name: "matrix", + columns: [{ name: "col1" }] + } + ] + }); + const q = survey.getQuestionByName("matrix"); + q.value = [{ col1: 1 }, { col1: 2 }, { col1: 3 }]; + let f_resFunc = (res: boolean): void => {}; + settings.confirmActionAsyncFunc = (message: string, resFunc: (res: boolean) => void): boolean => { + f_resFunc = resFunc; + return true; + }; + q.removeRow(1, true); + assert.equal(q.visibleRows.length, 3, "We are waiting for async function"); + f_resFunc(false); + assert.equal(q.visibleRows.length, 3, "confirm action return false"); + q.removeRow(1, true); + assert.equal(q.visibleRows.length, 3, "We are waiting for async function, #2"); + f_resFunc(true); + assert.equal(q.visibleRows.length, 2, "confirm action return true"); + assert.deepEqual(q.value, [{ col1: 1 }, { col1: 3 }], "Row is deleted correctly"); + + settings.confirmActionAsyncFunc = prevAsync; +}); diff --git a/tests/surveypaneldynamictests.ts b/tests/surveypaneldynamictests.ts index 4b9e137a45..384d9d5377 100644 --- a/tests/surveypaneldynamictests.ts +++ b/tests/surveypaneldynamictests.ts @@ -5979,3 +5979,40 @@ QUnit.test("Make sure that panel is not collapsed on focusing the question", fun q2.focus(); assert.equal(survey.currentPageNo, 1); }); +QUnit.test("paneldynamic.removePanelUI & confirmActionAsyncFunc, #6736", function(assert) { + const prevAsync = settings.confirmActionAsyncFunc; + const survey = new SurveyModel({ + questions: [ + { + type: "paneldynamic", + name: "panel1", + templateElements: [ + { + type: "text", + name: "q1", + }, + ] + }, + ], + }); + const panel = survey.getQuestionByName("panel1"); + panel.confirmDelete = true; + panel.value = [{ q1: 1 }, { q1: 2 }, { q1: 3 }]; + assert.equal(panel.panelCount, 3, "There are 3 panels by default"); + let f_resFunc = (res: boolean): void => {}; + settings.confirmActionAsyncFunc = (message: string, resFunc: (res: boolean) => void): boolean => { + f_resFunc = resFunc; + return true; + }; + panel.removePanelUI(1); + assert.equal(panel.panelCount, 3, "We are waiting for async function"); + f_resFunc(false); + assert.equal(panel.panelCount, 3, "confirm action return false"); + panel.removePanelUI(1); + assert.equal(panel.panelCount, 3, "We are waiting for async function, #2"); + f_resFunc(true); + assert.equal(panel.panelCount, 2, "confirm action return true"); + assert.deepEqual(panel.value, [{ q1: 1 }, { q1: 3 }], "Row is deleted correctly"); + + settings.confirmActionAsyncFunc = prevAsync; +}); \ No newline at end of file