diff --git a/CHANGELOG.md b/CHANGELOG.md index b7cf179aae..26ebfb6262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [1.9.91](https://github.com/surveyjs/survey-library/compare/v1.9.90...v1.9.91) (2023-06-08) + ## [1.9.90](https://github.com/surveyjs/survey-library/compare/v1.9.89...v1.9.90) (2023-05-31) ## [1.9.89](https://github.com/surveyjs/survey-library/compare/v1.9.88...v1.9.89) (2023-05-23) diff --git a/package.json b/package.json index 74f2134595..703c15e166 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "testcafe:ci:angular": "testcafe -c 4 -q attemptLimit=5,successThreshold=1 chrome:headless testCafe/ --app \"http-server ./packages/survey-angular-ui/example/dist --proxy http://localhost:8080? -p 8080\" --selector-timeout 1500 --reporter minimal --env=angular", "prepare": "husky install" }, - "version": "1.9.90", + "version": "1.9.91", "name": "survey-library", "private": true, "devDependencies": { diff --git a/packages/survey-angular-ui/CHANGELOG.md b/packages/survey-angular-ui/CHANGELOG.md index 327b95ea74..065065aa2e 100644 --- a/packages/survey-angular-ui/CHANGELOG.md +++ b/packages/survey-angular-ui/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [1.9.91](https://github.com/surveyjs/surveyjs/compare/v1.9.90...v1.9.91) (2023-06-08) + ## [1.9.90](https://github.com/surveyjs/surveyjs/compare/v1.9.89...v1.9.90) (2023-05-31) ## [1.9.89](https://github.com/surveyjs/surveyjs/compare/v1.9.88...v1.9.89) (2023-05-23) diff --git a/packages/survey-angular-ui/package.json b/packages/survey-angular-ui/package.json index 750f94c4a0..8cccf8c67d 100644 --- a/packages/survey-angular-ui/package.json +++ b/packages/survey-angular-ui/package.json @@ -1,6 +1,6 @@ { "name": "survey-angular-ui", - "version": "1.9.90", + "version": "1.9.91", "description": "survey.js is a JavaScript Survey Library. It is a modern way to add a survey to your website. It uses JSON for survey metadata and results.", "keywords": [ "Survey", diff --git a/packages/survey-angular-ui/src/components/dropdown/dropdown.component.html b/packages/survey-angular-ui/src/components/dropdown/dropdown.component.html index c47581b32d..ea91090b60 100644 --- a/packages/survey-angular-ui/src/components/dropdown/dropdown.component.html +++ b/packages/survey-angular-ui/src/components/dropdown/dropdown.component.html @@ -18,7 +18,7 @@ - + ; protected popupCssClasses = "sv-single-select-list"; protected listModelFilterStringChanged = (newValue: string) => { - if(this.filterString !== newValue) { + if (this.filterString !== newValue) { this.filterString = newValue; } } @@ -93,7 +93,7 @@ export class DropdownListModel extends Base { }); this._popupModel.cssClass = this.popupCssClasses; this._popupModel.onVisibilityChanged.add((_, option: { isVisible: boolean }) => { - if(option.isVisible) { + if (option.isVisible) { this.listModel.renderElements = true; } if (option.isVisible && this.question.choicesLazyLoadEnabled) { @@ -220,11 +220,9 @@ export class DropdownListModel extends Base { defaultValue: "", onSet: (newValue, target: DropdownListModel) => { target.question.inputHasValue = !!newValue; - target.showSelectedItemLocText = target.question.showSelectedItemLocText; } }) inputString: string; - @property({}) showSelectedItemLocText: boolean; @property({}) showInputFieldComponent: boolean; @property() ariaActivedescendant: string; @@ -317,11 +315,9 @@ export class DropdownListModel extends Base { question.onPropertyChanged.add((sender: any, options: any) => { if (options.name == "value") { this.showInputFieldComponent = this.question.showInputFieldComponent; - this.showSelectedItemLocText = this.question.showSelectedItemLocText; } }); this.showInputFieldComponent = this.question.showInputFieldComponent; - this.showSelectedItemLocText = this.question.showSelectedItemLocText; this.listModel = this.createListModel(); this.updateAfterListModelCreated(this.listModel); @@ -508,10 +504,10 @@ export class DropdownListModel extends Base { dispose(): void { super.dispose(); - if(!!this.listModel) { + if (!!this.listModel) { this.listModel.dispose(); } - if(!!this.popupModel) { + if (!!this.popupModel) { this.popupModel.dispose(); } } diff --git a/src/question_baseselect.ts b/src/question_baseselect.ts index cecc09a6f2..1512f042c5 100644 --- a/src/question_baseselect.ts +++ b/src/question_baseselect.ts @@ -323,7 +323,9 @@ export class QuestionSelectBase extends Question { protected onEnableItemCallBack(item: ItemValue): boolean { return true; } - protected onSelectedItemValuesChangedHandler(newValue: any): void { } + protected onSelectedItemValuesChangedHandler(newValue: any): void { + this.survey?.loadedChoicesFromServer(this); + } protected getItemIfChoicesNotContainThisValue(value: any, text?: string): any { if(!this.isReady) { return this.createItemValue(value, text); @@ -343,6 +345,9 @@ export class QuestionSelectBase extends Question { return itemValue || selectedItemValues || (this.isOtherSelected ? this.otherItem : this.getItemIfChoicesNotContainThisValue(this.value)); } protected onGetSingleSelectedItem(selectedItemByValue: ItemValue): void {} + protected getMultipleSelectedItems(): Array { + return []; + } private setConditionalChoicesRunner() { if (this.choicesVisibleIf) { if (!this.conditionChoicesVisibleIfRunner) { @@ -951,6 +956,8 @@ export class QuestionSelectBase extends Question { protected getChoicesDisplayValue(items: ItemValue[], val: any): any { if (val == this.otherItemValue.value) return this.otherValue ? this.otherValue : this.locOtherText.textOrHtml; + const selItem = this.getSingleSelectedItem(); + if(!!selItem && selItem.value === val) return selItem.locText.textOrHtml; var str = ItemValue.getTextOrHtmlByValue(items, val); return str == "" && val ? val : str; } @@ -958,11 +965,19 @@ export class QuestionSelectBase extends Question { onGetValueCallback?: (index: number) => any): string { var items = this.visibleChoices; var strs = []; + const vals = []; for (var i = 0; i < value.length; i++) { - let val = !onGetValueCallback ? value[i] : onGetValueCallback(i); - let valStr = this.getChoicesDisplayValue(items, val); - if (valStr) { - strs.push(valStr); + vals.push(!onGetValueCallback ? value[i] : onGetValueCallback(i)); + } + if(Helpers.isTwoValueEquals(this.value, vals)) { + this.getMultipleSelectedItems().forEach(item => strs.push(item.locText.textOrHtml)); + } + if(strs.length === 0) { + for (var i = 0; i < vals.length; i++) { + let valStr = this.getChoicesDisplayValue(items, vals[i]); + if (valStr) { + strs.push(valStr); + } } } return strs.join(", "); @@ -1180,12 +1195,11 @@ export class QuestionSelectBase extends Question { if (this.enableOnLoadingChoices) { this.readOnly = false; } + const errors = []; if (!this.isReadOnly) { - var errors = []; if (this.choicesByUrl && this.choicesByUrl.error) { errors.push(this.choicesByUrl.error); } - this.errors = errors; } var newChoices = null; var checkCachedValuesOnExisting = true; @@ -1240,6 +1254,10 @@ export class QuestionSelectBase extends Question { } } } + if(!this.isReadOnly && !newChoices && !this.isFirstLoadChoicesFromUrl) { + this.value = null; + } + this.errors = errors; this.choicesLoaded(); } private createCachedValueForUrlRequests( diff --git a/src/question_checkbox.ts b/src/question_checkbox.ts index 445bc05401..5078831366 100644 --- a/src/question_checkbox.ts +++ b/src/question_checkbox.ts @@ -200,6 +200,9 @@ export class QuestionCheckboxModel extends QuestionCheckboxBase { return this.validateItemValues(itemValues); } public get selectedItems(): Array { return this.selectedChoices; } + protected getMultipleSelectedItems(): Array { + return this.selectedChoices; + } protected validateItemValues(itemValues: Array): Array { if(!!itemValues.length) return itemValues; diff --git a/src/question_dropdown.ts b/src/question_dropdown.ts index 29cfbe568e..d52dc6b637 100644 --- a/src/question_dropdown.ts +++ b/src/question_dropdown.ts @@ -266,6 +266,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { } protected onSelectedItemValuesChangedHandler(newValue: any): void { this.dropdownListModel?.setInputStringFromSelectedItem(newValue); + super.onSelectedItemValuesChangedHandler(newValue); } protected hasUnknownValue( val: any, diff --git a/src/question_matrixdynamic.ts b/src/question_matrixdynamic.ts index 4f08eb7798..d5e2c5ac80 100644 --- a/src/question_matrixdynamic.ts +++ b/src/question_matrixdynamic.ts @@ -89,7 +89,13 @@ export class QuestionMatrixDynamicModel extends QuestionMatrixDropdownModelBase private draggedRow: MatrixDropdownRowModelBase; private isBanStartDrag(pointerDownEvent: PointerEvent): boolean { const target = (pointerDownEvent.target); - return target.getAttribute("contenteditable") === "true" || target.nodeName === "INPUT"; + return target.getAttribute("contenteditable") === "true" || target.nodeName === "INPUT" || !this.isDragHandleAreaValid(target); + } + public isDragHandleAreaValid(node:HTMLElement): boolean { + if (this.survey.matrixDragHandleArea === "icon") { + return node.classList.contains(this.cssClasses.dragElementDecorator); + } + return true; } public onPointerDown(pointerDownEvent: PointerEvent, row: MatrixDropdownRowModelBase):void { if (!row || !this.allowRowsDragAndDrop) return; diff --git a/src/survey.ts b/src/survey.ts index bc43c55dcd..90d27c6c3e 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -2925,6 +2925,20 @@ export class SurveyModel extends SurveyElementCore public get isShowStartingPage(): boolean { return this.state === "starting"; } + /** + * Specifies which part of a choice item responds to a drag gesture in MatrixDynamic questions. + * + * Possible values: + * + * - `"entireItem"` (default) - Users can use the entire choice item as a drag handle. + * - `"icon"` - Users can only use the choice item icon as a drag handle. + */ + public get matrixDragHandleArea():string { + return this.getPropertyValue("matrixDragHandleArea", "entireItem"); + } + public set matrixDragHandleArea(val: string) { + this.setPropertyValue("matrixDragHandleArea", val); + } /** * Survey is showing a page right now. It is in "running", "preview" or starting state. */ @@ -7082,6 +7096,12 @@ Serializer.addClass("survey", [ default: "initial", choices: ["initial", "random"], }, + { + name: "matrixDragHandleArea", + visible: false, + default: "entireItem", + choices: ["entireItem", "icon"] + }, "showPageNumbers:boolean", { name: "showQuestionNumbers", diff --git a/tests/choicesRestfultests.ts b/tests/choicesRestfultests.ts index b2049e64ce..f3d2de47a0 100644 --- a/tests/choicesRestfultests.ts +++ b/tests/choicesRestfultests.ts @@ -13,6 +13,7 @@ import { JsonObject, Serializer } from "../src/jsonobject"; import { QuestionRadiogroupModel } from "../src/question_radiogroup"; import { settings } from "../src/settings"; import { MatrixDropdownColumn } from "../src/question_matrixdropdowncolumn"; +import { SurveyError } from "../src/survey-error"; export default QUnit.module("choicesRestful"); @@ -719,8 +720,24 @@ QUnit.test("Set value before loading data, bug #1089", function(assert) { question.hasItemsCallbackDelay = true; question.onSurveyLoad(); survey.setValue("q1", "CA"); - question.doResultsCallback(); + question["onLoadChoicesFromUrl"]([new ItemValue("CA")]); assert.equal(question.value, "CA", "'CA' value is still here"); + assert.equal(question.selectedItem.value, "CA", "selectedItem is correct"); +}); +QUnit.test("Clear value on getting empty array, bug #6251", function(assert) { + var survey = new SurveyModel(); + survey.addNewPage("1"); + var question = new QuestionDropdownModelTester("q1"); + question.choicesByUrl.url = "{state}"; + survey.pages[0].addQuestion(question); + question.hasItemsCallbackDelay = true; + question.onSurveyLoad(); + survey.setValue("q1", "CA"); + question.choicesByUrl.error = new SurveyError("Empty request"); + question["onLoadChoicesFromUrl"]([]); + assert.equal(question.isEmpty(), true, "value is empty"); + assert.equal(question.selectedItem, null, "selectedItem is null"); + assert.equal(question.errors.length, 1, "It shows error on empty result"); }); QUnit.test( @@ -736,9 +753,10 @@ QUnit.test( question.onSurveyLoad(); survey.setValue("q1", "CA"); assert.equal(question.isOtherSelected, false, "There shuld not be other#1"); - question.doResultsCallback(); + question["onLoadChoicesFromUrl"]([new ItemValue("CA")]); assert.equal(question.isOtherSelected, false, "There shuld not be other#2"); assert.equal(question.value, "CA", "'CA' value is still here"); + assert.equal(question.selectedItem.value, "CA", "selectedItem is correct"); } ); @@ -748,20 +766,21 @@ QUnit.test("preset data and same data from url", function(assert) { survey.addNewPage("1"); var question = new QuestionDropdownModelTester("q1"); survey.storeOthersAsComment = false; - + question.hasItemsCallbackDelay = true; + question.choicesByUrl.url = "{state}"; + survey.pages[0].addQuestion(question); + question.onSurveyLoad(); + survey.data = { q1: "CA" }; survey.onValueChanging.add(function() { counter++; }); - survey.onValueChanged.add(function() { counter++; }); - - question.choicesByUrl.url = "{state}"; - survey.pages[0].addQuestion(question); - survey.data = { q1: "CA" }; + assert.equal(question.value, "CA", "value is here"); question["onLoadChoicesFromUrl"]([new ItemValue("CA"), new ItemValue("AA")]); - + assert.equal(question.value, "CA", "value is still here"); + assert.equal(question.selectedItem.value, "CA", "selecteditem is correct"); assert.equal(counter, 0, "value doesn't change"); }); diff --git a/tests/questionDropdownTests.ts b/tests/questionDropdownTests.ts index 67429fc331..c1d394078e 100644 --- a/tests/questionDropdownTests.ts +++ b/tests/questionDropdownTests.ts @@ -2,6 +2,7 @@ import { SurveyModel } from "../src/survey"; import { QuestionDropdownModel } from "../src/question_dropdown"; import { ListModel } from "../src/list"; import { createListContainerHtmlElement } from "./utilstests"; +import { Question } from "../src/question"; import { settings } from "../src/settings"; export default QUnit.module("choicesRestful"); @@ -1049,7 +1050,13 @@ QUnit.test("lazy loading + onGetChoiceDisplayValue: defaultValue", assert => { "name": "q1", "defaultValue": 55, "choicesLazyLoadEnabled": true - }] + }, + { + "type": "text", + "name": "q2", + "title": "{q1}" + } + ] }; const survey = new SurveyModel(json); survey.onChoicesLazyLoad.add((sender, options) => { @@ -1069,11 +1076,13 @@ QUnit.test("lazy loading + onGetChoiceDisplayValue: defaultValue", assert => { }); const question = survey.getAllQuestions()[0]; + const questionTitle = survey.getAllQuestions()[1]; assert.equal(question.choicesLazyLoadEnabled, true); assert.equal(question.choices.length, 0); assert.equal(question.value, 55); assert.equal(question.selectedItem.value, 55); assert.equal(question.selectedItem.text, "DisplayText_55"); + assert.equal(questionTitle.locTitle.textOrHtml, "DisplayText_55", "display text is correct"); question.dropdownListModel.popupModel.isVisible = true; setTimeout(() => { diff --git a/tests/question_matrixdynamictests.ts b/tests/question_matrixdynamictests.ts index 709c48533c..20f970f0f6 100644 --- a/tests/question_matrixdynamictests.ts +++ b/tests/question_matrixdynamictests.ts @@ -8185,4 +8185,37 @@ QUnit.test("Check rightIndents set correctly for detailElements with defaultV2 t const matrix = survey.getQuestionByName("matrix"); matrix.visibleRows[0].showHideDetailPanelClick(); assert.equal(matrix.renderedTable.rows[1].cells[1].panel.elements[0].rightIndent, 0); +}); + +QUnit.test("matrixDragHandleArea = 'icon'", function (assert) { + const survey = new SurveyModel({ + matrixDragHandleArea: "icon", + elements: [ + { + type: "matrixdynamic", + allowRowsDragAndDrop: "true", + name: "matrix", + rowCount: 2, + detailPanelMode: "underRow", + detailPanelShowOnAdding: true, + columns: [{ name: "col1" }, { name: "col2" }, { name: "col3" }], + detailElements: [{ type: "text", name: "q1" }, { type: "text", name: "q2", startWithNewLine: false, visibleIf: "{row.q1} notempty" }], + }, + ], + }); + survey.css = { root: "sd-root-modern" }; + const matrix = survey.getQuestionByName("matrix"); + let nodeMock = document.createElement("div"); + assert.equal(matrix.isDragHandleAreaValid(nodeMock), false); + + nodeMock.classList.add("some-class"); + assert.equal(matrix.isDragHandleAreaValid(nodeMock), false); + + nodeMock.classList.add(matrix.cssClasses.dragElementDecorator); + assert.equal(matrix.isDragHandleAreaValid(nodeMock), true); + + survey.matrixDragHandleArea = "entireItem"; + + nodeMock.classList.remove(matrix.cssClasses.dragElementDecorator); + assert.equal(matrix.isDragHandleAreaValid(nodeMock), true); }); \ No newline at end of file diff --git a/tests/question_tagbox_tests.ts b/tests/question_tagbox_tests.ts index 61b9880fc2..0c3feeb77a 100644 --- a/tests/question_tagbox_tests.ts +++ b/tests/question_tagbox_tests.ts @@ -547,8 +547,12 @@ QUnit.test("lazy loading + onGetChoiceDisplayValue: defaultValue", assert => { "name": "q1", "defaultValue": [52, 55], "choicesLazyLoadEnabled": true - }] - }; + }, + { + "type": "text", + "name": "q2", + "title": "{q1}" + }] }; const survey = new SurveyModel(json); survey.onChoicesLazyLoad.add((sender, options) => { const total = 55; @@ -567,6 +571,7 @@ QUnit.test("lazy loading + onGetChoiceDisplayValue: defaultValue", assert => { }); const question = survey.getAllQuestions()[0]; + const questionTitle = survey.getAllQuestions()[1]; assert.equal(question.choicesLazyLoadEnabled, true); assert.equal(question.choices.length, 0); assert.deepEqual(question.value, [52, 55]); @@ -575,6 +580,7 @@ QUnit.test("lazy loading + onGetChoiceDisplayValue: defaultValue", assert => { assert.equal(question.selectedItems[0].text, "DisplayText_52", "question.selectedItems[0] text"); assert.equal(question.selectedItems[1].value, 55, "question.selectedItems[1] value"); assert.equal(question.selectedItems[1].text, "DisplayText_55", "question.selectedItems[1] text"); + assert.equal(questionTitle.locTitle.textOrHtml, "DisplayText_52, DisplayText_55", "display text is correct"); question.dropdownListModel.popupModel.isVisible = true; setTimeout(() => {