diff --git a/src/common-styles/sv-popup.scss b/src/common-styles/sv-popup.scss index 2d67bcf712..c54210126b 100644 --- a/src/common-styles/sv-popup.scss +++ b/src/common-styles/sv-popup.scss @@ -63,6 +63,40 @@ sv-popup { } } +.sv-popup--confirm-delete { + .sv-popup__container { + border-radius: calcSize(1); + } + + .sv-popup__body-content { + border-radius: calcSize(1); + } + + .sv-popup__body-header { + color: $font-editorfont-color; + margin-bottom: 0; + + /* UI/Default */ + font-family: $font-family; + font-size: calcFontSize(1); + font-style: normal; + font-weight: 400; + line-height: calcLineHeight(1.5);/* 150% */ + } + + .sv-popup__scrolling-content { + display: none; + } + + .sv-popup__body-footer { + padding-bottom: 0; + + .sv-action-bar { + gap: calcSize(2); + } + } +} + .sv-popup.sv-popup--modal>.sv-popup__container { position: static; } diff --git a/src/default-styles.scss b/src/default-styles.scss index 6edce77a6d..af3b8fc7b7 100644 --- a/src/default-styles.scss +++ b/src/default-styles.scss @@ -1350,6 +1350,11 @@ sv-popup { background-color: var(--background-dim, #f3f3f3); } +.sv-popup__button.sv-popup__button--danger { + background-color: var(--sjs-special-red, #E50A3E); + color: var(--primary-foreground, #fff); +} + //eo popup //list .sv-list { diff --git a/src/defaultV2-theme/blocks/sd-button.scss b/src/defaultV2-theme/blocks/sd-button.scss index cf8c3d6934..a22a0eaa76 100644 --- a/src/defaultV2-theme/blocks/sd-button.scss +++ b/src/defaultV2-theme/blocks/sd-button.scss @@ -21,6 +21,11 @@ outline: none; } +.sd-btn--small { + flex-grow: 1; + padding: calcSize(1.5) calcSize(4); +} + .sd-btn:hover { background-color: $background-dark; } @@ -48,4 +53,19 @@ .sd-btn--action:disabled { color: $primary-foreground-disabled; pointer-events: none; +} + +.sd-btn--danger { + background-color: $red; + color: $primary-foreground; +} + +.sd-btn--danger:hover { + background-color: $red; + color: $primary-foreground; +} + +.sd-btn--danger:disabled { + color: $red-forecolor; + pointer-events: none; } \ No newline at end of file diff --git a/src/localization/english.ts b/src/localization/english.ts index 86dbe25431..1f93a552cc 100644 --- a/src/localization/english.ts +++ b/src/localization/english.ts @@ -101,6 +101,8 @@ export var englishStrings = { tagboxDoneButtonCaption: "OK", selectToRankEmptyRankedAreaText: "All choices are ranked", selectToRankEmptyUnrankedAreaText: "Drag and drop choices here to rank them", + ok: "OK", + cancel: "Cancel", }; // Uncomment the lines below if you create a custom dictionary. diff --git a/src/plugins/themes/common-theme-settings.ts b/src/plugins/themes/common-theme-settings.ts index 7cdd8583c0..d080787580 100644 --- a/src/plugins/themes/common-theme-settings.ts +++ b/src/plugins/themes/common-theme-settings.ts @@ -424,6 +424,7 @@ export function setStyles(): void { ".sv-popup__button:disabled:hover": "box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);", ".sv-popup__button.sv-popup__button--apply": "background-color: var(--primary, #19b394); color: var(--primary-foreground, #fff);", ".sv-popup__button.sv-popup__button--apply:disabled": "background-color: var(--background-dim, #f3f3f3);", + ".sv-popup__button.sv-popup__button--danger": "background-color: var(--sjs-special-red, #E50A3E); color: var(--primary-foreground, #fff);", //eo popup //list ".sv-list": "padding: 0; margin: 0; background: var(--background, #fff); list-style-type: none; overflow-y: auto;", diff --git a/src/popup-utils.ts b/src/popup-utils.ts index d18761594b..4e08f9dfc9 100644 --- a/src/popup-utils.ts +++ b/src/popup-utils.ts @@ -26,6 +26,7 @@ export function createPopupModalViewModel(options: IDialogOptions, rootElement?: options.title ); popupModel.displayMode = options.displayMode || "popup"; + popupModel.isFocusedContent = options.isFocusedContent ?? true; const popupViewModel: PopupBaseViewModel = new PopupModalViewModel(popupModel); if(!!rootElement && !!rootElement.appendChild) { var container: HTMLElement = document.createElement("div"); diff --git a/src/popup.ts b/src/popup.ts index 42b33bfcee..68fccb74c5 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -20,6 +20,7 @@ export interface IDialogOptions extends IPopupOptionsBase { componentName: string; data: any; onApply: () => boolean; + isFocusedContent?: boolean; } export interface IPopupModel extends IDialogOptions { contentComponentName: string; diff --git a/src/settings.ts b/src/settings.ts index b67ca77223..f0a497f94b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,5 @@ import { IDialogOptions } from "./popup"; +import { showConfirmDialog } from "./utils/utils"; export type ISurveyEnvironment = { root: Document | ShadowRoot, @@ -483,7 +484,7 @@ export var settings = { */ tagboxCloseOnSelect: false, /** - * A property that allows you to display a custom confirm dialog instead of the standard browser dialog. + * A property that allows you to display a custom confirm dialog. * * Set this property to a function that renders your custom dialog window. This function should return `true` if a user confirms an action or `false` otherwise. * @param message A message to be displayed in the confirm dialog window. @@ -492,14 +493,25 @@ export var settings = { return confirm(message); }, /** - * A property that allows you to display a custom confirm dialog instead of the standard browser dialog in async mode. + * A property that allows you to display a custom confirm dialog in async mode or activate the standard browser dialog. * - * Set this property to a function that renders your custom dialog window. This function should return `true` to be enabled; otherwise, a survey executes the [`confirmActionFunc`](#confirmActionFunc) function. Pass the dialog result as the `callback` parameter: `true` if a user confirms an action, `false` otherwise. + * To display a custom confirm dialog, set this property to a function that renders it. This function should return `true` to be enabled; otherwise, a survey executes the [`confirmActionFunc`](#confirmActionFunc) function. Pass the dialog result as the `callback` parameter: `true` if a user confirms an action, `false` otherwise. + * + * To activate the standard browser dialog, set the `confirmActionAsync` property to a function that returns `false`. With this configuration, a survey falls back to the [`confirmActionFunc`](#confirmActionFunc) function, which renders the standard browser dialog by default. + * + * ```js + * import { settings } from "survey-core"; + * + * // Display the standard browser dialog + * settings.confirmActionAsync = () => { + * return false; + * } + * ``` * @param message A message to be displayed in the confirm dialog window. * @param callback A callback function that should be called with `true` if a user confirms an action or `false` otherwise. */ confirmActionAsync: function (message: string, callback: (res: boolean) => void): boolean { - return false; + return showConfirmDialog(message, callback); }, /** * A minimum width value for all survey elements. diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 585b7c2c90..5a35f53807 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,8 @@ +import { LocalizableString } from "../localizablestring"; import { settings, ISurveyEnvironment } from "./../settings"; +import { IDialogOptions } from "../popup"; +import { surveyLocalization } from "../surveyStrings"; +import { PopupBaseViewModel } from "../popup-view-model"; function compareVersions(a: any, b: any) { const regExStrip0: RegExp = /(\.0+)+$/; @@ -14,19 +18,23 @@ function compareVersions(a: any, b: any) { } return segmentsA.length - segmentsB.length; } + function confirmAction(message: string): boolean { if (!!settings && !!settings.confirmActionFunc) return settings.confirmActionFunc(message); return confirm(message); } + function confirmActionAsync(message: string, funcOnYes: () => void, funcOnNo?: () => void): void { const callbackFunc = (res: boolean): void => { if(res) funcOnYes(); else if(!!funcOnNo) funcOnNo(); }; + if(!!settings && !!settings.confirmActionAsync) { if(settings.confirmActionAsync(message, callbackFunc)) return; } + callbackFunc(confirmAction(message)); } function detectIEBrowser(): boolean { @@ -399,6 +407,35 @@ export class Logger { } } +export function showConfirmDialog(message: string, callback: (res: boolean) => void): boolean { + const locStr = new LocalizableString(undefined); + const popupViewModel:PopupBaseViewModel = settings.showDialog({ + componentName: "sv-string-viewer", + data: { locStr: locStr, locString: locStr, model: locStr }, //TODO fix in library + onApply: () => { + callback(true); + return true; + }, + onCancel: () => { + callback(false); + return false; + }, + title: message, + displayMode: "popup", + isFocusedContent: false, + cssClass: "sv-popup--confirm-delete" + }, /*settings.rootElement*/document.body); //TODO survey root + const toolbar = popupViewModel.footerToolbar; + const applyBtn = toolbar.getActionById("apply"); + const cancelBtn = toolbar.getActionById("cancel"); + cancelBtn.title = surveyLocalization.getString("cancel"); + cancelBtn.innerCss = "sv-popup__body-footer-item sv-popup__button sd-btn sd-btn--small"; + applyBtn.title = surveyLocalization.getString("ok"); + applyBtn.innerCss = "sv-popup__body-footer-item sv-popup__button sv-popup__button--danger sd-btn sd-btn--small sd-btn--danger"; + popupViewModel.width = "452px"; + return true; +} + export { mergeValues, getElementWidth, diff --git a/src/vue/components/popup/popup-container.vue b/src/vue/components/popup/popup-container.vue index ffbe8ea160..3127b8cb99 100644 --- a/src/vue/components/popup/popup-container.vue +++ b/src/vue/components/popup/popup-container.vue @@ -105,11 +105,12 @@ export function showModal( export function showDialog(dialogOptions: IDialogOptions, rootElement?: HTMLElement): PopupBaseViewModel { dialogOptions.onHide = () => { popup.$destroy(); + popupViewModel.container.remove(); popupViewModel.dispose(); }; const popupViewModel: PopupBaseViewModel = createPopupModalViewModel(dialogOptions, rootElement); const popup = new PopupContainer({ - el: popupViewModel.container.appendChild(document.createElement("div")), + el: (popupViewModel.container).appendChild(document.createElement("div")), propsData: { model: popupViewModel }, }); popupViewModel.model.isVisible = true; diff --git a/testCafe/questions/file.js b/testCafe/questions/file.js index ceba77b6a0..be931c3481 100644 --- a/testCafe/questions/file.js +++ b/testCafe/questions/file.js @@ -162,26 +162,34 @@ frameworks.forEach(framework => { "input[type=file]", "../resources/small_Dashka.jpg" ); - await t - .setNativeDialogHandler(() => { - return false; - }) - .click(".sv_q_file_remove"); - await t - .setNativeDialogHandler(() => { - return false; - }) - .click(".sv_q_file_remove_button"); - const history = await t.getNativeDialogHistory(); - await t - .expect(history[1].type) - .eql("confirm") - .expect(history[1].text) - .eql("Are you sure that you want to remove this file: small_Dashka.jpg?") - .expect(history[0].type) - .eql("confirm") - .expect(history[0].text) - .eql("Are you sure that you want to remove all files?"); + + const getFileName = ClientFunction(() => window["survey"].getAllQuestions()[0].value[0].name); + const checkValue = ClientFunction(() => window["survey"].getAllQuestions()[0].value.length === 0); + await t.click(".sv_q_file_remove_button").click(".sv-popup--confirm-delete .sd-btn"); + assert.equal(await getFileName(), "small_Dashka.jpg"); + await t.click(".sv_q_file_remove_button").click(".sv-popup--confirm-delete .sd-btn--danger"); + assert.equal(await checkValue(), true); + + // await t + // .setNativeDialogHandler(() => { + // return false; + // }) + // .click(".sv_q_file_remove"); + // await t + // .setNativeDialogHandler(() => { + // return false; + // }) + // .click(".sv_q_file_remove_button"); + // const history = await t.getNativeDialogHistory(); + // await t + // .expect(history[1].type) + // .eql("confirm") + // .expect(history[1].text) + // .eql("Are you sure that you want to remove this file: small_Dashka.jpg?") + // .expect(history[0].type) + // .eql("confirm") + // .expect(history[0].text) + // .eql("Are you sure that you want to remove all files?"); }); // TODO testcafe waiting forever... // test(`change file max size`, async t => { diff --git a/visualRegressionTests/tests/defaultV2/etalons/paneldynamic-confirm-dialog.png b/visualRegressionTests/tests/defaultV2/etalons/paneldynamic-confirm-dialog.png new file mode 100644 index 0000000000..a74ab2faa5 Binary files /dev/null and b/visualRegressionTests/tests/defaultV2/etalons/paneldynamic-confirm-dialog.png differ diff --git a/visualRegressionTests/tests/defaultV2/paneldynamic.ts b/visualRegressionTests/tests/defaultV2/paneldynamic.ts index 32a271e436..1e0bcbf6a8 100644 --- a/visualRegressionTests/tests/defaultV2/paneldynamic.ts +++ b/visualRegressionTests/tests/defaultV2/paneldynamic.ts @@ -391,3 +391,37 @@ frameworks.forEach(framework => { }); }); }); + +frameworks.forEach(framework => { + const json = { + "pages": [ + { + "name": "page1", + "elements": [{ + "type": "paneldynamic", + "panelCount": 1, + "name": "question1", + "templateElements": [ + { + "type": "text", + "name": "question2" + } + ], + "confirmDelete": true + }] + } + ] + }; + fixture`${framework} ${title} ${theme}` + .page`${url_test}${theme}/${framework}`.beforeEach(async t => { + await applyTheme(theme); + await initSurvey(framework, json); + }); + test("Paneldynamic confirm dialog", async (t) => { + await wrapVisualTest(t, async (t, comparer) => { + await t.resizeWindow(1280, 900); + await t.click(Selector(".sd-paneldynamic__remove-btn")); + await takeElementScreenshot("paneldynamic-confirm-dialog", Selector(".sv-popup--confirm-delete .sv-popup__body-content"), t, comparer); + }); + }); +});