uicore-ts 1.1.216 → 1.1.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiledScripts/UIAutocompleteDropdownView.d.ts +3 -0
- package/compiledScripts/UIAutocompleteDropdownView.js +9 -0
- package/compiledScripts/UIAutocompleteDropdownView.js.map +2 -2
- package/compiledScripts/UIAutocompleteRowView.d.ts +4 -0
- package/compiledScripts/UIAutocompleteRowView.js +26 -1
- package/compiledScripts/UIAutocompleteRowView.js.map +2 -2
- package/compiledScripts/UIAutocompleteTextField.d.ts +42 -0
- package/compiledScripts/UIAutocompleteTextField.js +54 -6
- package/compiledScripts/UIAutocompleteTextField.js.map +2 -2
- package/package.json +1 -1
- package/scripts/UIAutocompleteDropdownView.ts +13 -0
- package/scripts/UIAutocompleteRowView.ts +46 -1
- package/scripts/UIAutocompleteTextField.ts +126 -6
|
@@ -5,6 +5,7 @@ export declare class UIAutocompleteDropdownView<T> extends UIView {
|
|
|
5
5
|
tableView: UITableView;
|
|
6
6
|
_fullHeightView: UIView;
|
|
7
7
|
_filteredItems: UIAutocompleteItem<T>[];
|
|
8
|
+
_filterWords: string[];
|
|
8
9
|
_highlightedRowIndex: number;
|
|
9
10
|
_rowHeight: number;
|
|
10
11
|
_maxVisibleRows: number;
|
|
@@ -23,6 +24,8 @@ export declare class UIAutocompleteDropdownView<T> extends UIView {
|
|
|
23
24
|
get highlightedItem(): UIAutocompleteItem<T> | undefined;
|
|
24
25
|
set filteredItems(items: UIAutocompleteItem<T>[]);
|
|
25
26
|
get filteredItems(): UIAutocompleteItem<T>[];
|
|
27
|
+
set filterWords(words: string[]);
|
|
28
|
+
get filterWords(): string[];
|
|
26
29
|
/** Anchors this dropdown below the given field view inside the rootView. */
|
|
27
30
|
showAnchoredToView(anchorView: UIView): void;
|
|
28
31
|
dismiss(): void;
|
|
@@ -30,6 +30,7 @@ class UIAutocompleteDropdownView extends import_UIView.UIView {
|
|
|
30
30
|
constructor(elementID) {
|
|
31
31
|
super(elementID);
|
|
32
32
|
this._filteredItems = [];
|
|
33
|
+
this._filterWords = [];
|
|
33
34
|
this._highlightedRowIndex = -1;
|
|
34
35
|
this._rowHeight = 36;
|
|
35
36
|
this._maxVisibleRows = 8;
|
|
@@ -66,6 +67,7 @@ class UIAutocompleteDropdownView extends import_UIView.UIView {
|
|
|
66
67
|
if ((0, import_UIObject.IS)(item)) {
|
|
67
68
|
row.item = item;
|
|
68
69
|
}
|
|
70
|
+
row.filterWords = this._filterWords;
|
|
69
71
|
row.selected = index === this._highlightedRowIndex;
|
|
70
72
|
const rowWasHovered = () => {
|
|
71
73
|
if (this._suppressHoverHighlight) {
|
|
@@ -145,6 +147,13 @@ class UIAutocompleteDropdownView extends import_UIView.UIView {
|
|
|
145
147
|
get filteredItems() {
|
|
146
148
|
return this._filteredItems;
|
|
147
149
|
}
|
|
150
|
+
set filterWords(words) {
|
|
151
|
+
this._filterWords = words;
|
|
152
|
+
this.tableView.reloadData();
|
|
153
|
+
}
|
|
154
|
+
get filterWords() {
|
|
155
|
+
return this._filterWords;
|
|
156
|
+
}
|
|
148
157
|
showAnchoredToView(anchorView) {
|
|
149
158
|
this.anchorView = anchorView;
|
|
150
159
|
this.calculateAndSetViewFrame = () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../scripts/UIAutocompleteDropdownView.ts"],
|
|
4
|
-
"sourcesContent": ["// noinspection JSConstantReassignment\n\nimport { UIAutocompleteItem, UIAutocompleteRowView } from \"./UIAutocompleteRowView\"\nimport { UIColor } from \"./UIColor\"\nimport { IS, IS_NOT, NO, YES } from \"./UIObject\"\nimport { UIRectangle } from \"./UIRectangle\"\nimport { UITableView } from \"./UITableView\"\nimport { UIView } from \"./UIView\"\n\n\nexport class UIAutocompleteDropdownView<T> extends UIView {\n \n tableView: UITableView\n _fullHeightView: UIView\n \n _filteredItems: UIAutocompleteItem<T>[] = []\n _highlightedRowIndex: number = -1\n _rowHeight: number = 36\n _maxVisibleRows: number = 8\n _isPointerInsideDropdown: boolean = NO\n _suppressHoverHighlight: boolean = NO\n \n didSelectItem?: (item: UIAutocompleteItem<T>) => void\n anchorView?: UIView\n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this.hidden = YES\n this.userInteractionEnabled = YES\n \n this.backgroundColor = UIColor.whiteColor\n this.setBorder(0, 1)\n this.style.boxSizing = \"content-box\"\n \n this.tableView = new UITableView(elementID ? elementID + \"TableView\" : undefined)\n this.addSubview(this.tableView)\n \n this.tableView.allRowsHaveEqualHeight = YES\n this.tableView.numberOfRows = () => this._filteredItems.length\n this.tableView.heightForRowWithIndex = () => this._rowHeight\n this.tableView.newReusableViewForIdentifier = (identifier, rowIndex) => this.newRowView(identifier, rowIndex)\n this.tableView.viewForRowWithIndex = (index) => this.viewForRowWithIndex(index)\n \n // A transparent full-height view so the native scrollbar reflects the total\n // content height rather than just the virtualised visible rows.\n this._fullHeightView = new UIView(elementID ? elementID + \"FullHeightView\" : undefined)\n this._fullHeightView.userInteractionEnabled = NO\n this.tableView.addSubview(this._fullHeightView)\n \n // Use a native mousemove listener on the tableView element so we catch movement\n // regardless of which child row the pointer is over (framework events don't bubble\n // up through the scroll container from its row children).\n this.tableView.viewHTMLElement.addEventListener(\"mousemove\", () => {\n this._suppressHoverHighlight = NO\n })\n \n }\n \n \n /** Override in subclass to provide custom row views. */\n newRowView(identifier: string, rowIndex: number): UIAutocompleteRowView<T> {\n return new UIAutocompleteRowView<T>(this.elementID + identifier + rowIndex)\n }\n \n \n viewForRowWithIndex(index: number): UIView {\n \n const row = this.tableView.reusableViewForIdentifier(\n \"AutocompleteRow\",\n index\n ) as UIAutocompleteRowView<T>\n \n const item = this._filteredItems[index]\n if (IS(item)) {\n row.item = item\n }\n \n // Reflect current keyboard highlight state via the native selected flag.\n row.selected = (index === this._highlightedRowIndex)\n \n // PointerHover fires as the pointer moves over the row.\n // We suppress scroll-into-view since the user is already looking at the row.\n // We also suppress highlight changes after a keyboard-triggered scroll, until\n // the pointer actually moves (PointerMove clears the suppression flag).\n const rowWasHovered = () => {\n if (this._suppressHoverHighlight) {\n return\n }\n this._setHighlightedRowIndex(index, NO)\n }\n if ((row as any)._autocompleteHoverHandler) {\n row.removeTargetForControlEvent(\n UIView.controlEvent.PointerHover,\n (row as any)._autocompleteHoverHandler\n )\n }\n row.controlEventTargetAccumulator.PointerHover = rowWasHovered;\n (row as any)._autocompleteHoverHandler = rowWasHovered\n \n // Clicking a row selects it.\n const rowWasTapped = () => {\n if (IS(item) && this.didSelectItem) {\n this.didSelectItem(item)\n }\n }\n if ((row as any)._autocompleteTapHandler) {\n row.removeTargetForControlEvent(\n UIView.controlEvent.PointerUpInside,\n (row as any)._autocompleteTapHandler\n )\n }\n row.controlEventTargetAccumulator.PointerUpInside = rowWasTapped;\n (row as any)._autocompleteTapHandler = rowWasTapped\n \n return row\n \n }\n \n \n get highlightedRowIndex(): number {\n return this._highlightedRowIndex\n }\n \n set highlightedRowIndex(index: number) {\n this._setHighlightedRowIndex(index, YES)\n }\n \n \n /** Internal setter. scrollIntoView=YES for keyboard navigation, NO for pointer hover. */\n _setHighlightedRowIndex(index: number, scrollIntoView: boolean) {\n \n const previousIndex = this._highlightedRowIndex\n this._highlightedRowIndex = index\n \n // Clear selected state on previous row.\n const previousRow = this.tableView.visibleRowWithIndex(previousIndex) as\n UIAutocompleteRowView<T> | undefined\n if (IS(previousRow)) {\n previousRow.selected = NO\n }\n \n // Set selected state on newly highlighted row.\n const currentRow = this.tableView.visibleRowWithIndex(index) as\n UIAutocompleteRowView<T> | undefined\n if (IS(currentRow)) {\n currentRow.selected = YES\n \n if (scrollIntoView) {\n // Scroll the view if needed\n let contentOffset = this.tableView.contentOffset\n if (currentRow.frame.y < contentOffset.y) {\n contentOffset.y = currentRow.frame.y\n }\n if (currentRow.frame.max.y > (contentOffset.y + this.tableView.bounds.height)) {\n contentOffset = contentOffset.pointByAddingY(-(contentOffset.y + this.tableView.bounds.height -\n currentRow.frame.max.y))\n }\n const animationDuration = this.tableView.animationDuration\n this.tableView.animationDuration = 0\n this.tableView.contentOffset = contentOffset\n this.tableView.animationDuration = animationDuration\n \n // Suppress hover-driven highlight changes until the user physically\n // moves the mouse \u2014 the native mousemove listener on the tableView\n // element clears this flag when actual movement is detected.\n this._suppressHoverHighlight = YES\n }\n }\n \n }\n \n \n get highlightedItem(): UIAutocompleteItem<T> | undefined {\n if (this._highlightedRowIndex >= 0 && this._highlightedRowIndex < this._filteredItems.length) {\n return this._filteredItems[this._highlightedRowIndex]\n }\n return undefined\n }\n \n \n set filteredItems(items: UIAutocompleteItem<T>[]) {\n this._filteredItems = items\n this._highlightedRowIndex = -1\n this.tableView.reloadData()\n this.hidden = (items.length === 0)\n this._updateFullHeightView()\n this.setNeedsLayout()\n }\n \n get filteredItems(): UIAutocompleteItem<T>[] {\n return this._filteredItems\n }\n \n \n /** Anchors this dropdown below the given field view inside the rootView. */\n showAnchoredToView(anchorView: UIView) {\n \n this.anchorView = anchorView\n \n this.calculateAndSetViewFrame = () => {\n \n const rootView = anchorView.rootView\n \n const padding = anchorView.core.paddingLength\n \n if (!this.superview || this.superview !== rootView) {\n this.removeFromSuperview()\n rootView.addSubview(this)\n }\n \n const fieldFrameInRoot = (this.anchorView?.superview?.rectangleInView(\n this.anchorView?.frame,\n rootView\n ) as UIRectangle)\n .rectangleByAddingX(padding)\n .rectangleByAddingY(padding)\n \n if (IS_NOT(fieldFrameInRoot)) {\n return\n }\n \n const dropdownHeight = Math.min(\n this._filteredItems.length * this._rowHeight,\n this._maxVisibleRows * this._rowHeight\n )\n \n \n this.frame = fieldFrameInRoot.rectangleForNextRow(0, dropdownHeight)\n \n }\n \n this.setNeedsLayoutUpToRootView()\n this.calculateAndSetViewFrame()\n \n this.style.zIndex = \"10000\"\n this.hidden = (this._filteredItems.length === 0)\n \n }\n \n \n dismiss() {\n this.hidden = YES\n this._highlightedRowIndex = -1\n this._isPointerInsideDropdown = NO\n }\n \n \n _updateFullHeightView() {\n const totalHeight = this._filteredItems.length * this._rowHeight\n this._fullHeightView.frame = this._fullHeightView.frame\n .rectangleWithY(0)\n .rectangleWithHeight(totalHeight)\n .rectangleWithWidth(1)\n this._fullHeightView.hasWeakFrame = YES\n }\n \n \n override layoutSubviews() {\n \n super.layoutSubviews()\n \n const bounds = this.contentBounds\n this.tableView.frame = bounds\n this._updateFullHeightView()\n \n }\n \n \n}\n\n\n\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mCAA0D;AAC1D,qBAAwB;AACxB,sBAAoC;AAEpC,yBAA4B;AAC5B,oBAAuB;AAGhB,MAAM,mCAAsC,qBAAO;AAAA,
|
|
4
|
+
"sourcesContent": ["// noinspection JSConstantReassignment\n\nimport { UIAutocompleteItem, UIAutocompleteRowView } from \"./UIAutocompleteRowView\"\nimport { UIColor } from \"./UIColor\"\nimport { IS, IS_NOT, NO, YES } from \"./UIObject\"\nimport { UIRectangle } from \"./UIRectangle\"\nimport { UITableView } from \"./UITableView\"\nimport { UIView } from \"./UIView\"\n\n\nexport class UIAutocompleteDropdownView<T> extends UIView {\n \n tableView: UITableView\n _fullHeightView: UIView\n \n _filteredItems: UIAutocompleteItem<T>[] = []\n _filterWords: string[] = []\n _highlightedRowIndex: number = -1\n _rowHeight: number = 36\n _maxVisibleRows: number = 8\n _isPointerInsideDropdown: boolean = NO\n _suppressHoverHighlight: boolean = NO\n \n didSelectItem?: (item: UIAutocompleteItem<T>) => void\n anchorView?: UIView\n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this.hidden = YES\n this.userInteractionEnabled = YES\n \n this.backgroundColor = UIColor.whiteColor\n this.setBorder(0, 1)\n this.style.boxSizing = \"content-box\"\n \n this.tableView = new UITableView(elementID ? elementID + \"TableView\" : undefined)\n this.addSubview(this.tableView)\n \n this.tableView.allRowsHaveEqualHeight = YES\n this.tableView.numberOfRows = () => this._filteredItems.length\n this.tableView.heightForRowWithIndex = () => this._rowHeight\n this.tableView.newReusableViewForIdentifier = (identifier, rowIndex) => this.newRowView(identifier, rowIndex)\n this.tableView.viewForRowWithIndex = (index) => this.viewForRowWithIndex(index)\n \n // A transparent full-height view so the native scrollbar reflects the total\n // content height rather than just the virtualised visible rows.\n this._fullHeightView = new UIView(elementID ? elementID + \"FullHeightView\" : undefined)\n this._fullHeightView.userInteractionEnabled = NO\n this.tableView.addSubview(this._fullHeightView)\n \n // Use a native mousemove listener on the tableView element so we catch movement\n // regardless of which child row the pointer is over (framework events don't bubble\n // up through the scroll container from its row children).\n this.tableView.viewHTMLElement.addEventListener(\"mousemove\", () => {\n this._suppressHoverHighlight = NO\n })\n \n }\n \n \n /** Override in subclass to provide custom row views. */\n newRowView(identifier: string, rowIndex: number): UIAutocompleteRowView<T> {\n return new UIAutocompleteRowView<T>(this.elementID + identifier + rowIndex)\n }\n \n \n viewForRowWithIndex(index: number): UIView {\n \n const row = this.tableView.reusableViewForIdentifier(\n \"AutocompleteRow\",\n index\n ) as UIAutocompleteRowView<T>\n \n const item = this._filteredItems[index]\n if (IS(item)) {\n row.item = item\n }\n \n row.filterWords = this._filterWords\n \n // Reflect current keyboard highlight state via the native selected flag.\n row.selected = (index === this._highlightedRowIndex)\n \n // PointerHover fires as the pointer moves over the row.\n // We suppress scroll-into-view since the user is already looking at the row.\n // We also suppress highlight changes after a keyboard-triggered scroll, until\n // the pointer actually moves (PointerMove clears the suppression flag).\n const rowWasHovered = () => {\n if (this._suppressHoverHighlight) {\n return\n }\n this._setHighlightedRowIndex(index, NO)\n }\n if ((row as any)._autocompleteHoverHandler) {\n row.removeTargetForControlEvent(\n UIView.controlEvent.PointerHover,\n (row as any)._autocompleteHoverHandler\n )\n }\n row.controlEventTargetAccumulator.PointerHover = rowWasHovered;\n (row as any)._autocompleteHoverHandler = rowWasHovered\n \n // Clicking a row selects it.\n const rowWasTapped = () => {\n if (IS(item) && this.didSelectItem) {\n this.didSelectItem(item)\n }\n }\n if ((row as any)._autocompleteTapHandler) {\n row.removeTargetForControlEvent(\n UIView.controlEvent.PointerUpInside,\n (row as any)._autocompleteTapHandler\n )\n }\n row.controlEventTargetAccumulator.PointerUpInside = rowWasTapped;\n (row as any)._autocompleteTapHandler = rowWasTapped\n \n return row\n \n }\n \n \n get highlightedRowIndex(): number {\n return this._highlightedRowIndex\n }\n \n set highlightedRowIndex(index: number) {\n this._setHighlightedRowIndex(index, YES)\n }\n \n \n /** Internal setter. scrollIntoView=YES for keyboard navigation, NO for pointer hover. */\n _setHighlightedRowIndex(index: number, scrollIntoView: boolean) {\n \n const previousIndex = this._highlightedRowIndex\n this._highlightedRowIndex = index\n \n // Clear selected state on previous row.\n const previousRow = this.tableView.visibleRowWithIndex(previousIndex) as\n UIAutocompleteRowView<T> | undefined\n if (IS(previousRow)) {\n previousRow.selected = NO\n }\n \n // Set selected state on newly highlighted row.\n const currentRow = this.tableView.visibleRowWithIndex(index) as\n UIAutocompleteRowView<T> | undefined\n if (IS(currentRow)) {\n currentRow.selected = YES\n \n if (scrollIntoView) {\n // Scroll the view if needed\n let contentOffset = this.tableView.contentOffset\n if (currentRow.frame.y < contentOffset.y) {\n contentOffset.y = currentRow.frame.y\n }\n if (currentRow.frame.max.y > (contentOffset.y + this.tableView.bounds.height)) {\n contentOffset = contentOffset.pointByAddingY(-(contentOffset.y + this.tableView.bounds.height -\n currentRow.frame.max.y))\n }\n const animationDuration = this.tableView.animationDuration\n this.tableView.animationDuration = 0\n this.tableView.contentOffset = contentOffset\n this.tableView.animationDuration = animationDuration\n \n // Suppress hover-driven highlight changes until the user physically\n // moves the mouse \u2014 the native mousemove listener on the tableView\n // element clears this flag when actual movement is detected.\n this._suppressHoverHighlight = YES\n }\n }\n \n }\n \n \n get highlightedItem(): UIAutocompleteItem<T> | undefined {\n if (this._highlightedRowIndex >= 0 && this._highlightedRowIndex < this._filteredItems.length) {\n return this._filteredItems[this._highlightedRowIndex]\n }\n return undefined\n }\n \n \n set filteredItems(items: UIAutocompleteItem<T>[]) {\n this._filteredItems = items\n this._highlightedRowIndex = -1\n this.tableView.reloadData()\n this.hidden = (items.length === 0)\n this._updateFullHeightView()\n this.setNeedsLayout()\n }\n \n get filteredItems(): UIAutocompleteItem<T>[] {\n return this._filteredItems\n }\n \n \n set filterWords(words: string[]) {\n this._filterWords = words\n this.tableView.reloadData()\n }\n \n get filterWords(): string[] {\n return this._filterWords\n }\n \n \n /** Anchors this dropdown below the given field view inside the rootView. */\n showAnchoredToView(anchorView: UIView) {\n \n this.anchorView = anchorView\n \n this.calculateAndSetViewFrame = () => {\n \n const rootView = anchorView.rootView\n \n const padding = anchorView.core.paddingLength\n \n if (!this.superview || this.superview !== rootView) {\n this.removeFromSuperview()\n rootView.addSubview(this)\n }\n \n const fieldFrameInRoot = (this.anchorView?.superview?.rectangleInView(\n this.anchorView?.frame,\n rootView\n ) as UIRectangle)\n .rectangleByAddingX(padding)\n .rectangleByAddingY(padding)\n \n if (IS_NOT(fieldFrameInRoot)) {\n return\n }\n \n const dropdownHeight = Math.min(\n this._filteredItems.length * this._rowHeight,\n this._maxVisibleRows * this._rowHeight\n )\n \n \n this.frame = fieldFrameInRoot.rectangleForNextRow(0, dropdownHeight)\n \n }\n \n this.setNeedsLayoutUpToRootView()\n this.calculateAndSetViewFrame()\n \n this.style.zIndex = \"10000\"\n this.hidden = (this._filteredItems.length === 0)\n \n }\n \n \n dismiss() {\n this.hidden = YES\n this._highlightedRowIndex = -1\n this._isPointerInsideDropdown = NO\n }\n \n \n _updateFullHeightView() {\n const totalHeight = this._filteredItems.length * this._rowHeight\n this._fullHeightView.frame = this._fullHeightView.frame\n .rectangleWithY(0)\n .rectangleWithHeight(totalHeight)\n .rectangleWithWidth(1)\n this._fullHeightView.hasWeakFrame = YES\n }\n \n \n override layoutSubviews() {\n \n super.layoutSubviews()\n \n const bounds = this.contentBounds\n this.tableView.frame = bounds\n this._updateFullHeightView()\n \n }\n \n \n}\n\n\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,7 +5,11 @@ export interface UIAutocompleteItem<T> {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class UIAutocompleteRowView<T> extends UIButton {
|
|
7
7
|
_item?: UIAutocompleteItem<T>;
|
|
8
|
+
_filterWords: string[];
|
|
8
9
|
constructor(elementID?: string);
|
|
9
10
|
set item(item: UIAutocompleteItem<T>);
|
|
10
11
|
get item(): UIAutocompleteItem<T> | undefined;
|
|
12
|
+
set filterWords(words: string[]);
|
|
13
|
+
get filterWords(): string[];
|
|
14
|
+
_updateLabelContent(): void;
|
|
11
15
|
}
|
|
@@ -28,6 +28,7 @@ var import_UITextView = require("./UITextView");
|
|
|
28
28
|
class UIAutocompleteRowView extends import_UIButton.UIButton {
|
|
29
29
|
constructor(elementID) {
|
|
30
30
|
super(elementID);
|
|
31
|
+
this._filterWords = [];
|
|
31
32
|
this.titleLabel.textAlignment = import_UITextView.UITextView.textAlignment.left;
|
|
32
33
|
this.userInteractionEnabled = import_UIObject.YES;
|
|
33
34
|
this.style.outline = "none";
|
|
@@ -48,11 +49,35 @@ class UIAutocompleteRowView extends import_UIButton.UIButton {
|
|
|
48
49
|
}
|
|
49
50
|
set item(item) {
|
|
50
51
|
this._item = item;
|
|
51
|
-
this.
|
|
52
|
+
this._updateLabelContent();
|
|
52
53
|
}
|
|
53
54
|
get item() {
|
|
54
55
|
return this._item;
|
|
55
56
|
}
|
|
57
|
+
set filterWords(words) {
|
|
58
|
+
this._filterWords = words;
|
|
59
|
+
this._updateLabelContent();
|
|
60
|
+
}
|
|
61
|
+
get filterWords() {
|
|
62
|
+
return this._filterWords;
|
|
63
|
+
}
|
|
64
|
+
_updateLabelContent() {
|
|
65
|
+
if (!this._item) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const label = this._item.label;
|
|
69
|
+
if (this._filterWords.length === 0) {
|
|
70
|
+
this.titleLabel.text = label;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const escapedWords = this._filterWords.map(
|
|
74
|
+
(word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
75
|
+
);
|
|
76
|
+
const pattern = new RegExp(`(${escapedWords.join("|")})`, "gi");
|
|
77
|
+
const escaped = label.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
78
|
+
const highlighted = escaped.replace(pattern, "<strong>$1</strong>");
|
|
79
|
+
this.titleLabel.innerHTML = highlighted;
|
|
80
|
+
}
|
|
56
81
|
}
|
|
57
82
|
// Annotate the CommonJS export names for ESM import in node:
|
|
58
83
|
0 && (module.exports = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../scripts/UIAutocompleteRowView.ts"],
|
|
4
|
-
"sourcesContent": ["import { UIButton } from \"./UIButton\"\nimport { UIColor } from \"./UIColor\"\nimport { NO, YES } from \"./UIObject\"\nimport { UITextView } from \"./UITextView\"\n\n\nexport interface UIAutocompleteItem<T> {\n label: string\n value: T\n}\n\n\nexport class UIAutocompleteRowView<T> extends UIButton {\n \n _item?: UIAutocompleteItem<T>\n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this.titleLabel.textAlignment = UITextView.textAlignment.left\n this.userInteractionEnabled = YES\n this.style.outline = \"none\"\n this.viewHTMLElement.setAttribute(\"tabindex\", \"-1\")\n \n this.colors = {\n titleLabel: {\n normal: UIColor.blackColor,\n highlighted: UIColor.blackColor,\n selected: UIColor.whiteColor\n },\n background: {\n normal: UIColor.whiteColor,\n hovered: UIColor.lightGreyColor,\n highlighted: UIColor.lightGreyColor,\n selected: UIColor.greyColor\n }\n }\n \n }\n \n \n set item(item: UIAutocompleteItem<T>) {\n this._item = item\n this.
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,qBAAwB;AACxB,sBAAwB;AACxB,wBAA2B;AASpB,MAAM,8BAAiC,yBAAS;AAAA,
|
|
4
|
+
"sourcesContent": ["import { UIButton } from \"./UIButton\"\nimport { UIColor } from \"./UIColor\"\nimport { NO, YES } from \"./UIObject\"\nimport { UITextView } from \"./UITextView\"\n\n\nexport interface UIAutocompleteItem<T> {\n label: string\n value: T\n}\n\n\nexport class UIAutocompleteRowView<T> extends UIButton {\n \n _item?: UIAutocompleteItem<T>\n _filterWords: string[] = []\n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this.titleLabel.textAlignment = UITextView.textAlignment.left\n this.userInteractionEnabled = YES\n this.style.outline = \"none\"\n this.viewHTMLElement.setAttribute(\"tabindex\", \"-1\")\n \n this.colors = {\n titleLabel: {\n normal: UIColor.blackColor,\n highlighted: UIColor.blackColor,\n selected: UIColor.whiteColor\n },\n background: {\n normal: UIColor.whiteColor,\n hovered: UIColor.lightGreyColor,\n highlighted: UIColor.lightGreyColor,\n selected: UIColor.greyColor\n }\n }\n \n }\n \n \n set item(item: UIAutocompleteItem<T>) {\n this._item = item\n this._updateLabelContent()\n }\n \n get item(): UIAutocompleteItem<T> | undefined {\n return this._item\n }\n \n \n set filterWords(words: string[]) {\n this._filterWords = words\n this._updateLabelContent()\n }\n \n get filterWords(): string[] {\n return this._filterWords\n }\n \n \n _updateLabelContent() {\n \n if (!this._item) {\n return\n }\n \n const label = this._item.label\n \n if (this._filterWords.length === 0) {\n this.titleLabel.text = label\n return\n }\n \n // Build a regex that matches any of the filter words (case-insensitive).\n // Words are escaped so special regex characters in the label are treated literally.\n const escapedWords = this._filterWords.map(\n word => word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n )\n const pattern = new RegExp(`(${escapedWords.join(\"|\")})`, \"gi\")\n \n // HTML-escape the label first, then re-insert <strong> tags around matches.\n const escaped = label\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n \n const highlighted = escaped.replace(pattern, \"<strong>$1</strong>\")\n \n this.titleLabel.innerHTML = highlighted\n \n }\n \n \n}\n\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,qBAAwB;AACxB,sBAAwB;AACxB,wBAA2B;AASpB,MAAM,8BAAiC,yBAAS;AAAA,EAKnD,YAAY,WAAoB;AAE5B,UAAM,SAAS;AAJnB,wBAAyB,CAAC;AAMtB,SAAK,WAAW,gBAAgB,6BAAW,cAAc;AACzD,SAAK,yBAAyB;AAC9B,SAAK,MAAM,UAAU;AACrB,SAAK,gBAAgB,aAAa,YAAY,IAAI;AAElD,SAAK,SAAS;AAAA,MACV,YAAY;AAAA,QACR,QAAQ,uBAAQ;AAAA,QAChB,aAAa,uBAAQ;AAAA,QACrB,UAAU,uBAAQ;AAAA,MACtB;AAAA,MACA,YAAY;AAAA,QACR,QAAQ,uBAAQ;AAAA,QAChB,SAAS,uBAAQ;AAAA,QACjB,aAAa,uBAAQ;AAAA,QACrB,UAAU,uBAAQ;AAAA,MACtB;AAAA,IACJ;AAAA,EAEJ;AAAA,EAGA,IAAI,KAAK,MAA6B;AAClC,SAAK,QAAQ;AACb,SAAK,oBAAoB;AAAA,EAC7B;AAAA,EAEA,IAAI,OAA0C;AAC1C,WAAO,KAAK;AAAA,EAChB;AAAA,EAGA,IAAI,YAAY,OAAiB;AAC7B,SAAK,eAAe;AACpB,SAAK,oBAAoB;AAAA,EAC7B;AAAA,EAEA,IAAI,cAAwB;AACxB,WAAO,KAAK;AAAA,EAChB;AAAA,EAGA,sBAAsB;AAElB,QAAI,CAAC,KAAK,OAAO;AACb;AAAA,IACJ;AAEA,UAAM,QAAQ,KAAK,MAAM;AAEzB,QAAI,KAAK,aAAa,WAAW,GAAG;AAChC,WAAK,WAAW,OAAO;AACvB;AAAA,IACJ;AAIA,UAAM,eAAe,KAAK,aAAa;AAAA,MACnC,UAAQ,KAAK,QAAQ,uBAAuB,MAAM;AAAA,IACtD;AACA,UAAM,UAAU,IAAI,OAAO,IAAI,aAAa,KAAK,GAAG,MAAM,IAAI;AAG9D,UAAM,UAAU,MACX,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAE3B,UAAM,cAAc,QAAQ,QAAQ,SAAS,qBAAqB;AAElE,SAAK,WAAW,YAAY;AAAA,EAEhC;AAGJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,6 +9,12 @@ export declare class UIAutocompleteTextField<T = string> extends UITextField {
|
|
|
9
9
|
_isDropdownOpen: boolean;
|
|
10
10
|
_strictSelection: boolean;
|
|
11
11
|
_isValid: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* When YES, the filter text is split on whitespace and all words must appear
|
|
14
|
+
* in the item label (AND logic). When NO (default), the full filter string is
|
|
15
|
+
* matched as a single substring.
|
|
16
|
+
*/
|
|
17
|
+
usesMultiWordAndSearch: boolean;
|
|
12
18
|
static controlEvent: {
|
|
13
19
|
readonly PointerDown: "PointerDown";
|
|
14
20
|
readonly PointerMove: "PointerMove";
|
|
@@ -50,6 +56,42 @@ export declare class UIAutocompleteTextField<T = string> extends UITextField {
|
|
|
50
56
|
get autocompleteStrings(): string[];
|
|
51
57
|
set autocompleteData(items: UIAutocompleteItem<T>[]);
|
|
52
58
|
get autocompleteData(): UIAutocompleteItem<T>[];
|
|
59
|
+
/**
|
|
60
|
+
* Splits the given lowercase-trimmed filter text into individual words when
|
|
61
|
+
* usesMultiWordAndSearch is YES, or returns it as a single-element array otherwise.
|
|
62
|
+
* Returns an empty array when the input is empty.
|
|
63
|
+
*/
|
|
64
|
+
_filterWordsFromText(filterText: string): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the given label (already lowercased) satisfies all filter
|
|
67
|
+
* words — i.e. every word appears somewhere in the label.
|
|
68
|
+
*/
|
|
69
|
+
_labelMatchesFilterWords(label: string, filterWords: string[]): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Returns true when the character immediately before `position` in `label`
|
|
72
|
+
* is a word separator (or the position is at the start of the string).
|
|
73
|
+
* Used to give a bonus to matches that start at a word boundary.
|
|
74
|
+
*/
|
|
75
|
+
_isWordBoundary(label: string, position: number): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Scores a label against the filter words. Lower score = better match.
|
|
78
|
+
*
|
|
79
|
+
* Scoring factors (in priority order):
|
|
80
|
+
* 1. Non-sequential penalty — words must appear in typed order to avoid
|
|
81
|
+
* a large penalty that pushes them below all sequential matches.
|
|
82
|
+
* 2. Per-word boundary score — for each filter word, a mid-word match
|
|
83
|
+
* scores worse than a word-boundary match. The sum across all words
|
|
84
|
+
* determines the boundary tier.
|
|
85
|
+
* 3. Position of the first matched word — within the same boundary tier,
|
|
86
|
+
* earlier appearances rank higher.
|
|
87
|
+
* 4. Total label length — shorter labels are more specific (tiebreaker).
|
|
88
|
+
*
|
|
89
|
+
* Example: query "põ pu"
|
|
90
|
+
* "Põhjavee puhastusvahendid" → "põ" at boundary(0), "pu" at boundary(9) → low boundary score
|
|
91
|
+
* "põrandapuhastusvahendid" → "põ" at boundary(0), "pu" mid-word(7) → higher boundary score
|
|
92
|
+
* → "Põhjavee puhastusvahendid" ranks first.
|
|
93
|
+
*/
|
|
94
|
+
_scoreLabel(label: string, filterWords: string[]): number;
|
|
53
95
|
updateFilteredItems(): void;
|
|
54
96
|
openDropdown(): void;
|
|
55
97
|
closeDropdown(): void;
|
|
@@ -32,6 +32,7 @@ const _UIAutocompleteTextField = class extends import_UITextField.UITextField {
|
|
|
32
32
|
this._isDropdownOpen = import_UIObject.NO;
|
|
33
33
|
this._strictSelection = import_UIObject.NO;
|
|
34
34
|
this._isValid = import_UIObject.YES;
|
|
35
|
+
this.usesMultiWordAndSearch = import_UIObject.NO;
|
|
35
36
|
this._dropdownView = this.newDropdownView();
|
|
36
37
|
this._dropdownView.didSelectItem = (item) => {
|
|
37
38
|
this.commitSelection(item);
|
|
@@ -141,17 +142,63 @@ const _UIAutocompleteTextField = class extends import_UITextField.UITextField {
|
|
|
141
142
|
get autocompleteData() {
|
|
142
143
|
return this._autocompleteItems;
|
|
143
144
|
}
|
|
145
|
+
_filterWordsFromText(filterText) {
|
|
146
|
+
if (filterText.length === 0) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
if (this.usesMultiWordAndSearch) {
|
|
150
|
+
return filterText.split(/\s+/).filter((word) => word.length > 0);
|
|
151
|
+
}
|
|
152
|
+
return [filterText];
|
|
153
|
+
}
|
|
154
|
+
_labelMatchesFilterWords(label, filterWords) {
|
|
155
|
+
return filterWords.every((word) => label.includes(word));
|
|
156
|
+
}
|
|
157
|
+
_isWordBoundary(label, position) {
|
|
158
|
+
if (position === 0) {
|
|
159
|
+
return import_UIObject.YES;
|
|
160
|
+
}
|
|
161
|
+
const charBefore = label[position - 1];
|
|
162
|
+
return " -/\\|._,;:()[]".includes(charBefore);
|
|
163
|
+
}
|
|
164
|
+
_scoreLabel(label, filterWords) {
|
|
165
|
+
if (filterWords.length === 0) {
|
|
166
|
+
return label.length;
|
|
167
|
+
}
|
|
168
|
+
let cursor = 0;
|
|
169
|
+
let isSequential = import_UIObject.YES;
|
|
170
|
+
const sequentialPositions = [];
|
|
171
|
+
for (const word of filterWords) {
|
|
172
|
+
const position = label.indexOf(word, cursor);
|
|
173
|
+
if (position === -1) {
|
|
174
|
+
isSequential = import_UIObject.NO;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
sequentialPositions.push(position);
|
|
178
|
+
cursor = position + word.length;
|
|
179
|
+
}
|
|
180
|
+
let boundaryScore = 0;
|
|
181
|
+
for (const word of filterWords) {
|
|
182
|
+
const position = label.indexOf(word);
|
|
183
|
+
if (position !== -1 && !this._isWordBoundary(label, position)) {
|
|
184
|
+
boundaryScore += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const firstMatchPosition = label.indexOf(filterWords[0]);
|
|
188
|
+
const sequentialPenalty = isSequential ? 0 : 1e7;
|
|
189
|
+
return sequentialPenalty + boundaryScore * 1e4 + firstMatchPosition * 100 + label.length;
|
|
190
|
+
}
|
|
144
191
|
updateFilteredItems() {
|
|
145
|
-
const
|
|
192
|
+
const rawFilterText = this.text.toLowerCase().trim();
|
|
193
|
+
const filterWords = this._filterWordsFromText(rawFilterText);
|
|
146
194
|
let filtered;
|
|
147
|
-
if (
|
|
195
|
+
if (filterWords.length === 0) {
|
|
148
196
|
filtered = this._autocompleteItems;
|
|
149
197
|
} else {
|
|
150
|
-
filtered = this._autocompleteItems.filter(
|
|
151
|
-
(item) => item.label.toLowerCase().includes(filterText)
|
|
152
|
-
);
|
|
198
|
+
filtered = this._autocompleteItems.filter((item) => this._labelMatchesFilterWords(item.label.toLowerCase(), filterWords)).map((item, originalIndex) => ({ item, originalIndex, score: this._scoreLabel(item.label.toLowerCase(), filterWords) })).sort((a, b) => a.score - b.score || a.originalIndex - b.originalIndex).map(({ item }) => item);
|
|
153
199
|
}
|
|
154
|
-
const isExactSingleMatch = filtered.length === 1 && filtered[0].label.toLowerCase() ===
|
|
200
|
+
const isExactSingleMatch = filtered.length === 1 && filtered[0].label.toLowerCase() === rawFilterText;
|
|
201
|
+
this._dropdownView.filterWords = filterWords;
|
|
155
202
|
this._dropdownView.filteredItems = isExactSingleMatch ? [] : filtered;
|
|
156
203
|
if (this._dropdownView.filteredItems.length > 0) {
|
|
157
204
|
this._dropdownView.highlightedRowIndex = 0;
|
|
@@ -165,6 +212,7 @@ const _UIAutocompleteTextField = class extends import_UITextField.UITextField {
|
|
|
165
212
|
return;
|
|
166
213
|
}
|
|
167
214
|
this._isDropdownOpen = import_UIObject.YES;
|
|
215
|
+
this._dropdownView.filterWords = [];
|
|
168
216
|
this.updateFilteredItems();
|
|
169
217
|
this._dropdownView.showAnchoredToView(this);
|
|
170
218
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../scripts/UIAutocompleteTextField.ts"],
|
|
4
|
-
"sourcesContent": ["import { UIAutocompleteDropdownView } from \"./UIAutocompleteDropdownView\"\nimport { UIAutocompleteItem } from \"./UIAutocompleteRowView\"\nimport { IS, IS_NOT, NO, YES } from \"./UIObject\"\nimport { UITextField } from \"./UITextField\"\nimport { UIView, UIViewAddControlEventTargetObject } from \"./UIView\"\n\n\nexport class UIAutocompleteTextField<T = string> extends UITextField {\n \n _autocompleteItems: UIAutocompleteItem<T>[] = []\n _selectedItem?: UIAutocompleteItem<T>\n _dropdownView: UIAutocompleteDropdownView<T>\n _isDropdownOpen: boolean = NO\n _strictSelection: boolean = NO\n _isValid: boolean = YES\n \n \n static override controlEvent = Object.assign({}, UITextField.controlEvent, {\n \"SelectionDidChange\": \"SelectionDidChange\"\n })\n \n override get controlEventTargetAccumulator(): UIViewAddControlEventTargetObject<typeof UIAutocompleteTextField> {\n return (super.controlEventTargetAccumulator as any)\n }\n \n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this._dropdownView = this.newDropdownView()\n \n this._dropdownView.didSelectItem = (item) => {\n this.commitSelection(item)\n }\n \n let textBeforeFocus = this.text\n let itemBeforeFocus = this.selectedItem\n // Open dropdown on focus\n this.controlEventTargetAccumulator.Focus = () => {\n textBeforeFocus = this.text\n itemBeforeFocus = this.selectedItem\n this.text = \"\"\n this.openDropdown()\n this.textElementView.viewHTMLElement.select()\n const matchIndex = this._dropdownView.filteredItems.findIndex(\n item => item.label === textBeforeFocus\n )\n if (matchIndex !== -1) {\n this._dropdownView.highlightedRowIndex = matchIndex\n }\n }\n \n // Close on blur\n this.controlEventTargetAccumulator.Blur = () => {\n this.closeDropdown()\n }\n \n // Filter on text change\n this.addTargetForControlEvent(UITextField.controlEvent.TextChange, () => {\n this._selectedItem = undefined\n this.updateFilteredItems()\n if (!this._isDropdownOpen) {\n this.openDropdown()\n }\n })\n \n // Keyboard navigation: down arrow\n this.textElementView.addTargetForControlEvent(UIView.controlEvent.DownArrowDown, (sender, event) => {\n event.preventDefault()\n if (!this._isDropdownOpen) {\n this.openDropdown()\n return\n }\n const maxIndex = this._dropdownView.filteredItems.length - 1\n if (this._dropdownView.highlightedRowIndex < maxIndex) {\n this._dropdownView.highlightedRowIndex = this._dropdownView.highlightedRowIndex + 1\n }\n })\n \n // Keyboard navigation: up arrow\n this.textElementView.addTargetForControlEvent(UIView.controlEvent.UpArrowDown, (sender, event) => {\n event.preventDefault()\n if (this._dropdownView.highlightedRowIndex > 0) {\n this._dropdownView.highlightedRowIndex = this._dropdownView.highlightedRowIndex - 1\n }\n })\n \n // Enter: commit focused item\n this.addTargetForControlEvent(UIView.controlEvent.EnterDown, () => {\n const highlightedItem = this._dropdownView.highlightedItem\n if (IS(highlightedItem)) {\n this.commitSelection(highlightedItem)\n }\n else if (this._isDropdownOpen) {\n this.closeDropdown()\n }\n })\n \n // Escape: dismiss dropdown\n this.addTargetForControlEvent(UIView.controlEvent.EscDown, () => {\n if (this._isDropdownOpen) {\n this.closeDropdown()\n if (this.strictSelection) {\n this.commitSelection(itemBeforeFocus as any)\n }\n else {\n this.text = textBeforeFocus\n }\n }\n })\n \n }\n \n \n /** Override in subclass to provide a custom dropdown view. */\n newDropdownView(): UIAutocompleteDropdownView<T> {\n return new UIAutocompleteDropdownView<T>(\n this.elementID ? this.elementID + \"Dropdown\" : undefined\n )\n }\n \n \n // MARK: - Selection\n \n get selectedItem(): T | undefined {\n return this._selectedItem?.value\n }\n \n \n get strictSelection(): boolean {\n return this._strictSelection\n }\n \n set strictSelection(strict: boolean) {\n this._strictSelection = strict\n this.updateValidationVisuals()\n }\n \n \n commitSelection(item: UIAutocompleteItem<T>) {\n \n this.blur()\n this._selectedItem = item\n this.text = item.label\n this.closeDropdown()\n this.updateValidationVisuals()\n this.sendControlEventForKey(UIAutocompleteTextField.controlEvent.SelectionDidChange)\n \n }\n \n \n // MARK: - Data\n \n /** Convenience: set string suggestions. Each string becomes { label: s, value: s }. */\n set autocompleteStrings(strings: string[]) {\n this._autocompleteItems = strings.map(s => ({\n label: s,\n value: s as unknown as T\n }))\n this.updateFilteredItems()\n }\n \n get autocompleteStrings(): string[] {\n return this._autocompleteItems.map(item => item.label)\n }\n \n set autocompleteData(items: UIAutocompleteItem<T>[]) {\n this._autocompleteItems = items\n this.updateFilteredItems()\n }\n \n get autocompleteData(): UIAutocompleteItem<T>[] {\n return this._autocompleteItems\n }\n \n \n // MARK: - Filtering\n \n updateFilteredItems() {\n \n const filterText = this.text.toLowerCase().trim()\n \n let filtered: UIAutocompleteItem<T>[]\n \n if (filterText.length === 0) {\n filtered = this._autocompleteItems\n }\n else {\n filtered = this._autocompleteItems.filter(item =>\n item.label.toLowerCase().includes(filterText)\n )\n }\n \n // If the only remaining result is an exact match for the current text,\n // the user has already made their selection \u2014 no need to show the dropdown.\n const isExactSingleMatch = filtered.length === 1 &&\n filtered[0].label.toLowerCase() === filterText\n \n this._dropdownView.filteredItems = isExactSingleMatch ? [] : filtered\n \n if (this._dropdownView.filteredItems.length > 0) {\n this._dropdownView.highlightedRowIndex = 0\n }\n \n if (this._isDropdownOpen) {\n this._dropdownView.showAnchoredToView(this)\n }\n \n }\n \n \n // MARK: - Dropdown Lifecycle\n \n openDropdown() {\n \n if (this._isDropdownOpen) {\n return\n }\n \n this._isDropdownOpen = YES\n this.updateFilteredItems()\n this._dropdownView.showAnchoredToView(this)\n \n }\n \n closeDropdown() {\n \n if (!this._isDropdownOpen) {\n return\n }\n \n this._isDropdownOpen = NO\n this._dropdownView.dismiss()\n \n // In strict mode, clear text if it doesn't match any item\n if (this._strictSelection && IS_NOT(this._selectedItem)) {\n const currentText = this.text.trim()\n if (currentText.length > 0) {\n const matchingItem = this._autocompleteItems.find(\n item => item.label === currentText\n )\n if (IS(matchingItem)) {\n this._selectedItem = matchingItem\n }\n else {\n this.text = \"\"\n this._selectedItem = undefined\n }\n }\n }\n \n this.updateValidationVisuals()\n \n }\n \n \n // MARK: - Validation\n \n /** Whether the current text is valid given the strictSelection setting. */\n get isValid(): boolean {\n \n if (!this._strictSelection) {\n return YES\n }\n \n const currentText = this.text.trim()\n \n if (currentText.length === 0) {\n return YES\n }\n \n return this._autocompleteItems.some(item => item.label === currentText)\n \n }\n \n \n /**\n * Hook for subclasses to apply custom visual styling based on validation state.\n * Called after dropdown closes and after selection changes.\n */\n updateValidationVisuals() {\n // Base implementation does nothing. Subclasses override.\n }\n \n \n // MARK: - Cleanup\n \n override wasRemovedFromViewTree() {\n \n super.wasRemovedFromViewTree()\n \n this._dropdownView.removeFromSuperview()\n \n }\n \n \n // MARK: - Layout\n \n override layoutSubviews() {\n \n super.layoutSubviews()\n \n if (this._isDropdownOpen) {\n this._dropdownView.showAnchoredToView(this)\n }\n \n }\n \n \n}\n\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAA2C;AAE3C,sBAAoC;AACpC,yBAA4B;AAC5B,oBAA0D;AAGnD,MAAM,2BAAN,cAAkD,+BAAY;AAAA,
|
|
4
|
+
"sourcesContent": ["import { UIAutocompleteDropdownView } from \"./UIAutocompleteDropdownView\"\nimport { UIAutocompleteItem } from \"./UIAutocompleteRowView\"\nimport { IS, IS_NOT, NO, YES } from \"./UIObject\"\nimport { UITextField } from \"./UITextField\"\nimport { UIView, UIViewAddControlEventTargetObject } from \"./UIView\"\n\n\nexport class UIAutocompleteTextField<T = string> extends UITextField {\n \n _autocompleteItems: UIAutocompleteItem<T>[] = []\n _selectedItem?: UIAutocompleteItem<T>\n _dropdownView: UIAutocompleteDropdownView<T>\n _isDropdownOpen: boolean = NO\n _strictSelection: boolean = NO\n _isValid: boolean = YES\n \n /**\n * When YES, the filter text is split on whitespace and all words must appear\n * in the item label (AND logic). When NO (default), the full filter string is\n * matched as a single substring.\n */\n usesMultiWordAndSearch: boolean = NO\n \n \n static override controlEvent = Object.assign({}, UITextField.controlEvent, {\n \"SelectionDidChange\": \"SelectionDidChange\"\n })\n \n override get controlEventTargetAccumulator(): UIViewAddControlEventTargetObject<typeof UIAutocompleteTextField> {\n return (super.controlEventTargetAccumulator as any)\n }\n \n \n constructor(elementID?: string) {\n \n super(elementID)\n \n this._dropdownView = this.newDropdownView()\n \n this._dropdownView.didSelectItem = (item) => {\n this.commitSelection(item)\n }\n \n let textBeforeFocus = this.text\n let itemBeforeFocus = this.selectedItem\n // Open dropdown on focus\n this.controlEventTargetAccumulator.Focus = () => {\n textBeforeFocus = this.text\n itemBeforeFocus = this.selectedItem\n this.text = \"\"\n this.openDropdown()\n this.textElementView.viewHTMLElement.select()\n const matchIndex = this._dropdownView.filteredItems.findIndex(\n item => item.label === textBeforeFocus\n )\n if (matchIndex !== -1) {\n this._dropdownView.highlightedRowIndex = matchIndex\n }\n }\n \n // Close on blur\n this.controlEventTargetAccumulator.Blur = () => {\n this.closeDropdown()\n }\n \n // Filter on text change\n this.addTargetForControlEvent(UITextField.controlEvent.TextChange, () => {\n this._selectedItem = undefined\n this.updateFilteredItems()\n if (!this._isDropdownOpen) {\n this.openDropdown()\n }\n })\n \n // Keyboard navigation: down arrow\n this.textElementView.addTargetForControlEvent(UIView.controlEvent.DownArrowDown, (sender, event) => {\n event.preventDefault()\n if (!this._isDropdownOpen) {\n this.openDropdown()\n return\n }\n const maxIndex = this._dropdownView.filteredItems.length - 1\n if (this._dropdownView.highlightedRowIndex < maxIndex) {\n this._dropdownView.highlightedRowIndex = this._dropdownView.highlightedRowIndex + 1\n }\n })\n \n // Keyboard navigation: up arrow\n this.textElementView.addTargetForControlEvent(UIView.controlEvent.UpArrowDown, (sender, event) => {\n event.preventDefault()\n if (this._dropdownView.highlightedRowIndex > 0) {\n this._dropdownView.highlightedRowIndex = this._dropdownView.highlightedRowIndex - 1\n }\n })\n \n // Enter: commit focused item\n this.addTargetForControlEvent(UIView.controlEvent.EnterDown, () => {\n const highlightedItem = this._dropdownView.highlightedItem\n if (IS(highlightedItem)) {\n this.commitSelection(highlightedItem)\n }\n else if (this._isDropdownOpen) {\n this.closeDropdown()\n }\n })\n \n // Escape: dismiss dropdown\n this.addTargetForControlEvent(UIView.controlEvent.EscDown, () => {\n if (this._isDropdownOpen) {\n this.closeDropdown()\n if (this.strictSelection) {\n this.commitSelection(itemBeforeFocus as any)\n }\n else {\n this.text = textBeforeFocus\n }\n }\n })\n \n }\n \n \n /** Override in subclass to provide a custom dropdown view. */\n newDropdownView(): UIAutocompleteDropdownView<T> {\n return new UIAutocompleteDropdownView<T>(\n this.elementID ? this.elementID + \"Dropdown\" : undefined\n )\n }\n \n \n // MARK: - Selection\n \n get selectedItem(): T | undefined {\n return this._selectedItem?.value\n }\n \n \n get strictSelection(): boolean {\n return this._strictSelection\n }\n \n set strictSelection(strict: boolean) {\n this._strictSelection = strict\n this.updateValidationVisuals()\n }\n \n \n commitSelection(item: UIAutocompleteItem<T>) {\n \n this.blur()\n this._selectedItem = item\n this.text = item.label\n this.closeDropdown()\n this.updateValidationVisuals()\n this.sendControlEventForKey(UIAutocompleteTextField.controlEvent.SelectionDidChange)\n \n }\n \n \n // MARK: - Data\n \n /** Convenience: set string suggestions. Each string becomes { label: s, value: s }. */\n set autocompleteStrings(strings: string[]) {\n this._autocompleteItems = strings.map(s => ({\n label: s,\n value: s as unknown as T\n }))\n this.updateFilteredItems()\n }\n \n get autocompleteStrings(): string[] {\n return this._autocompleteItems.map(item => item.label)\n }\n \n set autocompleteData(items: UIAutocompleteItem<T>[]) {\n this._autocompleteItems = items\n this.updateFilteredItems()\n }\n \n get autocompleteData(): UIAutocompleteItem<T>[] {\n return this._autocompleteItems\n }\n \n \n // MARK: - Filtering\n \n /**\n * Splits the given lowercase-trimmed filter text into individual words when\n * usesMultiWordAndSearch is YES, or returns it as a single-element array otherwise.\n * Returns an empty array when the input is empty.\n */\n _filterWordsFromText(filterText: string): string[] {\n if (filterText.length === 0) {\n return []\n }\n if (this.usesMultiWordAndSearch) {\n return filterText.split(/\\s+/).filter(word => word.length > 0)\n }\n return [filterText]\n }\n \n \n /**\n * Returns true when the given label (already lowercased) satisfies all filter\n * words \u2014 i.e. every word appears somewhere in the label.\n */\n _labelMatchesFilterWords(label: string, filterWords: string[]): boolean {\n return filterWords.every(word => label.includes(word))\n }\n \n \n /**\n * Returns true when the character immediately before `position` in `label`\n * is a word separator (or the position is at the start of the string).\n * Used to give a bonus to matches that start at a word boundary.\n */\n _isWordBoundary(label: string, position: number): boolean {\n if (position === 0) {\n return YES\n }\n const charBefore = label[position - 1]\n return \" -/\\\\|._,;:()[]\".includes(charBefore)\n }\n \n \n /**\n * Scores a label against the filter words. Lower score = better match.\n *\n * Scoring factors (in priority order):\n * 1. Non-sequential penalty \u2014 words must appear in typed order to avoid\n * a large penalty that pushes them below all sequential matches.\n * 2. Per-word boundary score \u2014 for each filter word, a mid-word match\n * scores worse than a word-boundary match. The sum across all words\n * determines the boundary tier.\n * 3. Position of the first matched word \u2014 within the same boundary tier,\n * earlier appearances rank higher.\n * 4. Total label length \u2014 shorter labels are more specific (tiebreaker).\n *\n * Example: query \"p\u00F5 pu\"\n * \"P\u00F5hjavee puhastusvahendid\" \u2192 \"p\u00F5\" at boundary(0), \"pu\" at boundary(9) \u2192 low boundary score\n * \"p\u00F5randapuhastusvahendid\" \u2192 \"p\u00F5\" at boundary(0), \"pu\" mid-word(7) \u2192 higher boundary score\n * \u2192 \"P\u00F5hjavee puhastusvahendid\" ranks first.\n */\n _scoreLabel(label: string, filterWords: string[]): number {\n \n if (filterWords.length === 0) {\n return label.length\n }\n \n // --- Sequential check ---\n // Scan left-to-right; if all words appear in order record the positions.\n let cursor = 0\n let isSequential = YES\n const sequentialPositions: number[] = []\n for (const word of filterWords) {\n const position = label.indexOf(word, cursor)\n if (position === -1) {\n isSequential = NO\n break\n }\n sequentialPositions.push(position)\n cursor = position + word.length\n }\n \n // --- Boundary score ---\n // For each filter word find its best (leftmost) match and check whether\n // it lands on a word boundary. Non-boundary matches incur a per-word\n // penalty of 1, so the boundary score is 0..filterWords.length.\n let boundaryScore = 0\n for (const word of filterWords) {\n const position = label.indexOf(word)\n if (position !== -1 && !this._isWordBoundary(label, position)) {\n boundaryScore += 1\n }\n }\n \n // Position of the first word's earliest match.\n const firstMatchPosition = label.indexOf(filterWords[0])\n \n // Compose score \u2014 each tier must not overflow into the next:\n // Non-sequential penalty : 10 000 000 (dominates everything)\n // Boundary score : 10 000 (per word, max ~10 words \u2192 100 000 max, safe)\n // First-match position : 100 (labels rarely exceed 200 chars)\n // Label length : 1 (tiebreaker)\n const sequentialPenalty = isSequential ? 0 : 10_000_000\n \n return sequentialPenalty +\n boundaryScore * 10_000 +\n firstMatchPosition * 100 +\n label.length\n \n }\n \n \n updateFilteredItems() {\n \n const rawFilterText = this.text.toLowerCase().trim()\n const filterWords = this._filterWordsFromText(rawFilterText)\n \n let filtered: UIAutocompleteItem<T>[]\n \n if (filterWords.length === 0) {\n filtered = this._autocompleteItems\n }\n else {\n filtered = this._autocompleteItems\n .filter(item => this._labelMatchesFilterWords(item.label.toLowerCase(), filterWords))\n .map((item, originalIndex) => ({ item, originalIndex, score: this._scoreLabel(item.label.toLowerCase(), filterWords) }))\n .sort((a, b) => a.score - b.score || a.originalIndex - b.originalIndex)\n .map(({ item }) => item)\n }\n \n // If the only remaining result is an exact match for the current text,\n // the user has already made their selection \u2014 no need to show the dropdown.\n const isExactSingleMatch = filtered.length === 1 &&\n filtered[0].label.toLowerCase() === rawFilterText\n \n this._dropdownView.filterWords = filterWords\n this._dropdownView.filteredItems = isExactSingleMatch ? [] : filtered\n \n if (this._dropdownView.filteredItems.length > 0) {\n this._dropdownView.highlightedRowIndex = 0\n }\n \n if (this._isDropdownOpen) {\n this._dropdownView.showAnchoredToView(this)\n }\n \n }\n \n \n // MARK: - Dropdown Lifecycle\n \n openDropdown() {\n \n if (this._isDropdownOpen) {\n return\n }\n \n this._isDropdownOpen = YES\n this._dropdownView.filterWords = []\n this.updateFilteredItems()\n this._dropdownView.showAnchoredToView(this)\n \n }\n \n closeDropdown() {\n \n if (!this._isDropdownOpen) {\n return\n }\n \n this._isDropdownOpen = NO\n this._dropdownView.dismiss()\n \n // In strict mode, clear text if it doesn't match any item\n if (this._strictSelection && IS_NOT(this._selectedItem)) {\n const currentText = this.text.trim()\n if (currentText.length > 0) {\n const matchingItem = this._autocompleteItems.find(\n item => item.label === currentText\n )\n if (IS(matchingItem)) {\n this._selectedItem = matchingItem\n }\n else {\n this.text = \"\"\n this._selectedItem = undefined\n }\n }\n }\n \n this.updateValidationVisuals()\n \n }\n \n \n // MARK: - Validation\n \n /** Whether the current text is valid given the strictSelection setting. */\n get isValid(): boolean {\n \n if (!this._strictSelection) {\n return YES\n }\n \n const currentText = this.text.trim()\n \n if (currentText.length === 0) {\n return YES\n }\n \n return this._autocompleteItems.some(item => item.label === currentText)\n \n }\n \n \n /**\n * Hook for subclasses to apply custom visual styling based on validation state.\n * Called after dropdown closes and after selection changes.\n */\n updateValidationVisuals() {\n // Base implementation does nothing. Subclasses override.\n }\n \n \n // MARK: - Cleanup\n \n override wasRemovedFromViewTree() {\n \n super.wasRemovedFromViewTree()\n \n this._dropdownView.removeFromSuperview()\n \n }\n \n \n // MARK: - Layout\n \n override layoutSubviews() {\n \n super.layoutSubviews()\n \n if (this._isDropdownOpen) {\n this._dropdownView.showAnchoredToView(this)\n }\n \n }\n \n \n}\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uicore-ts",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.218",
|
|
4
4
|
"description": "UICore is a library to build native-like user interfaces using pure Typescript. No HTML is needed at all. Components are described as TS classes and all user interactions are handled explicitly. This library is strongly inspired by the UIKit framework that is used in IOS. In addition, UICore has tools to handle URL based routing, array sorting and filtering and adds a number of other utilities for convenience.",
|
|
5
5
|
"main": "compiledScripts/index.js",
|
|
6
6
|
"types": "compiledScripts/index.d.ts",
|
|
@@ -14,6 +14,7 @@ export class UIAutocompleteDropdownView<T> extends UIView {
|
|
|
14
14
|
_fullHeightView: UIView
|
|
15
15
|
|
|
16
16
|
_filteredItems: UIAutocompleteItem<T>[] = []
|
|
17
|
+
_filterWords: string[] = []
|
|
17
18
|
_highlightedRowIndex: number = -1
|
|
18
19
|
_rowHeight: number = 36
|
|
19
20
|
_maxVisibleRows: number = 8
|
|
@@ -77,6 +78,8 @@ export class UIAutocompleteDropdownView<T> extends UIView {
|
|
|
77
78
|
row.item = item
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
row.filterWords = this._filterWords
|
|
82
|
+
|
|
80
83
|
// Reflect current keyboard highlight state via the native selected flag.
|
|
81
84
|
row.selected = (index === this._highlightedRowIndex)
|
|
82
85
|
|
|
@@ -194,6 +197,16 @@ export class UIAutocompleteDropdownView<T> extends UIView {
|
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
|
|
200
|
+
set filterWords(words: string[]) {
|
|
201
|
+
this._filterWords = words
|
|
202
|
+
this.tableView.reloadData()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get filterWords(): string[] {
|
|
206
|
+
return this._filterWords
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
197
210
|
/** Anchors this dropdown below the given field view inside the rootView. */
|
|
198
211
|
showAnchoredToView(anchorView: UIView) {
|
|
199
212
|
|
|
@@ -13,6 +13,7 @@ export interface UIAutocompleteItem<T> {
|
|
|
13
13
|
export class UIAutocompleteRowView<T> extends UIButton {
|
|
14
14
|
|
|
15
15
|
_item?: UIAutocompleteItem<T>
|
|
16
|
+
_filterWords: string[] = []
|
|
16
17
|
|
|
17
18
|
constructor(elementID?: string) {
|
|
18
19
|
|
|
@@ -42,7 +43,7 @@ export class UIAutocompleteRowView<T> extends UIButton {
|
|
|
42
43
|
|
|
43
44
|
set item(item: UIAutocompleteItem<T>) {
|
|
44
45
|
this._item = item
|
|
45
|
-
this.
|
|
46
|
+
this._updateLabelContent()
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
get item(): UIAutocompleteItem<T> | undefined {
|
|
@@ -50,5 +51,49 @@ export class UIAutocompleteRowView<T> extends UIButton {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
set filterWords(words: string[]) {
|
|
55
|
+
this._filterWords = words
|
|
56
|
+
this._updateLabelContent()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get filterWords(): string[] {
|
|
60
|
+
return this._filterWords
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_updateLabelContent() {
|
|
65
|
+
|
|
66
|
+
if (!this._item) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const label = this._item.label
|
|
71
|
+
|
|
72
|
+
if (this._filterWords.length === 0) {
|
|
73
|
+
this.titleLabel.text = label
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build a regex that matches any of the filter words (case-insensitive).
|
|
78
|
+
// Words are escaped so special regex characters in the label are treated literally.
|
|
79
|
+
const escapedWords = this._filterWords.map(
|
|
80
|
+
word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
81
|
+
)
|
|
82
|
+
const pattern = new RegExp(`(${escapedWords.join("|")})`, "gi")
|
|
83
|
+
|
|
84
|
+
// HTML-escape the label first, then re-insert <strong> tags around matches.
|
|
85
|
+
const escaped = label
|
|
86
|
+
.replace(/&/g, "&")
|
|
87
|
+
.replace(/</g, "<")
|
|
88
|
+
.replace(/>/g, ">")
|
|
89
|
+
.replace(/"/g, """)
|
|
90
|
+
|
|
91
|
+
const highlighted = escaped.replace(pattern, "<strong>$1</strong>")
|
|
92
|
+
|
|
93
|
+
this.titleLabel.innerHTML = highlighted
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
53
98
|
}
|
|
54
99
|
|
|
@@ -14,6 +14,13 @@ export class UIAutocompleteTextField<T = string> extends UITextField {
|
|
|
14
14
|
_strictSelection: boolean = NO
|
|
15
15
|
_isValid: boolean = YES
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* When YES, the filter text is split on whitespace and all words must appear
|
|
19
|
+
* in the item label (AND logic). When NO (default), the full filter string is
|
|
20
|
+
* matched as a single substring.
|
|
21
|
+
*/
|
|
22
|
+
usesMultiWordAndSearch: boolean = NO
|
|
23
|
+
|
|
17
24
|
|
|
18
25
|
static override controlEvent = Object.assign({}, UITextField.controlEvent, {
|
|
19
26
|
"SelectionDidChange": "SelectionDidChange"
|
|
@@ -177,26 +184,138 @@ export class UIAutocompleteTextField<T = string> extends UITextField {
|
|
|
177
184
|
|
|
178
185
|
// MARK: - Filtering
|
|
179
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Splits the given lowercase-trimmed filter text into individual words when
|
|
189
|
+
* usesMultiWordAndSearch is YES, or returns it as a single-element array otherwise.
|
|
190
|
+
* Returns an empty array when the input is empty.
|
|
191
|
+
*/
|
|
192
|
+
_filterWordsFromText(filterText: string): string[] {
|
|
193
|
+
if (filterText.length === 0) {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
if (this.usesMultiWordAndSearch) {
|
|
197
|
+
return filterText.split(/\s+/).filter(word => word.length > 0)
|
|
198
|
+
}
|
|
199
|
+
return [filterText]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns true when the given label (already lowercased) satisfies all filter
|
|
205
|
+
* words — i.e. every word appears somewhere in the label.
|
|
206
|
+
*/
|
|
207
|
+
_labelMatchesFilterWords(label: string, filterWords: string[]): boolean {
|
|
208
|
+
return filterWords.every(word => label.includes(word))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Returns true when the character immediately before `position` in `label`
|
|
214
|
+
* is a word separator (or the position is at the start of the string).
|
|
215
|
+
* Used to give a bonus to matches that start at a word boundary.
|
|
216
|
+
*/
|
|
217
|
+
_isWordBoundary(label: string, position: number): boolean {
|
|
218
|
+
if (position === 0) {
|
|
219
|
+
return YES
|
|
220
|
+
}
|
|
221
|
+
const charBefore = label[position - 1]
|
|
222
|
+
return " -/\\|._,;:()[]".includes(charBefore)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Scores a label against the filter words. Lower score = better match.
|
|
228
|
+
*
|
|
229
|
+
* Scoring factors (in priority order):
|
|
230
|
+
* 1. Non-sequential penalty — words must appear in typed order to avoid
|
|
231
|
+
* a large penalty that pushes them below all sequential matches.
|
|
232
|
+
* 2. Per-word boundary score — for each filter word, a mid-word match
|
|
233
|
+
* scores worse than a word-boundary match. The sum across all words
|
|
234
|
+
* determines the boundary tier.
|
|
235
|
+
* 3. Position of the first matched word — within the same boundary tier,
|
|
236
|
+
* earlier appearances rank higher.
|
|
237
|
+
* 4. Total label length — shorter labels are more specific (tiebreaker).
|
|
238
|
+
*
|
|
239
|
+
* Example: query "põ pu"
|
|
240
|
+
* "Põhjavee puhastusvahendid" → "põ" at boundary(0), "pu" at boundary(9) → low boundary score
|
|
241
|
+
* "põrandapuhastusvahendid" → "põ" at boundary(0), "pu" mid-word(7) → higher boundary score
|
|
242
|
+
* → "Põhjavee puhastusvahendid" ranks first.
|
|
243
|
+
*/
|
|
244
|
+
_scoreLabel(label: string, filterWords: string[]): number {
|
|
245
|
+
|
|
246
|
+
if (filterWords.length === 0) {
|
|
247
|
+
return label.length
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Sequential check ---
|
|
251
|
+
// Scan left-to-right; if all words appear in order record the positions.
|
|
252
|
+
let cursor = 0
|
|
253
|
+
let isSequential = YES
|
|
254
|
+
const sequentialPositions: number[] = []
|
|
255
|
+
for (const word of filterWords) {
|
|
256
|
+
const position = label.indexOf(word, cursor)
|
|
257
|
+
if (position === -1) {
|
|
258
|
+
isSequential = NO
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
sequentialPositions.push(position)
|
|
262
|
+
cursor = position + word.length
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Boundary score ---
|
|
266
|
+
// For each filter word find its best (leftmost) match and check whether
|
|
267
|
+
// it lands on a word boundary. Non-boundary matches incur a per-word
|
|
268
|
+
// penalty of 1, so the boundary score is 0..filterWords.length.
|
|
269
|
+
let boundaryScore = 0
|
|
270
|
+
for (const word of filterWords) {
|
|
271
|
+
const position = label.indexOf(word)
|
|
272
|
+
if (position !== -1 && !this._isWordBoundary(label, position)) {
|
|
273
|
+
boundaryScore += 1
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Position of the first word's earliest match.
|
|
278
|
+
const firstMatchPosition = label.indexOf(filterWords[0])
|
|
279
|
+
|
|
280
|
+
// Compose score — each tier must not overflow into the next:
|
|
281
|
+
// Non-sequential penalty : 10 000 000 (dominates everything)
|
|
282
|
+
// Boundary score : 10 000 (per word, max ~10 words → 100 000 max, safe)
|
|
283
|
+
// First-match position : 100 (labels rarely exceed 200 chars)
|
|
284
|
+
// Label length : 1 (tiebreaker)
|
|
285
|
+
const sequentialPenalty = isSequential ? 0 : 10_000_000
|
|
286
|
+
|
|
287
|
+
return sequentialPenalty +
|
|
288
|
+
boundaryScore * 10_000 +
|
|
289
|
+
firstMatchPosition * 100 +
|
|
290
|
+
label.length
|
|
291
|
+
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
180
295
|
updateFilteredItems() {
|
|
181
296
|
|
|
182
|
-
const
|
|
297
|
+
const rawFilterText = this.text.toLowerCase().trim()
|
|
298
|
+
const filterWords = this._filterWordsFromText(rawFilterText)
|
|
183
299
|
|
|
184
300
|
let filtered: UIAutocompleteItem<T>[]
|
|
185
301
|
|
|
186
|
-
if (
|
|
302
|
+
if (filterWords.length === 0) {
|
|
187
303
|
filtered = this._autocompleteItems
|
|
188
304
|
}
|
|
189
305
|
else {
|
|
190
|
-
filtered = this._autocompleteItems
|
|
191
|
-
item.label.toLowerCase()
|
|
192
|
-
|
|
306
|
+
filtered = this._autocompleteItems
|
|
307
|
+
.filter(item => this._labelMatchesFilterWords(item.label.toLowerCase(), filterWords))
|
|
308
|
+
.map((item, originalIndex) => ({ item, originalIndex, score: this._scoreLabel(item.label.toLowerCase(), filterWords) }))
|
|
309
|
+
.sort((a, b) => a.score - b.score || a.originalIndex - b.originalIndex)
|
|
310
|
+
.map(({ item }) => item)
|
|
193
311
|
}
|
|
194
312
|
|
|
195
313
|
// If the only remaining result is an exact match for the current text,
|
|
196
314
|
// the user has already made their selection — no need to show the dropdown.
|
|
197
315
|
const isExactSingleMatch = filtered.length === 1 &&
|
|
198
|
-
filtered[0].label.toLowerCase() ===
|
|
316
|
+
filtered[0].label.toLowerCase() === rawFilterText
|
|
199
317
|
|
|
318
|
+
this._dropdownView.filterWords = filterWords
|
|
200
319
|
this._dropdownView.filteredItems = isExactSingleMatch ? [] : filtered
|
|
201
320
|
|
|
202
321
|
if (this._dropdownView.filteredItems.length > 0) {
|
|
@@ -219,6 +338,7 @@ export class UIAutocompleteTextField<T = string> extends UITextField {
|
|
|
219
338
|
}
|
|
220
339
|
|
|
221
340
|
this._isDropdownOpen = YES
|
|
341
|
+
this._dropdownView.filterWords = []
|
|
222
342
|
this.updateFilteredItems()
|
|
223
343
|
this._dropdownView.showAnchoredToView(this)
|
|
224
344
|
|