Skip to content

Commit

Permalink
Display the None option at the end of a choice list fixed #6465 (#7310)
Browse files Browse the repository at this point in the history
* Display the None option at the end of a choice list fixed #6465

* Support headItems&footItems #6465
  • Loading branch information
andrewtelnov committed Nov 10, 2023
1 parent 28cfd30 commit 59a8f66
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 38 deletions.
63 changes: 40 additions & 23 deletions src/question_baseselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,27 +898,52 @@ export class QuestionSelectBase extends Question {
}
}
public get newItem(): ItemValue { return this.newItemValue; }
protected addToVisibleChoices(items: Array<ItemValue>, isAddAll: boolean) {
protected addToVisibleChoices(items: Array<ItemValue>, isAddAll: boolean): void {
this.headItemsCount = 0;
this.footItemsCount = 0;
if (isAddAll) {
if (!this.newItemValue) {
this.newItemValue = this.createItemValue("newitem"); //TODO
this.newItemValue.isGhost = true;
}
if (!this.isUsingCarryForward && this.canShowOptionItem(this.newItemValue, isAddAll, false)) {
this.footItemsCount ++;
items.push(this.newItemValue);
}
}
const dict = new Array<{ index: number, item: ItemValue }>();
this.addNonChoicesItems(dict, isAddAll);
dict.sort((a: { index: number, item: ItemValue }, b: { index: number, item: ItemValue }): number => {
if(a.index === b.index) return 0;
return a.index < b.index ? -1 : 1;
});
for(let i = 0; i < dict.length; i ++) {
const rec = dict[i];
if(rec.index < 0) {
items.splice(i, 0, rec.item);
this.headItemsCount ++;
}
else {
items.push(rec.item);
this.footItemsCount ++;
}
}
}
protected addNonChoicesItems(dict: Array<{ index: number, item: ItemValue }>, isAddAll: boolean): void {
if (
this.supportNone() && this.canShowOptionItem(this.noneItem, isAddAll, this.hasNone)
) {
items.push(this.noneItem);
this.addNonChoiceItem(dict, this.noneItem, settings.specialChoicesOrder.noneItem);
}
if (
this.supportOther() && this.canShowOptionItem(this.otherItem, isAddAll, this.hasOther)
) {
items.push(this.otherItem);
this.addNonChoiceItem(dict, this.otherItem, settings.specialChoicesOrder.otherItem);
}
}
protected addNonChoiceItem(dict: Array<{ index: number, item: ItemValue }>, item: ItemValue, order: Array<number>): void {
order.forEach(val => dict.push({ index: val, item: item }));
}
protected canShowOptionItem(item: ItemValue, isAddAll: boolean, hasItem: boolean): boolean {
let res: boolean = (isAddAll && (!!this.canShowOptionItemCallback ? this.canShowOptionItemCallback(item) : true)) || hasItem;
if (this.canSurveyChangeItemVisibility()) {
Expand Down Expand Up @@ -1131,28 +1156,13 @@ export class QuestionSelectBase extends Question {
}
return false;
}
protected isHeadChoice(
item: ItemValue,
question: QuestionSelectBase
): boolean {
return false;
}
protected isFootChoice(
item: ItemValue,
question: QuestionSelectBase
): boolean {
protected isBuiltInChoice(item: ItemValue, question: QuestionSelectBase): boolean {
return (
item === question.noneItem ||
item === question.otherItem ||
item === question.newItemValue
);
}
protected isBuiltInChoice(
item: ItemValue,
question: QuestionSelectBase
): boolean {
return this.isHeadChoice(item, question) || this.isFootChoice(item, question);
}
protected getChoices(): Array<ItemValue> {
return this.choices;
}
Expand Down Expand Up @@ -1583,13 +1593,20 @@ export class QuestionSelectBase extends Question {
.append(this.cssClasses.controlLabelChecked, this.isItemSelected(item))
.toString() || undefined;
}
private headItemsCount: number = 0;
private footItemsCount: number = 0;
get headItems(): ItemValue[] {
return (this.separateSpecialChoices || this.isDesignMode) ?
this.visibleChoices.filter(choice => this.isHeadChoice(choice, this)) : [];
const count = (this.separateSpecialChoices || this.isDesignMode) ? this.headItemsCount : 0;
const res = [];
for(let i = 0; i < count; i ++) res.push(this.visibleChoices[i]);
return res;
}
get footItems(): ItemValue[] {
return (this.separateSpecialChoices || this.isDesignMode) ?
this.visibleChoices.filter(choice => this.isFootChoice(choice, this)) : [];
const count = (this.separateSpecialChoices || this.isDesignMode) ? this.footItemsCount : 0;
const res = [];
const items = this.visibleChoices;
for(let i = 0; i < count; i ++) res.push(items[items.length - count + i]);
return res;
}
get dataChoices(): ItemValue[] {
return this.visibleChoices.filter((item) => !this.isBuiltInChoice(item, this));
Expand Down
26 changes: 12 additions & 14 deletions src/question_checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CssClassBuilder } from "./utils/cssClassBuilder";
import { IQuestion } from "./base-interfaces";
import { SurveyError } from "./survey-error";
import { CustomError } from "./error";
import { settings } from "./settings";

/**
* A class that describes the Checkboxes question type.
Expand Down Expand Up @@ -116,9 +117,10 @@ export class QuestionCheckboxModel extends QuestionCheckboxBase {
if (!val || !Array.isArray(val)) return false;
if (this.isItemSelected(this.noneItem)) return false;
var allItemCount = this.visibleChoices.length;
if (this.hasOther) allItemCount--;
if (this.hasNone) allItemCount--;
if (this.hasSelectAll) allItemCount--;
const order = settings.specialChoicesOrder;
if (this.hasOther) allItemCount -= order.otherItem.length;
if (this.hasNone) allItemCount -= order.noneItem.length;
if (this.hasSelectAll) allItemCount -= order.selectAllItem.length;
var selectedCount = val.length;
if (this.isOtherSelected) selectedCount--;
return selectedCount === allItemCount;
Expand Down Expand Up @@ -378,22 +380,18 @@ export class QuestionCheckboxModel extends QuestionCheckboxBase {
protected supportSelectAll() {
return this.isSupportProperty("showSelectAllItem");
}
protected addToVisibleChoices(items: Array<ItemValue>, isAddAll: boolean) {
protected addNonChoicesItems(dict: Array<{ index: number, item: ItemValue }>, isAddAll: boolean): void {
super.addNonChoicesItems(dict, isAddAll);
if (
this.supportSelectAll() && this.canShowOptionItem(this.selectAllItem, isAddAll, this.hasSelectAll)
) {
items.unshift(this.selectAllItem);
this.addNonChoiceItem(dict, this.selectAllItem, settings.specialChoicesOrder.selectAllItem);
}
super.addToVisibleChoices(items, isAddAll);
}
protected isHeadChoice(
item: ItemValue,
question: QuestionSelectBase
): boolean {
return (
item === (<QuestionCheckboxBase>question).selectAllItem
);
}
protected isBuiltInChoice(item: ItemValue, question: QuestionSelectBase): boolean {
return item === (<QuestionCheckboxBase>question).selectAllItem || super.isBuiltInChoice(item, question);
}

public isItemInList(item: ItemValue): boolean {
if (item == this.selectAllItem) return this.hasSelectAll;
return super.isItemInList(item);
Expand Down
3 changes: 2 additions & 1 deletion src/question_imagepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,10 @@ export class QuestionImagePickerModel extends QuestionCheckboxBase {
return this.multiSelect ? "checkbox" : "radio";
}

protected isFootChoice(_item: ItemValue, _question: QuestionSelectBase): boolean {
protected isBuiltInChoice(item: ItemValue, question: QuestionSelectBase): boolean {
return false;
}
protected addToVisibleChoices(items: Array<ItemValue>, isAddAll: boolean): void { }
public getSelectBaseRootCss(): string {
return new CssClassBuilder().append(super.getSelectBaseRootCss()).append(this.cssClasses.rootColumn, this.getCurrentColCount() == 1).toString();
}
Expand Down
5 changes: 5 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,11 @@ export var settings = {
* Default value: `"none"`
*/
noneItemValue: "none",
specialChoicesOrder: {
selectAllItem: [-1],
noneItem: [1],
otherItem: [2]
},
/**
* A list of supported validators by question type.
*/
Expand Down
75 changes: 75 additions & 0 deletions tests/question_baseselecttests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1425,3 +1425,78 @@ QUnit.test("SelectBase visibleChoices order & locale change", function (assert)
assert.equal(question.visibleChoices[0].calculatedText, "ABB", "the first visible item calculatedText, de");
assert.equal(question.visibleChoices[1].calculatedText, "BAA", "the second visible item calculatedText, de");
});
QUnit.test("SelectBase visibleChoices for selectAll, none and showOtherItem", function (assert) {
const json = { elements: [
{ type: "checkbox", name: "q1", choices: ["a", "b", "c"], showSelectAllItem: true, showNoneItem: true, showOtherItem: true }
] };
const survey = new SurveyModel(json);
const question = <QuestionCheckboxModel>survey.getQuestionByName("q1");
let choices = question.visibleChoices;
assert.equal(choices.length, 6, "6 items, #1");
assert.equal(choices[0].value, "selectall", "all index #1");
assert.equal(choices[4].value, "none", "none index #1");
assert.equal(choices[5].value, "other", "other index #1");

settings.specialChoicesOrder.noneItem = [3];
question.showNoneItem = false;
question.showNoneItem = true;
choices = question.visibleChoices;
assert.equal(choices.length, 6, "6 items, #2");
assert.equal(choices[0].value, "selectall", "all index #2");
assert.equal(choices[4].value, "other", "other index #2");
assert.equal(choices[5].value, "none", "none index #2");

settings.specialChoicesOrder.noneItem = [-3, 3];
question.showNoneItem = false;
question.showNoneItem = true;
choices = question.visibleChoices;
assert.equal(choices.length, 7, "7 items, #3");
assert.equal(choices[0].value, "none", "none index (up) #3");
assert.equal(choices[1].value, "selectall", "all index #3");
assert.equal(choices[5].value, "other", "other index #3");
assert.equal(choices[6].value, "none", "none index (bottom) #3");

settings.specialChoicesOrder.selectAllItem = [-1];
settings.specialChoicesOrder.noneItem = [1];
settings.specialChoicesOrder.otherItem = [2];
});
QUnit.test("Double noneItem and SelectAllItem", function (assert) {
settings.specialChoicesOrder.noneItem = [-3, 3];
const json = { elements: [
{ type: "checkbox", name: "q1", choices: ["a", "b", "c"], showSelectAllItem: true, showNoneItem: true, showOtherItem: true }
] };
const survey = new SurveyModel(json);
const question = <QuestionCheckboxModel>survey.getQuestionByName("q1");
question.selectAll();
assert.equal(question.isItemSelected(question.selectAllItem), true, "Select Item is selected");
assert.equal(question.isAllSelected, true, "isAllSelected #1");
question.value = ["a", "b"];
assert.equal(question.isItemSelected(question.selectAllItem), false, "Select Item is not selected");
assert.equal(question.isAllSelected, false, "isAllSelected #2");
settings.specialChoicesOrder.selectAllItem = [-1];
settings.specialChoicesOrder.noneItem = [1];
settings.specialChoicesOrder.otherItem = [2];
});
QUnit.test("Double noneItem & selectAllItem and headItems/footItems", function (assert) {
settings.specialChoicesOrder.noneItem = [-2, 4];
settings.specialChoicesOrder.selectAllItem = [-2, 3];
const json = { elements: [
{ type: "checkbox", name: "q1", choices: ["a", "b", "c"], showSelectAllItem: true, showNoneItem: true, showOtherItem: true }
] };
const survey = new SurveyModel(json);
const question = <QuestionCheckboxModel>survey.getQuestionByName("q1");
question.separateSpecialChoices = true;

assert.equal(question.visibleChoices.length, 8, "There are 8 items");
assert.equal(question.headItems.length, 2, "There are two items in head items");
assert.equal(question.headItems[0].value, "none", "head none");
assert.equal(question.headItems[1].value, "selectall", "head selectall");
assert.equal(question.footItems.length, 3, "There are three items in footer items");
assert.equal(question.footItems[0].value, "other", "foot other");
assert.equal(question.footItems[1].value, "selectall", "foot selectall");
assert.equal(question.footItems[2].value, "none", "foot none");

settings.specialChoicesOrder.selectAllItem = [-1];
settings.specialChoicesOrder.noneItem = [1];
settings.specialChoicesOrder.otherItem = [2];
});

0 comments on commit 59a8f66

Please sign in to comment.