Skip to content

Commit

Permalink
#7702 Dropdown - Support the Start With search method
Browse files Browse the repository at this point in the history
Fixes #7702
  • Loading branch information
novikov82 committed Jan 25, 2024
1 parent c94d452 commit 0d92116
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 4 deletions.
11 changes: 11 additions & 0 deletions src/dropdownListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ListModel } from "./list";
import { PopupModel } from "./popup";
import { Question } from "./question";
import { QuestionDropdownModel } from "./question_dropdown";
import { settings } from "./settings";
import { SurveyModel } from "./survey";
import { CssClassBuilder } from "./utils/cssClassBuilder";
import { IsTouch } from "./utils/devices";
Expand Down Expand Up @@ -159,6 +160,16 @@ export class DropdownListModel extends Base {
};
}
const res = new ListModel<ItemValue>(visibleItems, _onSelectionChanged, false, undefined, this.question.choicesLazyLoadEnabled ? this.listModelFilterStringChanged : undefined, this.listElementId);
res.setOnTextSearchCallback((text: string, textToSearch: string) => {
const options = { question: this.question, text: text, filter: textToSearch, result: undefined as boolean };
(this.question.survey as SurveyModel).onItemTextSearch.fire(this.question.survey as SurveyModel, options);
if (options.result !== undefined) return options.result;

let textInLow = text.toLocaleLowerCase();
textInLow = settings.comparator.normalizeTextCallback(textInLow, "filter");
const index = textInLow.indexOf(textToSearch.toLocaleLowerCase());
return this.question.searchMode == "startsWith" ? index == 0 : index > -1;
});
res.renderElements = false;
res.forceShowFilter = true;
res.areSameItemsCallback = (item1: IAction, item2: IAction): boolean => {
Expand Down
10 changes: 8 additions & 2 deletions src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface IListModel {
allowSelection?: boolean;
selectedItem?: IAction;
onFilterStringChangedCallback?: (text: string) => void;
onTextSearchCallback?: (text: string, textToSearch: string) => boolean;
}
export class ListModel<T extends BaseAction = Action> extends ActionContainer<T> {
private listContainerHtmlElement: HTMLElement;
Expand Down Expand Up @@ -65,7 +66,9 @@ export class ListModel<T extends BaseAction = Action> extends ActionContainer<T>

private hasText(item: T, filterStringInLow: string): boolean {
if (!filterStringInLow) return true;
let textInLow = (item.title || "").toLocaleLowerCase();
const text = item.title || "";
if (this.onTextSearchCallback) return this.onTextSearchCallback(text, filterStringInLow);
let textInLow = text.toLocaleLowerCase();
textInLow = settings.comparator.normalizeTextCallback(textInLow, "filter");
return textInLow.indexOf(filterStringInLow.toLocaleLowerCase()) > -1;
}
Expand Down Expand Up @@ -109,10 +112,13 @@ export class ListModel<T extends BaseAction = Action> extends ActionContainer<T>
this.setItems(items);
this.selectedItem = selectedItem;
}

private onTextSearchCallback: (text: string, textToSearch: string) => boolean;
public setOnFilterStringChangedCallback(callback: (text: string) => void) {
this.onFilterStringChangedCallback = callback;
}
public setOnTextSearchCallback(callback: (text: string, textToSearch: string) => boolean) {
this.onTextSearchCallback = callback;
}
public setItems(items: Array<IAction>, sortByVisibleIndex = true): void {
super.setItems(items, sortByVisibleIndex);
if(this.elementId) {
Expand Down
3 changes: 3 additions & 0 deletions src/question_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export class QuestionDropdownModel extends QuestionSelectBase {
}
}) searchEnabled: boolean;

@property() searchMode: "contains" | "startsWith";

@property({ defaultValue: false }) inputHasValue: boolean;
@property({ defaultValue: "" }) readOnlyText: string;
/**
Expand Down Expand Up @@ -343,6 +345,7 @@ Serializer.addClass(
{ name: "autocomplete", alternativeName: "autoComplete", choices: settings.questions.dataList, },
{ name: "renderAs", default: "default", visible: false },
{ name: "searchEnabled:boolean", default: true, visible: false },
{ name: "searchMode", default: "contains", choices: ["contains", "startsWith"], },
{ name: "choicesLazyLoadEnabled:boolean", default: false, visible: false },
{ name: "choicesLazyLoadPageSize:number", default: 25, visible: false },
{ name: "inputFieldComponent", visible: false },
Expand Down
14 changes: 14 additions & 0 deletions src/survey-events-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,20 @@ export interface ChoicesLazyLoadEvent extends QuestionEventMixin {
*/
skip: number;
}

