uicore-ts 1.1.216 → 1.1.221

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.
@@ -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": "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",
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.titleLabel.text = item.label;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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.titleLabel.text = item.label\n }\n \n get item(): UIAutocompleteItem<T> | undefined {\n return this._item\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,EAInD,YAAY,WAAoB;AAE5B,UAAM,SAAS;AAEf,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,WAAW,OAAO,KAAK;AAAA,EAChC;AAAA,EAEA,IAAI,OAA0C;AAC1C,WAAO,KAAK;AAAA,EAChB;AAGJ;",
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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\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 filterText = this.text.toLowerCase().trim();
192
+ const rawFilterText = this.text.toLowerCase().trim();
193
+ const filterWords = this._filterWordsFromText(rawFilterText);
146
194
  let filtered;
147
- if (filterText.length === 0) {
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() === filterText;
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,EAmBjE,YAAY,WAAoB;AAE5B,UAAM,SAAS;AAnBnB,8BAA8C,CAAC;AAG/C,2BAA2B;AAC3B,4BAA4B;AAC5B,oBAAoB;AAgBhB,SAAK,gBAAgB,KAAK,gBAAgB;AAE1C,SAAK,cAAc,gBAAgB,CAAC,SAAS;AACzC,WAAK,gBAAgB,IAAI;AAAA,IAC7B;AAEA,QAAI,kBAAkB,KAAK;AAC3B,QAAI,kBAAkB,KAAK;AAE3B,SAAK,8BAA8B,QAAQ,MAAM;AAC7C,wBAAkB,KAAK;AACvB,wBAAkB,KAAK;AACvB,WAAK,OAAO;AACZ,WAAK,aAAa;AAClB,WAAK,gBAAgB,gBAAgB,OAAO;AAC5C,YAAM,aAAa,KAAK,cAAc,cAAc;AAAA,QAChD,UAAQ,KAAK,UAAU;AAAA,MAC3B;AACA,UAAI,eAAe,IAAI;AACnB,aAAK,cAAc,sBAAsB;AAAA,MAC7C;AAAA,IACJ;AAGA,SAAK,8BAA8B,OAAO,MAAM;AAC5C,WAAK,cAAc;AAAA,IACvB;AAGA,SAAK,yBAAyB,+BAAY,aAAa,YAAY,MAAM;AACrE,WAAK,gBAAgB;AACrB,WAAK,oBAAoB;AACzB,UAAI,CAAC,KAAK,iBAAiB;AACvB,aAAK,aAAa;AAAA,MACtB;AAAA,IACJ,CAAC;AAGD,SAAK,gBAAgB,yBAAyB,qBAAO,aAAa,eAAe,CAAC,QAAQ,UAAU;AAChG,YAAM,eAAe;AACrB,UAAI,CAAC,KAAK,iBAAiB;AACvB,aAAK,aAAa;AAClB;AAAA,MACJ;AACA,YAAM,WAAW,KAAK,cAAc,cAAc,SAAS;AAC3D,UAAI,KAAK,cAAc,sBAAsB,UAAU;AACnD,aAAK,cAAc,sBAAsB,KAAK,cAAc,sBAAsB;AAAA,MACtF;AAAA,IACJ,CAAC;AAGD,SAAK,gBAAgB,yBAAyB,qBAAO,aAAa,aAAa,CAAC,QAAQ,UAAU;AAC9F,YAAM,eAAe;AACrB,UAAI,KAAK,cAAc,sBAAsB,GAAG;AAC5C,aAAK,cAAc,sBAAsB,KAAK,cAAc,sBAAsB;AAAA,MACtF;AAAA,IACJ,CAAC;AAGD,SAAK,yBAAyB,qBAAO,aAAa,WAAW,MAAM;AAC/D,YAAM,kBAAkB,KAAK,cAAc;AAC3C,cAAI,oBAAG,eAAe,GAAG;AACrB,aAAK,gBAAgB,eAAe;AAAA,MACxC,WACS,KAAK,iBAAiB;AAC3B,aAAK,cAAc;AAAA,MACvB;AAAA,IACJ,CAAC;AAGD,SAAK,yBAAyB,qBAAO,aAAa,SAAS,MAAM;AAC7D,UAAI,KAAK,iBAAiB;AACtB,aAAK,cAAc;AACnB,YAAI,KAAK,iBAAiB;AACtB,eAAK,gBAAgB,eAAsB;AAAA,QAC/C,OACK;AACD,eAAK,OAAO;AAAA,QAChB;AAAA,MACJ;AAAA,IACJ,CAAC;AAAA,EAEL;AAAA,EA3FA,IAAa,gCAAmG;AAC5G,WAAQ,MAAM;AAAA,EAClB;AAAA,EA6FA,kBAAiD;AAC7C,WAAO,IAAI;AAAA,MACP,KAAK,YAAY,KAAK,YAAY,aAAa;AAAA,IACnD;AAAA,EACJ;AAAA,EAKA,IAAI,eAA8B;AA7HtC;AA8HQ,YAAO,UAAK,kBAAL,mBAAoB;AAAA,EAC/B;AAAA,EAGA,IAAI,kBAA2B;AAC3B,WAAO,KAAK;AAAA,EAChB;AAAA,EAEA,IAAI,gBAAgB,QAAiB;AACjC,SAAK,mBAAmB;AACxB,SAAK,wBAAwB;AAAA,EACjC;AAAA,EAGA,gBAAgB,MAA6B;AAEzC,SAAK,KAAK;AACV,SAAK,gBAAgB;AACrB,SAAK,OAAO,KAAK;AACjB,SAAK,cAAc;AACnB,SAAK,wBAAwB;AAC7B,SAAK,uBAAuB,yBAAwB,aAAa,kBAAkB;AAAA,EAEvF;AAAA,EAMA,IAAI,oBAAoB,SAAmB;AACvC,SAAK,qBAAqB,QAAQ,IAAI,QAAM;AAAA,MACxC,OAAO;AAAA,MACP,OAAO;AAAA,IACX,EAAE;AACF,SAAK,oBAAoB;AAAA,EAC7B;AAAA,EAEA,IAAI,sBAAgC;AAChC,WAAO,KAAK,mBAAmB,IAAI,UAAQ,KAAK,KAAK;AAAA,EACzD;AAAA,EAEA,IAAI,iBAAiB,OAAgC;AACjD,SAAK,qBAAqB;AAC1B,SAAK,oBAAoB;AAAA,EAC7B;AAAA,EAEA,IAAI,mBAA4C;AAC5C,WAAO,KAAK;AAAA,EAChB;AAAA,EAKA,sBAAsB;AAElB,UAAM,aAAa,KAAK,KAAK,YAAY,EAAE,KAAK;AAEhD,QAAI;AAEJ,QAAI,WAAW,WAAW,GAAG;AACzB,iBAAW,KAAK;AAAA,IACpB,OACK;AACD,iBAAW,KAAK,mBAAmB;AAAA,QAAO,UACtC,KAAK,MAAM,YAAY,EAAE,SAAS,UAAU;AAAA,MAChD;AAAA,IACJ;AAIA,UAAM,qBAAqB,SAAS,WAAW,KAC3C,SAAS,GAAG,MAAM,YAAY,MAAM;AAExC,SAAK,cAAc,gBAAgB,qBAAqB,CAAC,IAAI;AAE7D,QAAI,KAAK,cAAc,cAAc,SAAS,GAAG;AAC7C,WAAK,cAAc,sBAAsB;AAAA,IAC7C;AAEA,QAAI,KAAK,iBAAiB;AACtB,WAAK,cAAc,mBAAmB,IAAI;AAAA,IAC9C;AAAA,EAEJ;AAAA,EAKA,eAAe;AAEX,QAAI,KAAK,iBAAiB;AACtB;AAAA,IACJ;AAEA,SAAK,kBAAkB;AACvB,SAAK,oBAAoB;AACzB,SAAK,cAAc,mBAAmB,IAAI;AAAA,EAE9C;AAAA,EAEA,gBAAgB;AAEZ,QAAI,CAAC,KAAK,iBAAiB;AACvB;AAAA,IACJ;AAEA,SAAK,kBAAkB;AACvB,SAAK,cAAc,QAAQ;AAG3B,QAAI,KAAK,wBAAoB,wBAAO,KAAK,aAAa,GAAG;AACrD,YAAM,cAAc,KAAK,KAAK,KAAK;AACnC,UAAI,YAAY,SAAS,GAAG;AACxB,cAAM,eAAe,KAAK,mBAAmB;AAAA,UACzC,UAAQ,KAAK,UAAU;AAAA,QAC3B;AACA,gBAAI,oBAAG,YAAY,GAAG;AAClB,eAAK,gBAAgB;AAAA,QACzB,OACK;AACD,eAAK,OAAO;AACZ,eAAK,gBAAgB;AAAA,QACzB;AAAA,MACJ;AAAA,IACJ;AAEA,SAAK,wBAAwB;AAAA,EAEjC;AAAA,EAMA,IAAI,UAAmB;AAEnB,QAAI,CAAC,KAAK,kBAAkB;AACxB,aAAO;AAAA,IACX;AAEA,UAAM,cAAc,KAAK,KAAK,KAAK;AAEnC,QAAI,YAAY,WAAW,GAAG;AAC1B,aAAO;AAAA,IACX;AAEA,WAAO,KAAK,mBAAmB,KAAK,UAAQ,KAAK,UAAU,WAAW;AAAA,EAE1E;AAAA,EAOA,0BAA0B;AAAA,EAE1B;AAAA,EAKS,yBAAyB;AAE9B,UAAM,uBAAuB;AAE7B,SAAK,cAAc,oBAAoB;AAAA,EAE3C;AAAA,EAKS,iBAAiB;AAEtB,UAAM,eAAe;AAErB,QAAI,KAAK,iBAAiB;AACtB,WAAK,cAAc,mBAAmB,IAAI;AAAA,IAC9C;AAAA,EAEJ;AAGJ;AA/SO,IAAM,0BAAN;AAAM,wBAUO,eAAe,OAAO,OAAO,CAAC,GAAG,+BAAY,cAAc;AAAA,EACvE,sBAAsB;AAC1B,CAAC;",
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
  }
@@ -76,6 +76,12 @@ export declare class UITextView extends UIView {
76
76
  notificationAmountDidChange(notificationAmount: number): void;
77
77
  get text(): string;
78
78
  set text(text: string);
79
+ /**
80
+ * Formats a raw number string by inserting `separator` every three digits
81
+ * in the integer part. Handles negative numbers and decimals (machine locale
82
+ * "." as decimal point). Non-numeric strings are returned unchanged.
83
+ */
84
+ static applyThousandsSeparatorToNumericalString(value: string, separator: string): string;
79
85
  set innerHTML(innerHTML: string);
80
86
  get innerHTML(): string;
81
87
  setText(key: string, defaultString: string, parameters?: {
@@ -99,6 +105,9 @@ export declare class UITextView extends UIView {
99
105
  textPrefix: string;
100
106
  textSuffix: string;
101
107
  _notificationAmount: number;
108
+ _thousandsSeparator: string | null;
109
+ get thousandsSeparator(): string | null;
110
+ set thousandsSeparator(value: string | null);
102
111
  _textColor: UIColor;
103
112
  _textAlignment?: ValueOf<typeof UITextView.textAlignment>;
104
113
  _isSingleLine: boolean;
@@ -34,6 +34,7 @@ const _UITextView = class extends import_UIView.UIView {
34
34
  this.textPrefix = "";
35
35
  this.textSuffix = "";
36
36
  this._notificationAmount = 0;
37
+ this._thousandsSeparator = null;
37
38
  this._textColor = _UITextView.defaultTextColor;
38
39
  this._isSingleLine = import_UIObject.YES;
39
40
  this._automaticFontSizeSelection = import_UIObject.NO;
@@ -268,9 +269,10 @@ const _UITextView = class extends import_UIView.UIView {
268
269
  if (this.notificationAmount) {
269
270
  notificationText = '<span style="color: ' + _UITextView.notificationTextColor.stringValue + ';">' + (" (" + this.notificationAmount + ")").bold() + "</span>";
270
271
  }
271
- if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {
272
+ const displayText = this.thousandsSeparator !== null ? _UITextView.applyThousandsSeparatorToNumericalString(text, this.thousandsSeparator) : text;
273
+ if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + displayText + this.textSuffix + notificationText) {
272
274
  this.textElementView.viewHTMLElement.innerHTML = this.textPrefix + (0, import_UIObject.FIRST)(
273
- text,
275
+ displayText,
274
276
  ""
275
277
  ) + this.textSuffix + notificationText;
276
278
  }
@@ -285,6 +287,30 @@ const _UITextView = class extends import_UIView.UIView {
285
287
  this.clearIntrinsicSizeCache();
286
288
  this.setNeedsLayout();
287
289
  }
290
+ static applyThousandsSeparatorToNumericalString(value, separator) {
291
+ const trimmed = (value || "").trim();
292
+ if (trimmed === "") {
293
+ return value;
294
+ }
295
+ const parts = trimmed.split(".");
296
+ const integerPart = parts[0];
297
+ const decimalPart = parts.length > 1 ? parts[1] : null;
298
+ if (!/^-?\d+$/.test(integerPart)) {
299
+ return value;
300
+ }
301
+ const isNegative = integerPart.startsWith("-");
302
+ const digits = isNegative ? integerPart.slice(1) : integerPart;
303
+ let formatted = "";
304
+ const offset = digits.length % 3;
305
+ for (let index = 0; index < digits.length; index++) {
306
+ if (index > 0 && (index - offset) % 3 === 0) {
307
+ formatted += separator;
308
+ }
309
+ formatted += digits[index];
310
+ }
311
+ const result = (isNegative ? "-" : "") + formatted;
312
+ return decimalPart !== null ? result + "." + decimalPart : result;
313
+ }
288
314
  set innerHTML(innerHTML) {
289
315
  this.text = innerHTML;
290
316
  this.invalidateMeasurementStrategy();
@@ -371,6 +397,12 @@ const _UITextView = class extends import_UIView.UIView {
371
397
  }
372
398
  return maxFittingFontSize;
373
399
  }
400
+ get thousandsSeparator() {
401
+ return this._thousandsSeparator;
402
+ }
403
+ set thousandsSeparator(value) {
404
+ this._thousandsSeparator = value;
405
+ }
374
406
  addStyleClass(styleClass) {
375
407
  super.addStyleClass(styleClass);
376
408
  this._invalidateFontCache();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../scripts/UITextView.ts"],
4
- "sourcesContent": ["import { UIColor } from \"./UIColor\"\nimport { UILocalizedTextObject } from \"./UIInterfaces\"\nimport { EXTEND, FIRST, IS_LIKE_NULL, nil, NO, UIObject, ValueOf, YES } from \"./UIObject\"\nimport { UIRectangle } from \"./UIRectangle\"\nimport { TextMeasurementStyle, UITextMeasurement } from \"./UITextMeasurement\"\nimport { UIView, UIViewBroadcastEvent } from \"./UIView\"\n\n\nexport class UITextView extends UIView {\n \n //#region Static Properties\n \n static defaultTextColor = UIColor.blackColor\n static notificationTextColor = UIColor.redColor\n \n // Global caches for all UILabels\n static _intrinsicHeightCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n static _intrinsicWidthCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n \n static _ptToPx: number\n static _pxToPt: number\n \n static type = {\n \"paragraph\": \"p\",\n \"header1\": \"h1\",\n \"header2\": \"h2\",\n \"header3\": \"h3\",\n \"header4\": \"h4\",\n \"header5\": \"h5\",\n \"header6\": \"h6\",\n \"textArea\": \"textarea\",\n \"textField\": \"input\",\n \"span\": \"span\",\n \"label\": \"label\"\n } as const\n \n static textAlignment = {\n \"left\": \"left\",\n \"center\": \"center\",\n \"right\": \"right\",\n \"justify\": \"justify\"\n } as const\n \n //#endregion\n \n //#region Constructor\n \n \n constructor(\n elementID?: string,\n textViewType: string | ValueOf<typeof UITextView.type> = UITextView.type.paragraph,\n viewHTMLElement = null\n ) {\n \n // Create inner text element as a UIView\n const innerElementID = elementID ? `${elementID}_textElement` : undefined\n const _textElementView = new UIView(innerElementID, null, textViewType)\n \n // Create outer container (wrapper) - this is the main viewHTMLElement\n super(elementID, viewHTMLElement, \"span\", { _textElementView })\n \n // Configure outer container for vertical centering using direct property access\n \n this.configureWithObject({\n // @ts-ignore\n viewHTMLElement: {\n style: {\n display: \"flex\",\n alignItems: \"center\", // Vertical centering\n overflow: \"hidden\"\n }\n }\n })\n \n this.text = \"\"\n \n this._textElementView = _textElementView\n \n // Configure inner text element for ellipsis and positioning\n this._textElementView.configureWithObject({\n style: {\n position: \"relative\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n margin: \"0\",\n padding: \"0\"\n },\n // Forward control events from text element to the container\n sendControlEventForKey: EXTEND(this.sendControlEventForKey.bind(this))\n })\n \n // Add text element as a subview\n this.addSubview(this._textElementView)\n \n \n this.isSingleLine = YES\n \n this.textColor = this.textColor\n \n this.userInteractionEnabled = YES\n \n if (textViewType == UITextView.type.textArea) {\n this.pausesPointerEvents = YES\n this.addTargetForControlEvent(\n UIView.controlEvent.PointerUpInside,\n (sender, event) => sender.focus()\n )\n }\n }\n \n //#endregion\n \n //#region Text Element View Property\n \n private _textElementView: UIView\n \n /**\n * The inner text element that holds the actual text content\n */\n get textElementView(): UIView {\n return this._textElementView\n }\n \n /**\n * Override style to apply to the text element instead of the container\n */\n // override get style() {\n // return this._textElementView.style\n // }\n //\n // /**\n // * Override computedStyle to get computed styles from the text element\n // */\n // override get computedStyle() {\n // return this._textElementView.computedStyle\n // }\n \n /**\n * Access the outer container's style (for positioning, layout, etc.)\n */\n get containerStyle() {\n return this.viewHTMLElement.style\n }\n \n /**\n * Override styleClasses to apply to the text element\n */\n override get styleClasses() {\n return this._textElementView.styleClasses\n }\n \n override set styleClasses(styleClasses: string[]) {\n this._textElementView.styleClasses = styleClasses\n }\n \n //#endregion\n \n //#region Lifecycle Methods\n \n override didReceiveBroadcastEvent(event: UIViewBroadcastEvent) {\n super.didReceiveBroadcastEvent(event)\n }\n \n override willMoveToSuperview(superview: UIView) {\n super.willMoveToSuperview(superview)\n }\n \n override documentFontsDidLoad() {\n super.documentFontsDidLoad()\n this._invalidateFontCache()\n this.invalidateMeasurementStrategy()\n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n UITextView._intrinsicHeightCache = new UIObject() as any\n UITextView._intrinsicWidthCache = new UIObject() as any\n }\n \n override layoutSubviews() {\n super.layoutSubviews()\n \n if (this._automaticFontSizeSelection) {\n this.fontSize = UITextView.automaticallyCalculatedFontSize(\n new UIRectangle(\n 0,\n 0,\n this.textElementView.viewHTMLElement.offsetHeight,\n this.textElementView.viewHTMLElement.offsetWidth\n ),\n this.intrinsicContentSize(),\n this.fontSize,\n this._minFontSize,\n this._maxFontSize\n )\n }\n }\n \n //#endregion\n \n //#region Measurement & Sizing - Private Methods\n \n private _invalidateMeasurementStyles(): void {\n this._cachedMeasurementStyles = undefined\n UITextMeasurement.invalidateElement(this.textElementView.viewHTMLElement)\n this._intrinsicSizesCache = {}\n }\n \n private _getMeasurementStyles(): TextMeasurementStyle | null {\n if (this._cachedMeasurementStyles) {\n return this._cachedMeasurementStyles\n }\n \n // Ensure element is in document\n if (!this.textElementView.viewHTMLElement.isConnected) {\n return null\n }\n \n // Force a layout flush ONCE to ensure computed styles are available\n // This is only paid once per style change, then we use cached values\n this.textElementView.viewHTMLElement.offsetHeight\n \n const computed = window.getComputedStyle(this.textElementView.viewHTMLElement)\n const fontSizeStr = computed.fontSize\n const fontSize = parseFloat(fontSizeStr)\n \n console.log(computed.letterSpacing)\n \n if (!fontSize || isNaN(fontSize)) {\n return null\n }\n \n const lineHeight = this._parseLineHeight(computed.lineHeight, fontSize)\n \n if (isNaN(lineHeight)) {\n return null\n }\n \n const font = [\n computed.fontStyle || \"normal\",\n computed.fontVariant || \"normal\",\n computed.fontWeight || \"normal\",\n fontSize + \"px\",\n computed.fontFamily || \"sans-serif\"\n ].join(\" \")\n \n this._cachedMeasurementStyles = {\n font: font,\n fontSize: fontSize,\n lineHeight: lineHeight,\n whiteSpace: computed.whiteSpace || \"normal\",\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0,\n letterSpacing: parseFloat(computed.letterSpacing) || 0,\n textTransform: computed.textTransform || \"none\"\n }\n \n return this._cachedMeasurementStyles\n }\n \n private _parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === \"normal\") {\n return fontSize * 1.2\n }\n if (lineHeight.endsWith(\"px\")) {\n return parseFloat(lineHeight)\n }\n const numericLineHeight = parseFloat(lineHeight)\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight\n }\n return fontSize * 1.2\n }\n \n private _shouldUseFastMeasurement(): boolean {\n const content = this.text || this.textElementView.innerHTML\n \n if (this._innerHTMLKey || this._localizedTextObject) {\n return false\n }\n \n if (this.notificationAmount > 0) {\n return false\n }\n \n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content)\n \n if (hasComplexHTML) {\n return false\n }\n \n // Canvas measureText silently falls back to the system font when the\n // custom font hasn't been loaded into the canvas font system yet, even\n // if getComputedStyle already reports the correct font family. Guard\n // against this by checking the font is confirmed available before\n // trusting canvas-based measurement.\n const styles = this._getMeasurementStyles()\n if (styles && !document.fonts.check(styles.font)) {\n return false\n }\n \n return true\n }\n \n //#endregion\n \n //#region Measurement & Sizing - Public Methods\n \n setUseFastMeasurement(useFast: boolean): void {\n this._useFastMeasurement = useFast\n this._intrinsicSizesCache = {}\n }\n \n invalidateMeasurementStrategy(): void {\n this._useFastMeasurement = undefined\n this._invalidateMeasurementStyles()\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Alignment\n \n get textAlignment() {\n return this._textElementView.style.textAlign as ValueOf<typeof UITextView.textAlignment>\n }\n \n set textAlignment(textAlignment: ValueOf<typeof UITextView.textAlignment>) {\n this._textAlignment = textAlignment\n this._textElementView.style.textAlign = textAlignment\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Color\n \n get textColor() {\n return this._textColor\n }\n \n set textColor(color: UIColor) {\n this._textColor = color || UITextView.defaultTextColor\n this._textElementView.style.color = this._textColor.stringValue\n }\n \n //#endregion\n \n //#region Getters & Setters - Single Line\n \n get isSingleLine() {\n return this._isSingleLine\n }\n \n set isSingleLine(isSingleLine: boolean) {\n this._isSingleLine = isSingleLine\n \n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n \n if (isSingleLine) {\n // Single line: use nowrap with ellipsis\n this._textElementView.style.whiteSpace = \"nowrap\"\n this._textElementView.style.textOverflow = \"ellipsis\"\n this._textElementView.style.display = \"\"\n this._textElementView.style.webkitLineClamp = \"\"\n this._textElementView.style.webkitBoxOrient = \"\"\n return\n }\n \n // Multiline: allow wrapping, but still show ellipsis if content overflows the container\n // This uses the -webkit-line-clamp approach which works for multiline ellipsis\n this._textElementView.style.whiteSpace = \"normal\"\n this._textElementView.style.textOverflow = \"ellipsis\"\n this._textElementView.style.display = \"-webkit-box\"\n this._textElementView.style.webkitBoxOrient = \"vertical\"\n // Don't set line-clamp to a specific number - let it fill available space\n // The overflow: hidden from the constructor will clip content that exceeds the height\n this.invalidateMeasurementStrategy()\n }\n \n //#endregion\n \n //#region Getters & Setters - Notification Amount\n \n get notificationAmount() {\n return this._notificationAmount\n }\n \n set notificationAmount(notificationAmount: number) {\n if (this._notificationAmount == notificationAmount) {\n return\n }\n \n this._notificationAmount = notificationAmount\n this.text = this.text\n this.setNeedsLayoutUpToRootView()\n this.notificationAmountDidChange(notificationAmount)\n }\n \n notificationAmountDidChange(notificationAmount: number) {\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Content\n \n get text() {\n return (this._text || this.textElementView.viewHTMLElement.innerHTML)\n }\n \n set text(text) {\n this._text = text\n var notificationText = \"\"\n if (this.notificationAmount) {\n notificationText = \"<span style=\\\"color: \" + UITextView.notificationTextColor.stringValue + \";\\\">\" +\n (\" (\" + this.notificationAmount + \")\").bold() + \"</span>\"\n }\n \n if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {\n this.textElementView.viewHTMLElement.innerHTML = this.textPrefix + FIRST(\n text, \"\") + this.textSuffix + notificationText\n }\n \n if (this.changesOften) {\n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n }\n \n this._useFastMeasurement = undefined\n this._intrinsicSizesCache = {}\n this.invalidateMeasurementStrategy()\n this._invalidateMeasurementStyles()\n this.clearIntrinsicSizeCache()\n \n this.setNeedsLayout()\n }\n \n override set innerHTML(innerHTML: string) {\n this.text = innerHTML\n this.invalidateMeasurementStrategy()\n }\n \n override get innerHTML() {\n return this.viewHTMLElement.innerHTML\n }\n \n setText(key: string, defaultString: string, parameters?: { [x: string]: string | UILocalizedTextObject }) {\n this.textElementView.setInnerHTML(key, defaultString, parameters)\n this.invalidateMeasurementStrategy()\n }\n \n //#endregion\n \n //#region Getters & Setters - Font Size\n \n get fontSize() {\n const style = this._textElementView.style.fontSize || window.getComputedStyle(this._textElementView.viewHTMLElement, null).fontSize\n const result = (parseFloat(style) * UITextView._pxToPt)\n return result\n }\n \n set fontSize(fontSize: number) {\n if (fontSize != this.fontSize) {\n this._textElementView.style.fontSize = \"\" + fontSize + \"pt\"\n \n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n \n this._invalidateFontCache()\n this._invalidateMeasurementStyles()\n this.clearIntrinsicSizeCache()\n }\n }\n \n useAutomaticFontSize(minFontSize: number = nil, maxFontSize: number = nil) {\n this._automaticFontSizeSelection = YES\n this._minFontSize = minFontSize\n this._maxFontSize = maxFontSize\n this.setNeedsLayout()\n }\n \n //#endregion\n \n //#region Font Caching - Private Methods\n \n /**\n * Get a stable cache key for the font without triggering reflow.\n * Only computes font on first access or when font properties change.\n */\n private _getFontCacheKey(): string {\n // Check if font-related properties have changed\n const currentTriggers = {\n fontSize: this._textElementView.style.fontSize || \"\",\n fontFamily: this._textElementView.style.fontFamily || \"\",\n fontWeight: this._textElementView.style.fontWeight || \"\",\n fontStyle: this._textElementView.style.fontStyle || \"\",\n styleClasses: this.styleClasses.join(\",\")\n }\n \n const hasChanged =\n currentTriggers.fontSize !== this._fontInvalidationTriggers.fontSize ||\n currentTriggers.fontFamily !== this._fontInvalidationTriggers.fontFamily ||\n currentTriggers.fontWeight !== this._fontInvalidationTriggers.fontWeight ||\n currentTriggers.fontStyle !== this._fontInvalidationTriggers.fontStyle ||\n currentTriggers.styleClasses !== this._fontInvalidationTriggers.styleClasses\n \n if (!this._cachedFontKey || hasChanged) {\n // Only access computedStyle when we know something changed\n const computed = this._textElementView.computedStyle\n this._cachedFontKey = [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(\"_\").replace(/[.\\s]/g, \"_\")\n \n this._fontInvalidationTriggers = currentTriggers\n }\n \n return this._cachedFontKey\n }\n \n /**\n * Invalidate font cache when font properties change\n */\n private _invalidateFontCache(): void {\n this._cachedFontKey = undefined\n }\n \n //#endregion\n \n //#region Static Methods\n \n static _determinePXAndPTRatios() {\n if (UITextView._ptToPx) {\n return\n }\n \n const o = document.createElement(\"div\")\n o.style.width = \"1000pt\"\n document.body.appendChild(o)\n UITextView._ptToPx = o.clientWidth / 1000\n document.body.removeChild(o)\n UITextView._pxToPt = 1 / UITextView._ptToPx\n }\n \n static automaticallyCalculatedFontSize(\n bounds: UIRectangle,\n currentSize: UIRectangle,\n currentFontSize: number,\n minFontSize?: number,\n maxFontSize?: number\n ) {\n minFontSize = FIRST(minFontSize, 1)\n maxFontSize = FIRST(maxFontSize, 100000000000)\n \n const heightMultiplier = bounds.height / (currentSize.height + 1)\n const widthMultiplier = bounds.width / (currentSize.width + 1)\n \n var multiplier = heightMultiplier\n if (heightMultiplier > widthMultiplier) {\n multiplier = widthMultiplier\n }\n \n const maxFittingFontSize = currentFontSize * multiplier\n \n if (maxFittingFontSize > maxFontSize) {\n return maxFontSize\n }\n \n if (minFontSize > maxFittingFontSize) {\n return minFontSize\n }\n \n return maxFittingFontSize\n }\n \n //#endregion\n \n //#region Instance Properties - Text Content\n \n _text?: string\n textPrefix = \"\"\n textSuffix = \"\"\n _notificationAmount = 0\n \n //#endregion\n \n //#region Instance Properties - Styling\n \n _textColor: UIColor = UITextView.defaultTextColor\n _textAlignment?: ValueOf<typeof UITextView.textAlignment>\n _isSingleLine = YES\n \n //#endregion\n \n //#region Instance Properties - Font & Sizing\n \n _minFontSize?: number\n _maxFontSize?: number\n _automaticFontSizeSelection = NO\n \n // Cache for the computed font string\n private _cachedFontKey?: string\n private _fontInvalidationTriggers = {\n fontSize: \"\",\n fontFamily: \"\",\n fontWeight: \"\",\n fontStyle: \"\",\n styleClasses: \"\"\n }\n \n //#endregion\n \n //#region Instance Properties - Caching & Performance\n \n changesOften = NO\n \n // Local cache for this instance if the label changes often\n _intrinsicHeightCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n _intrinsicWidthCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n \n private _useFastMeasurement: boolean | undefined\n private _cachedMeasurementStyles: TextMeasurementStyle | undefined | null\n \n override usesVirtualLayoutingForIntrinsicSizing = NO\n \n //#endregion\n \n // Override addStyleClass to invalidate font cache\n override addStyleClass(styleClass: string) {\n super.addStyleClass(styleClass)\n this._invalidateFontCache()\n }\n \n // Override removeStyleClass to invalidate font cache\n override removeStyleClass(styleClass: string) {\n super.removeStyleClass(styleClass)\n this._invalidateFontCache()\n }\n \n // Override focus to focus the text element\n override focus() {\n this._textElementView.focus()\n }\n \n // Override blur to blur the text element\n override blur() {\n this._textElementView.blur()\n }\n \n override intrinsicContentHeight(constrainingWidth = 0) {\n \n const keyPath = ((this.textElementView.viewHTMLElement.innerHTML || this.text) + \"_csf_\" + this._getFontCacheKey()) + \".\" +\n (\"\" + constrainingWidth).replace(new RegExp(\"\\\\.\", \"g\"), \"_\")\n \n let cacheObject = UITextView._intrinsicHeightCache\n \n if (this.changesOften) {\n cacheObject = this._intrinsicHeightCache\n }\n \n var result = cacheObject.valueForKeyPath(keyPath)\n \n if (IS_LIKE_NULL(result)) {\n // Determine if we should use fast measurement\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement()\n \n if (shouldUseFastPath) {\n // Fast path: use UITextMeasurement with pre-extracted styles\n const styles = this._getMeasurementStyles()\n \n // If styles are invalid (element not properly initialized), fall back to DOM\n if (styles) {\n const size = UITextMeasurement.calculateTextSize(\n this.textElementView.viewHTMLElement,\n this.text || this.textElementView.innerHTML,\n constrainingWidth || undefined,\n undefined,\n styles\n )\n result = size.height\n }\n else {\n // Styles not ready, use DOM measurement\n result = super.intrinsicContentHeight(constrainingWidth)\n }\n }\n else {\n // Fallback: DOM-based measurement for complex content\n result = super.intrinsicContentHeight(constrainingWidth)\n }\n \n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n if (isNaN(result) || (!result && !this.text)) {\n result = super.intrinsicContentHeight(constrainingWidth)\n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n return result\n }\n \n override intrinsicContentWidth(constrainingHeight = 0) {\n \n const keyPath = ((this.textElementView.viewHTMLElement.innerHTML || this.text) + \"_csf_\" + this._getFontCacheKey()) + \".\" +\n (\"\" + constrainingHeight).replace(new RegExp(\"\\\\.\", \"g\"), \"_\")\n \n let cacheObject = UITextView._intrinsicWidthCache\n \n if (this.changesOften) {\n cacheObject = this._intrinsicWidthCache\n }\n \n var result = cacheObject.valueForKeyPath(keyPath)\n \n if (IS_LIKE_NULL(result)) {\n // Determine if we should use fast measurement\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement()\n \n if (shouldUseFastPath) {\n // Fast path: use UITextMeasurement with pre-extracted styles\n const styles = this._getMeasurementStyles()\n \n // If styles are invalid (element not properly initialized), fall back to DOM\n if (styles) {\n const size = UITextMeasurement.calculateTextSize(\n this.textElementView.viewHTMLElement,\n this.text || this.textElementView.innerHTML,\n undefined,\n constrainingHeight || undefined,\n styles\n )\n result = size.width\n }\n else {\n // Styles not ready, use DOM measurement\n result = super.intrinsicContentWidth(constrainingHeight)\n }\n }\n else {\n // Fallback: DOM-based measurement for complex content\n result = super.intrinsicContentWidth(constrainingHeight)\n }\n \n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n return result\n }\n \n \n override intrinsicContentSizeWithConstraints(constrainingHeight: number = 0, constrainingWidth: number = 0) {\n \n const cacheKey = this._getIntrinsicSizeCacheKey(constrainingHeight, constrainingWidth)\n const cachedResult = this._getCachedIntrinsicSize(cacheKey)\n if (cachedResult) {\n return cachedResult\n }\n \n // UITextView needs to measure the text element, not the outer container\n const result = new UIRectangle(0, 0, 0, 0)\n if (this.rootView.forceIntrinsicSizeZero) {\n return result\n }\n \n let temporarilyInViewTree = NO\n let nodeAboveThisView: Node | null = null\n if (!this.isMemberOfViewTree) {\n document.body.appendChild(this.viewHTMLElement)\n temporarilyInViewTree = YES\n nodeAboveThisView = this.viewHTMLElement.nextSibling\n }\n \n // Save and clear styles on the TEXT ELEMENT (not the container)\n const height = this._textElementView.style.height\n const width = this._textElementView.style.width\n \n this._textElementView.style.height = \"\" + constrainingHeight\n this._textElementView.style.width = \"\" + constrainingWidth\n \n const left = this._textElementView.style.left\n const right = this._textElementView.style.right\n const bottom = this._textElementView.style.bottom\n const top = this._textElementView.style.top\n \n this._textElementView.style.left = \"\"\n this._textElementView.style.right = \"\"\n this._textElementView.style.bottom = \"\"\n this._textElementView.style.top = \"\"\n \n // Measure height with the text element\n const resultHeight = this._textElementView.viewHTMLElement.scrollHeight\n \n // Measure width by temporarily setting nowrap\n const whiteSpace = this._textElementView.style.whiteSpace\n this._textElementView.style.whiteSpace = \"nowrap\"\n \n const resultWidth = this._textElementView.viewHTMLElement.scrollWidth\n \n this._textElementView.style.whiteSpace = whiteSpace\n \n // Restore styles on the TEXT ELEMENT\n this._textElementView.style.height = height\n this._textElementView.style.width = width\n \n this._textElementView.style.left = left\n this._textElementView.style.right = right\n this._textElementView.style.bottom = bottom\n this._textElementView.style.top = top\n \n if (temporarilyInViewTree) {\n document.body.removeChild(this.viewHTMLElement)\n if (this.superview) {\n if (nodeAboveThisView) {\n this.superview.viewHTMLElement.insertBefore(this.viewHTMLElement, nodeAboveThisView)\n }\n else {\n this.superview.viewHTMLElement.appendChild(this.viewHTMLElement)\n }\n }\n }\n \n result.height = resultHeight\n result.width = resultWidth\n \n this._setCachedIntrinsicSize(cacheKey, result)\n \n return result\n }\n \n \n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { UIColor } from \"./UIColor\"\nimport { UILocalizedTextObject } from \"./UIInterfaces\"\nimport { EXTEND, FIRST, IS_LIKE_NULL, nil, NO, UIObject, ValueOf, YES } from \"./UIObject\"\nimport { UIRectangle } from \"./UIRectangle\"\nimport { TextMeasurementStyle, UITextMeasurement } from \"./UITextMeasurement\"\nimport { UIView, UIViewBroadcastEvent } from \"./UIView\"\n\n\nexport class UITextView extends UIView {\n \n //#region Static Properties\n \n static defaultTextColor = UIColor.blackColor\n static notificationTextColor = UIColor.redColor\n \n // Global caches for all UILabels\n static _intrinsicHeightCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n static _intrinsicWidthCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n \n static _ptToPx: number\n static _pxToPt: number\n \n static type = {\n \"paragraph\": \"p\",\n \"header1\": \"h1\",\n \"header2\": \"h2\",\n \"header3\": \"h3\",\n \"header4\": \"h4\",\n \"header5\": \"h5\",\n \"header6\": \"h6\",\n \"textArea\": \"textarea\",\n \"textField\": \"input\",\n \"span\": \"span\",\n \"label\": \"label\"\n } as const\n \n static textAlignment = {\n \"left\": \"left\",\n \"center\": \"center\",\n \"right\": \"right\",\n \"justify\": \"justify\"\n } as const\n \n //#endregion\n \n //#region Constructor\n \n \n constructor(\n elementID?: string,\n textViewType: string | ValueOf<typeof UITextView.type> = UITextView.type.paragraph,\n viewHTMLElement = null\n ) {\n \n // Create inner text element as a UIView\n const innerElementID = elementID ? `${elementID}_textElement` : undefined\n const _textElementView = new UIView(innerElementID, null, textViewType)\n \n // Create outer container (wrapper) - this is the main viewHTMLElement\n super(elementID, viewHTMLElement, \"span\", { _textElementView })\n \n // Configure outer container for vertical centering using direct property access\n \n this.configureWithObject({\n // @ts-ignore\n viewHTMLElement: {\n style: {\n display: \"flex\",\n alignItems: \"center\", // Vertical centering\n overflow: \"hidden\"\n }\n }\n })\n \n this.text = \"\"\n \n this._textElementView = _textElementView\n \n // Configure inner text element for ellipsis and positioning\n this._textElementView.configureWithObject({\n style: {\n position: \"relative\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n margin: \"0\",\n padding: \"0\"\n },\n // Forward control events from text element to the container\n sendControlEventForKey: EXTEND(this.sendControlEventForKey.bind(this))\n })\n \n // Add text element as a subview\n this.addSubview(this._textElementView)\n \n \n this.isSingleLine = YES\n \n this.textColor = this.textColor\n \n this.userInteractionEnabled = YES\n \n if (textViewType == UITextView.type.textArea) {\n this.pausesPointerEvents = YES\n this.addTargetForControlEvent(\n UIView.controlEvent.PointerUpInside,\n (sender, event) => sender.focus()\n )\n }\n }\n \n //#endregion\n \n //#region Text Element View Property\n \n private _textElementView: UIView\n \n /**\n * The inner text element that holds the actual text content\n */\n get textElementView(): UIView {\n return this._textElementView\n }\n \n /**\n * Override style to apply to the text element instead of the container\n */\n // override get style() {\n // return this._textElementView.style\n // }\n //\n // /**\n // * Override computedStyle to get computed styles from the text element\n // */\n // override get computedStyle() {\n // return this._textElementView.computedStyle\n // }\n \n /**\n * Access the outer container's style (for positioning, layout, etc.)\n */\n get containerStyle() {\n return this.viewHTMLElement.style\n }\n \n /**\n * Override styleClasses to apply to the text element\n */\n override get styleClasses() {\n return this._textElementView.styleClasses\n }\n \n override set styleClasses(styleClasses: string[]) {\n this._textElementView.styleClasses = styleClasses\n }\n \n //#endregion\n \n //#region Lifecycle Methods\n \n override didReceiveBroadcastEvent(event: UIViewBroadcastEvent) {\n super.didReceiveBroadcastEvent(event)\n }\n \n override willMoveToSuperview(superview: UIView) {\n super.willMoveToSuperview(superview)\n }\n \n override documentFontsDidLoad() {\n super.documentFontsDidLoad()\n this._invalidateFontCache()\n this.invalidateMeasurementStrategy()\n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n UITextView._intrinsicHeightCache = new UIObject() as any\n UITextView._intrinsicWidthCache = new UIObject() as any\n }\n \n override layoutSubviews() {\n super.layoutSubviews()\n \n if (this._automaticFontSizeSelection) {\n this.fontSize = UITextView.automaticallyCalculatedFontSize(\n new UIRectangle(\n 0,\n 0,\n this.textElementView.viewHTMLElement.offsetHeight,\n this.textElementView.viewHTMLElement.offsetWidth\n ),\n this.intrinsicContentSize(),\n this.fontSize,\n this._minFontSize,\n this._maxFontSize\n )\n }\n }\n \n //#endregion\n \n //#region Measurement & Sizing - Private Methods\n \n private _invalidateMeasurementStyles(): void {\n this._cachedMeasurementStyles = undefined\n UITextMeasurement.invalidateElement(this.textElementView.viewHTMLElement)\n this._intrinsicSizesCache = {}\n }\n \n private _getMeasurementStyles(): TextMeasurementStyle | null {\n if (this._cachedMeasurementStyles) {\n return this._cachedMeasurementStyles\n }\n \n // Ensure element is in document\n if (!this.textElementView.viewHTMLElement.isConnected) {\n return null\n }\n \n // Force a layout flush ONCE to ensure computed styles are available\n // This is only paid once per style change, then we use cached values\n this.textElementView.viewHTMLElement.offsetHeight\n \n const computed = window.getComputedStyle(this.textElementView.viewHTMLElement)\n const fontSizeStr = computed.fontSize\n const fontSize = parseFloat(fontSizeStr)\n \n console.log(computed.letterSpacing)\n \n if (!fontSize || isNaN(fontSize)) {\n return null\n }\n \n const lineHeight = this._parseLineHeight(computed.lineHeight, fontSize)\n \n if (isNaN(lineHeight)) {\n return null\n }\n \n const font = [\n computed.fontStyle || \"normal\",\n computed.fontVariant || \"normal\",\n computed.fontWeight || \"normal\",\n fontSize + \"px\",\n computed.fontFamily || \"sans-serif\"\n ].join(\" \")\n \n this._cachedMeasurementStyles = {\n font: font,\n fontSize: fontSize,\n lineHeight: lineHeight,\n whiteSpace: computed.whiteSpace || \"normal\",\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0,\n letterSpacing: parseFloat(computed.letterSpacing) || 0,\n textTransform: computed.textTransform || \"none\"\n }\n \n return this._cachedMeasurementStyles\n }\n \n private _parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === \"normal\") {\n return fontSize * 1.2\n }\n if (lineHeight.endsWith(\"px\")) {\n return parseFloat(lineHeight)\n }\n const numericLineHeight = parseFloat(lineHeight)\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight\n }\n return fontSize * 1.2\n }\n \n private _shouldUseFastMeasurement(): boolean {\n const content = this.text || this.textElementView.innerHTML\n \n if (this._innerHTMLKey || this._localizedTextObject) {\n return false\n }\n \n if (this.notificationAmount > 0) {\n return false\n }\n \n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content)\n \n if (hasComplexHTML) {\n return false\n }\n \n // Canvas measureText silently falls back to the system font when the\n // custom font hasn't been loaded into the canvas font system yet, even\n // if getComputedStyle already reports the correct font family. Guard\n // against this by checking the font is confirmed available before\n // trusting canvas-based measurement.\n const styles = this._getMeasurementStyles()\n if (styles && !document.fonts.check(styles.font)) {\n return false\n }\n \n return true\n }\n \n //#endregion\n \n //#region Measurement & Sizing - Public Methods\n \n setUseFastMeasurement(useFast: boolean): void {\n this._useFastMeasurement = useFast\n this._intrinsicSizesCache = {}\n }\n \n invalidateMeasurementStrategy(): void {\n this._useFastMeasurement = undefined\n this._invalidateMeasurementStyles()\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Alignment\n \n get textAlignment() {\n return this._textElementView.style.textAlign as ValueOf<typeof UITextView.textAlignment>\n }\n \n set textAlignment(textAlignment: ValueOf<typeof UITextView.textAlignment>) {\n this._textAlignment = textAlignment\n this._textElementView.style.textAlign = textAlignment\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Color\n \n get textColor() {\n return this._textColor\n }\n \n set textColor(color: UIColor) {\n this._textColor = color || UITextView.defaultTextColor\n this._textElementView.style.color = this._textColor.stringValue\n }\n \n //#endregion\n \n //#region Getters & Setters - Single Line\n \n get isSingleLine() {\n return this._isSingleLine\n }\n \n set isSingleLine(isSingleLine: boolean) {\n this._isSingleLine = isSingleLine\n \n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n \n if (isSingleLine) {\n // Single line: use nowrap with ellipsis\n this._textElementView.style.whiteSpace = \"nowrap\"\n this._textElementView.style.textOverflow = \"ellipsis\"\n this._textElementView.style.display = \"\"\n this._textElementView.style.webkitLineClamp = \"\"\n this._textElementView.style.webkitBoxOrient = \"\"\n return\n }\n \n // Multiline: allow wrapping, but still show ellipsis if content overflows the container\n // This uses the -webkit-line-clamp approach which works for multiline ellipsis\n this._textElementView.style.whiteSpace = \"normal\"\n this._textElementView.style.textOverflow = \"ellipsis\"\n this._textElementView.style.display = \"-webkit-box\"\n this._textElementView.style.webkitBoxOrient = \"vertical\"\n // Don't set line-clamp to a specific number - let it fill available space\n // The overflow: hidden from the constructor will clip content that exceeds the height\n this.invalidateMeasurementStrategy()\n }\n \n //#endregion\n \n //#region Getters & Setters - Notification Amount\n \n get notificationAmount() {\n return this._notificationAmount\n }\n \n set notificationAmount(notificationAmount: number) {\n if (this._notificationAmount == notificationAmount) {\n return\n }\n \n this._notificationAmount = notificationAmount\n this.text = this.text\n this.setNeedsLayoutUpToRootView()\n this.notificationAmountDidChange(notificationAmount)\n }\n \n notificationAmountDidChange(notificationAmount: number) {\n }\n \n //#endregion\n \n //#region Getters & Setters - Text Content\n \n get text() {\n return (this._text || this.textElementView.viewHTMLElement.innerHTML)\n }\n \n set text(text) {\n this._text = text\n var notificationText = \"\"\n if (this.notificationAmount) {\n notificationText = \"<span style=\\\"color: \" + UITextView.notificationTextColor.stringValue + \";\\\">\" +\n (\" (\" + this.notificationAmount + \")\").bold() + \"</span>\"\n }\n \n const displayText = this.thousandsSeparator !== null\n ? UITextView.applyThousandsSeparatorToNumericalString(text, this.thousandsSeparator)\n : text\n \n if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + displayText + this.textSuffix + notificationText) {\n this.textElementView.viewHTMLElement.innerHTML = this.textPrefix + FIRST(\n displayText, \"\") + this.textSuffix + notificationText\n }\n \n if (this.changesOften) {\n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n }\n \n this._useFastMeasurement = undefined\n this._intrinsicSizesCache = {}\n this.invalidateMeasurementStrategy()\n this._invalidateMeasurementStyles()\n this.clearIntrinsicSizeCache()\n \n this.setNeedsLayout()\n }\n \n \n /**\n * Formats a raw number string by inserting `separator` every three digits\n * in the integer part. Handles negative numbers and decimals (machine locale\n * \".\" as decimal point). Non-numeric strings are returned unchanged.\n */\n static applyThousandsSeparatorToNumericalString(value: string, separator: string): string {\n const trimmed = (value || \"\").trim()\n if (trimmed === \"\") {\n return value\n }\n \n // Split on the decimal point (machine locale uses \".\")\n const parts = trimmed.split(\".\")\n const integerPart = parts[0]\n const decimalPart = parts.length > 1 ? parts[1] : null\n \n // Only format if the integer part consists solely of digits (optionally\n // prefixed with a minus sign). Non-numeric strings pass through as-is.\n if (!/^-?\\d+$/.test(integerPart)) {\n return value\n }\n \n const isNegative = integerPart.startsWith(\"-\")\n const digits = isNegative ? integerPart.slice(1) : integerPart\n \n let formatted = \"\"\n const offset = digits.length % 3\n for (let index = 0; index < digits.length; index++) {\n if (index > 0 && (index - offset) % 3 === 0) {\n formatted += separator\n }\n formatted += digits[index]\n }\n \n const result = (isNegative ? \"-\" : \"\") + formatted\n return decimalPart !== null ? result + \".\" + decimalPart : result\n }\n \n override set innerHTML(innerHTML: string) {\n this.text = innerHTML\n this.invalidateMeasurementStrategy()\n }\n \n override get innerHTML() {\n return this.viewHTMLElement.innerHTML\n }\n \n setText(key: string, defaultString: string, parameters?: { [x: string]: string | UILocalizedTextObject }) {\n this.textElementView.setInnerHTML(key, defaultString, parameters)\n this.invalidateMeasurementStrategy()\n }\n \n //#endregion\n \n //#region Getters & Setters - Font Size\n \n get fontSize() {\n const style = this._textElementView.style.fontSize || window.getComputedStyle(this._textElementView.viewHTMLElement, null).fontSize\n const result = (parseFloat(style) * UITextView._pxToPt)\n return result\n }\n \n set fontSize(fontSize: number) {\n if (fontSize != this.fontSize) {\n this._textElementView.style.fontSize = \"\" + fontSize + \"pt\"\n \n this._intrinsicHeightCache = new UIObject() as any\n this._intrinsicWidthCache = new UIObject() as any\n \n this._invalidateFontCache()\n this._invalidateMeasurementStyles()\n this.clearIntrinsicSizeCache()\n }\n }\n \n useAutomaticFontSize(minFontSize: number = nil, maxFontSize: number = nil) {\n this._automaticFontSizeSelection = YES\n this._minFontSize = minFontSize\n this._maxFontSize = maxFontSize\n this.setNeedsLayout()\n }\n \n //#endregion\n \n //#region Font Caching - Private Methods\n \n /**\n * Get a stable cache key for the font without triggering reflow.\n * Only computes font on first access or when font properties change.\n */\n private _getFontCacheKey(): string {\n // Check if font-related properties have changed\n const currentTriggers = {\n fontSize: this._textElementView.style.fontSize || \"\",\n fontFamily: this._textElementView.style.fontFamily || \"\",\n fontWeight: this._textElementView.style.fontWeight || \"\",\n fontStyle: this._textElementView.style.fontStyle || \"\",\n styleClasses: this.styleClasses.join(\",\")\n }\n \n const hasChanged =\n currentTriggers.fontSize !== this._fontInvalidationTriggers.fontSize ||\n currentTriggers.fontFamily !== this._fontInvalidationTriggers.fontFamily ||\n currentTriggers.fontWeight !== this._fontInvalidationTriggers.fontWeight ||\n currentTriggers.fontStyle !== this._fontInvalidationTriggers.fontStyle ||\n currentTriggers.styleClasses !== this._fontInvalidationTriggers.styleClasses\n \n if (!this._cachedFontKey || hasChanged) {\n // Only access computedStyle when we know something changed\n const computed = this._textElementView.computedStyle\n this._cachedFontKey = [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(\"_\").replace(/[.\\s]/g, \"_\")\n \n this._fontInvalidationTriggers = currentTriggers\n }\n \n return this._cachedFontKey\n }\n \n /**\n * Invalidate font cache when font properties change\n */\n private _invalidateFontCache(): void {\n this._cachedFontKey = undefined\n }\n \n //#endregion\n \n //#region Static Methods\n \n static _determinePXAndPTRatios() {\n if (UITextView._ptToPx) {\n return\n }\n \n const o = document.createElement(\"div\")\n o.style.width = \"1000pt\"\n document.body.appendChild(o)\n UITextView._ptToPx = o.clientWidth / 1000\n document.body.removeChild(o)\n UITextView._pxToPt = 1 / UITextView._ptToPx\n }\n \n static automaticallyCalculatedFontSize(\n bounds: UIRectangle,\n currentSize: UIRectangle,\n currentFontSize: number,\n minFontSize?: number,\n maxFontSize?: number\n ) {\n minFontSize = FIRST(minFontSize, 1)\n maxFontSize = FIRST(maxFontSize, 100000000000)\n \n const heightMultiplier = bounds.height / (currentSize.height + 1)\n const widthMultiplier = bounds.width / (currentSize.width + 1)\n \n var multiplier = heightMultiplier\n if (heightMultiplier > widthMultiplier) {\n multiplier = widthMultiplier\n }\n \n const maxFittingFontSize = currentFontSize * multiplier\n \n if (maxFittingFontSize > maxFontSize) {\n return maxFontSize\n }\n \n if (minFontSize > maxFittingFontSize) {\n return minFontSize\n }\n \n return maxFittingFontSize\n }\n \n //#endregion\n \n //#region Instance Properties - Text Content\n \n _text?: string\n textPrefix = \"\"\n textSuffix = \"\"\n _notificationAmount = 0\n \n _thousandsSeparator: string | null = null\n \n get thousandsSeparator(): string | null {\n return this._thousandsSeparator\n }\n \n set thousandsSeparator(value: string | null) {\n this._thousandsSeparator = value\n }\n \n //#endregion\n \n //#region Instance Properties - Styling\n \n _textColor: UIColor = UITextView.defaultTextColor\n _textAlignment?: ValueOf<typeof UITextView.textAlignment>\n _isSingleLine = YES\n \n //#endregion\n \n //#region Instance Properties - Font & Sizing\n \n _minFontSize?: number\n _maxFontSize?: number\n _automaticFontSizeSelection = NO\n \n // Cache for the computed font string\n private _cachedFontKey?: string\n private _fontInvalidationTriggers = {\n fontSize: \"\",\n fontFamily: \"\",\n fontWeight: \"\",\n fontStyle: \"\",\n styleClasses: \"\"\n }\n \n //#endregion\n \n //#region Instance Properties - Caching & Performance\n \n changesOften = NO\n \n // Local cache for this instance if the label changes often\n _intrinsicHeightCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n _intrinsicWidthCache: { [x: string]: { [x: string]: number; }; } & UIObject = new UIObject() as any\n \n private _useFastMeasurement: boolean | undefined\n private _cachedMeasurementStyles: TextMeasurementStyle | undefined | null\n \n override usesVirtualLayoutingForIntrinsicSizing = NO\n \n //#endregion\n \n // Override addStyleClass to invalidate font cache\n override addStyleClass(styleClass: string) {\n super.addStyleClass(styleClass)\n this._invalidateFontCache()\n }\n \n // Override removeStyleClass to invalidate font cache\n override removeStyleClass(styleClass: string) {\n super.removeStyleClass(styleClass)\n this._invalidateFontCache()\n }\n \n // Override focus to focus the text element\n override focus() {\n this._textElementView.focus()\n }\n \n // Override blur to blur the text element\n override blur() {\n this._textElementView.blur()\n }\n \n override intrinsicContentHeight(constrainingWidth = 0) {\n \n const keyPath = ((this.textElementView.viewHTMLElement.innerHTML || this.text) + \"_csf_\" + this._getFontCacheKey()) + \".\" +\n (\"\" + constrainingWidth).replace(new RegExp(\"\\\\.\", \"g\"), \"_\")\n \n let cacheObject = UITextView._intrinsicHeightCache\n \n if (this.changesOften) {\n cacheObject = this._intrinsicHeightCache\n }\n \n var result = cacheObject.valueForKeyPath(keyPath)\n \n if (IS_LIKE_NULL(result)) {\n // Determine if we should use fast measurement\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement()\n \n if (shouldUseFastPath) {\n // Fast path: use UITextMeasurement with pre-extracted styles\n const styles = this._getMeasurementStyles()\n \n // If styles are invalid (element not properly initialized), fall back to DOM\n if (styles) {\n const size = UITextMeasurement.calculateTextSize(\n this.textElementView.viewHTMLElement,\n this.text || this.textElementView.innerHTML,\n constrainingWidth || undefined,\n undefined,\n styles\n )\n result = size.height\n }\n else {\n // Styles not ready, use DOM measurement\n result = super.intrinsicContentHeight(constrainingWidth)\n }\n }\n else {\n // Fallback: DOM-based measurement for complex content\n result = super.intrinsicContentHeight(constrainingWidth)\n }\n \n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n if (isNaN(result) || (!result && !this.text)) {\n result = super.intrinsicContentHeight(constrainingWidth)\n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n return result\n }\n \n override intrinsicContentWidth(constrainingHeight = 0) {\n \n const keyPath = ((this.textElementView.viewHTMLElement.innerHTML || this.text) + \"_csf_\" + this._getFontCacheKey()) + \".\" +\n (\"\" + constrainingHeight).replace(new RegExp(\"\\\\.\", \"g\"), \"_\")\n \n let cacheObject = UITextView._intrinsicWidthCache\n \n if (this.changesOften) {\n cacheObject = this._intrinsicWidthCache\n }\n \n var result = cacheObject.valueForKeyPath(keyPath)\n \n if (IS_LIKE_NULL(result)) {\n // Determine if we should use fast measurement\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement()\n \n if (shouldUseFastPath) {\n // Fast path: use UITextMeasurement with pre-extracted styles\n const styles = this._getMeasurementStyles()\n \n // If styles are invalid (element not properly initialized), fall back to DOM\n if (styles) {\n const size = UITextMeasurement.calculateTextSize(\n this.textElementView.viewHTMLElement,\n this.text || this.textElementView.innerHTML,\n undefined,\n constrainingHeight || undefined,\n styles\n )\n result = size.width\n }\n else {\n // Styles not ready, use DOM measurement\n result = super.intrinsicContentWidth(constrainingHeight)\n }\n }\n else {\n // Fallback: DOM-based measurement for complex content\n result = super.intrinsicContentWidth(constrainingHeight)\n }\n \n cacheObject.setValueForKeyPath(keyPath, result)\n }\n \n return result\n }\n \n \n override intrinsicContentSizeWithConstraints(constrainingHeight: number = 0, constrainingWidth: number = 0) {\n \n const cacheKey = this._getIntrinsicSizeCacheKey(constrainingHeight, constrainingWidth)\n const cachedResult = this._getCachedIntrinsicSize(cacheKey)\n if (cachedResult) {\n return cachedResult\n }\n \n // UITextView needs to measure the text element, not the outer container\n const result = new UIRectangle(0, 0, 0, 0)\n if (this.rootView.forceIntrinsicSizeZero) {\n return result\n }\n \n let temporarilyInViewTree = NO\n let nodeAboveThisView: Node | null = null\n if (!this.isMemberOfViewTree) {\n document.body.appendChild(this.viewHTMLElement)\n temporarilyInViewTree = YES\n nodeAboveThisView = this.viewHTMLElement.nextSibling\n }\n \n // Save and clear styles on the TEXT ELEMENT (not the container)\n const height = this._textElementView.style.height\n const width = this._textElementView.style.width\n \n this._textElementView.style.height = \"\" + constrainingHeight\n this._textElementView.style.width = \"\" + constrainingWidth\n \n const left = this._textElementView.style.left\n const right = this._textElementView.style.right\n const bottom = this._textElementView.style.bottom\n const top = this._textElementView.style.top\n \n this._textElementView.style.left = \"\"\n this._textElementView.style.right = \"\"\n this._textElementView.style.bottom = \"\"\n this._textElementView.style.top = \"\"\n \n // Measure height with the text element\n const resultHeight = this._textElementView.viewHTMLElement.scrollHeight\n \n // Measure width by temporarily setting nowrap\n const whiteSpace = this._textElementView.style.whiteSpace\n this._textElementView.style.whiteSpace = \"nowrap\"\n \n const resultWidth = this._textElementView.viewHTMLElement.scrollWidth\n \n this._textElementView.style.whiteSpace = whiteSpace\n \n // Restore styles on the TEXT ELEMENT\n this._textElementView.style.height = height\n this._textElementView.style.width = width\n \n this._textElementView.style.left = left\n this._textElementView.style.right = right\n this._textElementView.style.bottom = bottom\n this._textElementView.style.top = top\n \n if (temporarilyInViewTree) {\n document.body.removeChild(this.viewHTMLElement)\n if (this.superview) {\n if (nodeAboveThisView) {\n this.superview.viewHTMLElement.insertBefore(this.viewHTMLElement, nodeAboveThisView)\n }\n else {\n this.superview.viewHTMLElement.appendChild(this.viewHTMLElement)\n }\n }\n }\n \n result.height = resultHeight\n result.width = resultWidth\n \n this._setCachedIntrinsicSize(cacheKey, result)\n \n return result\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.216",
3
+ "version": "1.1.221",
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.titleLabel.text = item.label
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, "&amp;")
87
+ .replace(/</g, "&lt;")
88
+ .replace(/>/g, "&gt;")
89
+ .replace(/"/g, "&quot;")
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 filterText = this.text.toLowerCase().trim()
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 (filterText.length === 0) {
302
+ if (filterWords.length === 0) {
187
303
  filtered = this._autocompleteItems
188
304
  }
189
305
  else {
190
- filtered = this._autocompleteItems.filter(item =>
191
- item.label.toLowerCase().includes(filterText)
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() === filterText
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
 
@@ -416,9 +416,13 @@ export class UITextView extends UIView {
416
416
  (" (" + this.notificationAmount + ")").bold() + "</span>"
417
417
  }
418
418
 
419
- if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {
419
+ const displayText = this.thousandsSeparator !== null
420
+ ? UITextView.applyThousandsSeparatorToNumericalString(text, this.thousandsSeparator)
421
+ : text
422
+
423
+ if (this.textElementView.viewHTMLElement.innerHTML != this.textPrefix + displayText + this.textSuffix + notificationText) {
420
424
  this.textElementView.viewHTMLElement.innerHTML = this.textPrefix + FIRST(
421
- text, "") + this.textSuffix + notificationText
425
+ displayText, "") + this.textSuffix + notificationText
422
426
  }
423
427
 
424
428
  if (this.changesOften) {
@@ -435,6 +439,45 @@ export class UITextView extends UIView {
435
439
  this.setNeedsLayout()
436
440
  }
437
441
 
442
+
443
+ /**
444
+ * Formats a raw number string by inserting `separator` every three digits
445
+ * in the integer part. Handles negative numbers and decimals (machine locale
446
+ * "." as decimal point). Non-numeric strings are returned unchanged.
447
+ */
448
+ static applyThousandsSeparatorToNumericalString(value: string, separator: string): string {
449
+ const trimmed = (value || "").trim()
450
+ if (trimmed === "") {
451
+ return value
452
+ }
453
+
454
+ // Split on the decimal point (machine locale uses ".")
455
+ const parts = trimmed.split(".")
456
+ const integerPart = parts[0]
457
+ const decimalPart = parts.length > 1 ? parts[1] : null
458
+
459
+ // Only format if the integer part consists solely of digits (optionally
460
+ // prefixed with a minus sign). Non-numeric strings pass through as-is.
461
+ if (!/^-?\d+$/.test(integerPart)) {
462
+ return value
463
+ }
464
+
465
+ const isNegative = integerPart.startsWith("-")
466
+ const digits = isNegative ? integerPart.slice(1) : integerPart
467
+
468
+ let formatted = ""
469
+ const offset = digits.length % 3
470
+ for (let index = 0; index < digits.length; index++) {
471
+ if (index > 0 && (index - offset) % 3 === 0) {
472
+ formatted += separator
473
+ }
474
+ formatted += digits[index]
475
+ }
476
+
477
+ const result = (isNegative ? "-" : "") + formatted
478
+ return decimalPart !== null ? result + "." + decimalPart : result
479
+ }
480
+
438
481
  override set innerHTML(innerHTML: string) {
439
482
  this.text = innerHTML
440
483
  this.invalidateMeasurementStrategy()
@@ -585,6 +628,16 @@ export class UITextView extends UIView {
585
628
  textSuffix = ""
586
629
  _notificationAmount = 0
587
630
 
631
+ _thousandsSeparator: string | null = null
632
+
633
+ get thousandsSeparator(): string | null {
634
+ return this._thousandsSeparator
635
+ }
636
+
637
+ set thousandsSeparator(value: string | null) {
638
+ this._thousandsSeparator = value
639
+ }
640
+
588
641
  //#endregion
589
642
 
590
643
  //#region Instance Properties - Styling