export interface ItemTextSearchEvent extends QuestionEventMixin {
/**
* A text...
*/
text: string;
/**
* A search string used to filter.
*/
filter: string;

result: boolean;
}

export interface GetChoiceDisplayValueEvent extends QuestionEventMixin {
/**
* A method that you should call to assign display texts to the question.
Expand Down
4 changes: 3 additions & 1 deletion src/survey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
MatrixCellValidateEvent, DynamicPanelModifiedEvent, DynamicPanelRemovingEvent, TimerPanelInfoTextEvent, DynamicPanelItemValueChangedEvent, DynamicPanelGetTabTitleEvent,
DynamicPanelCurrentIndexChangedEvent, IsAnswerCorrectEvent, DragDropAllowEvent, ScrollingElementToTopEvent, GetQuestionTitleActionsEvent, GetPanelTitleActionsEvent,
GetPageTitleActionsEvent, GetPanelFooterActionsEvent, GetMatrixRowActionsEvent, ElementContentVisibilityChangedEvent, GetExpressionDisplayValueEvent,
ServerValidateQuestionsEvent, MultipleTextItemAddedEvent, MatrixColumnAddedEvent, GetQuestionDisplayValueEvent, PopupVisibleChangedEvent
ServerValidateQuestionsEvent, MultipleTextItemAddedEvent, MatrixColumnAddedEvent, GetQuestionDisplayValueEvent, PopupVisibleChangedEvent, ItemTextSearchEvent
} from "./survey-events-api";
import { QuestionMatrixDropdownModelBase } from "./question_matrixdropdownbase";
import { QuestionMatrixDynamicModel } from "./question_matrixdynamic";
Expand Down Expand Up @@ -611,6 +611,8 @@ export class SurveyModel extends SurveyElementCore
*/
public onChoicesLazyLoad: EventBase<SurveyModel, ChoicesLazyLoadEvent> = this.addEvent<SurveyModel, ChoicesLazyLoadEvent>();

public onItemTextSearch: EventBase<SurveyModel, ItemTextSearchEvent> = this.addEvent<SurveyModel, ItemTextSearchEvent>();

/**
* Use this event to load a display text for the [default choice item](https://surveyjs.io/form-library/documentation/questiondropdownmodel#defaultValue) in [Dropdown](https://surveyjs.io/form-library/documentation/questiondropdownmodel) and [Tag Box](https://surveyjs.io/form-library/documentation/questiontagboxmodel) questions.
*
Expand Down
64 changes: 63 additions & 1 deletion tests/dropdown_list_model_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,4 +916,66 @@ QUnit.test("DropdownListModel in panel filterString change callback", (assert) =
const dropdownListModel = (question.panels[0].elements[0] as QuestionDropdownModel).dropdownListModel;
dropdownListModel["listModel"].filterString = "abc";
assert.equal(dropdownListModel.filterString, "abc");
});
});
QUnit.test("DropdownListModel filter options", (assert) => {
const survey = new SurveyModel({
questions: [{
type: "dropdown",
name: "question1",
searchEnabled: true,
choices: [
"abc",
"abd",
"cab",
"efg"
]
}]
});
const question = <QuestionDropdownModel>survey.getAllQuestions()[0];
const dropdownListModel = question.dropdownListModel;
const list: ListModel = dropdownListModel.popupModel.contentComponentData.model as ListModel;

dropdownListModel.filterString = "ab";
const getfilteredItems = () => list.renderedActions.filter(item => list.isItemVisible(item));

assert.equal(list.renderedActions.length, 4);
assert.equal(getfilteredItems().length, 3);

question.searchMode = "startsWith";
assert.equal(list.renderedActions.length, 4);
assert.equal(getfilteredItems().length, 2);
});

QUnit.test("DropdownListModel filter event", (assert) => {
const survey = new SurveyModel({
questions: [{
type: "dropdown",
name: "question1",
searchEnabled: true,
choices: [
"abcd",
"abdd",
"cabd",
"efab"
]
}]
});

survey.onItemTextSearch.add((sender, options) => {
options.result = options.text.indexOf(options.filter) + options.filter.length == options.text.length;
});

const question = <QuestionDropdownModel>survey.getAllQuestions()[0];
const dropdownListModel = question.dropdownListModel;
const list: ListModel = dropdownListModel.popupModel.contentComponentData.model as ListModel;

dropdownListModel.filterString = "ab";
const getfilteredItems = () => list.renderedActions.filter(item => list.isItemVisible(item));

assert.equal(list.renderedActions.length, 4);
assert.equal(getfilteredItems().length, 1);

question.searchMode = "startsWith";
assert.equal(list.renderedActions.length, 4);
assert.equal(getfilteredItems().length, 1);
});

0 comments on commit 0d92116

Please sign in to comment.