@koobiq/components 21.0.0 → 21.0.1

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.
Files changed (222) hide show
  1. package/fesm2022/koobiq-components-code-block.mjs +45 -17
  2. package/fesm2022/koobiq-components-code-block.mjs.map +1 -1
  3. package/fesm2022/koobiq-components-core.mjs +1 -1
  4. package/fesm2022/koobiq-components-core.mjs.map +1 -1
  5. package/fesm2022/koobiq-components-filter-bar.mjs +9 -5
  6. package/fesm2022/koobiq-components-filter-bar.mjs.map +1 -1
  7. package/fesm2022/koobiq-components-icon.mjs +37 -16
  8. package/fesm2022/koobiq-components-icon.mjs.map +1 -1
  9. package/fesm2022/koobiq-components-tree-select.mjs +20 -5
  10. package/fesm2022/koobiq-components-tree-select.mjs.map +1 -1
  11. package/fesm2022/koobiq-components-tree.mjs +218 -83
  12. package/fesm2022/koobiq-components-tree.mjs.map +1 -1
  13. package/package.json +3 -3
  14. package/schematics/collection.json +94 -89
  15. package/schematics/migrations/accordion-state-saving-default/{index.js → index.cjs} +5 -4
  16. package/schematics/migrations/actions-panel-overlay-container/{index.js → index.cjs} +5 -4
  17. package/schematics/migrations/alert-signals/{index.js → index.cjs} +14 -13
  18. package/schematics/migrations/angular-animations-removal/index.cjs +21 -0
  19. package/schematics/migrations/app-switcher-signals/{index.js → index.cjs} +7 -6
  20. package/schematics/migrations/autocomplete-panel-width-auto/{index.js → index.cjs} +6 -5
  21. package/schematics/migrations/autocomplete-signals/{index.js → index.cjs} +7 -6
  22. package/schematics/migrations/badge-signals/{index.js → index.cjs} +7 -6
  23. package/schematics/migrations/button-state-and-styles/{index.js → index.cjs} +7 -6
  24. package/schematics/migrations/button-supported-colors/{index.js → index.cjs} +7 -6
  25. package/schematics/migrations/button-toggle-signals-and-aria/{index.js → index.cjs} +7 -6
  26. package/schematics/migrations/button-truncation/{index.js → index.cjs} +7 -6
  27. package/schematics/migrations/checkbox-signals/{index.js → index.cjs} +9 -8
  28. package/schematics/migrations/clamped-text-disclosure/{index.js → index.cjs} +5 -4
  29. package/schematics/migrations/code-block-highlight-js-core/README.md +53 -0
  30. package/schematics/migrations/code-block-highlight-js-core/data.cjs +64 -0
  31. package/schematics/migrations/code-block-highlight-js-core/index.cjs +319 -0
  32. package/schematics/migrations/code-block-highlight-js-core/schema.json +20 -0
  33. package/schematics/migrations/code-block-signals/{index.js → index.cjs} +7 -6
  34. package/schematics/migrations/color-signals/index.cjs +20 -0
  35. package/schematics/migrations/core-mixins-removal/index.cjs +18 -0
  36. package/schematics/migrations/css-selectors/{index.js → index.cjs} +4 -3
  37. package/schematics/migrations/datepicker-signals/index.cjs +18 -0
  38. package/schematics/migrations/deprecated-icons/{index.js → index.cjs} +8 -7
  39. package/schematics/migrations/design-tokens-v4/{index.js → index.cjs} +3 -2
  40. package/schematics/migrations/divider-signals-and-aria/{index.js → index.cjs} +5 -4
  41. package/schematics/migrations/dl-attribute-coercion/{index.js → index.cjs} +7 -6
  42. package/schematics/migrations/dl-standalone-names/{index.js → index.cjs} +11 -10
  43. package/schematics/migrations/dropdown-demote-overlay/{index.js → index.cjs} +7 -6
  44. package/schematics/migrations/dropdown-signals/index.cjs +20 -0
  45. package/schematics/migrations/empty-state-error-color/{index.js → index.cjs} +5 -4
  46. package/schematics/migrations/empty-state-size-attr/{index.js → index.cjs} +6 -5
  47. package/schematics/migrations/file-upload-cva-and-primitives/{index.js → index.cjs} +5 -4
  48. package/schematics/migrations/file-upload-deprecated-outputs/{index.js → index.cjs} +5 -4
  49. package/schematics/migrations/filter-bar-rename-action/{index.js → index.cjs} +5 -4
  50. package/schematics/migrations/filter-bar-signals/{index.js → index.cjs} +16 -15
  51. package/schematics/migrations/filter-bar-state-saving-default/{index.js → index.cjs} +5 -4
  52. package/schematics/migrations/flag-inner-html/{index.js → index.cjs} +5 -4
  53. package/schematics/migrations/form-field-signals/{index.js → index.cjs} +18 -17
  54. package/schematics/migrations/icon-semantics-and-api/{data.js → data.cjs} +4 -4
  55. package/schematics/migrations/icon-semantics-and-api/{index.js → index.cjs} +6 -5
  56. package/schematics/migrations/icons-replacement/{index.js → index.cjs} +4 -3
  57. package/schematics/migrations/inline-edit-a11y-and-types/{index.js → index.cjs} +5 -4
  58. package/schematics/migrations/input-number-surface/{index.js → index.cjs} +5 -4
  59. package/schematics/migrations/input-signals/index.cjs +18 -0
  60. package/schematics/migrations/link-deprecated-path/{index.js → index.cjs} +4 -3
  61. package/schematics/migrations/link-signals/{index.js → index.cjs} +5 -4
  62. package/schematics/migrations/list-signals/index.cjs +18 -0
  63. package/schematics/migrations/list-tree-multiple-input/{index.js → index.cjs} +7 -6
  64. package/schematics/migrations/loader-overlay-signals/{index.js → index.cjs} +7 -6
  65. package/schematics/migrations/loader-overlay-size-attr/{index.js → index.cjs} +6 -5
  66. package/schematics/migrations/locale-configuration-providers/{index.js → index.cjs} +5 -4
  67. package/schematics/migrations/mandatory-peer-dependencies/{index.js → index.cjs} +3 -2
  68. package/schematics/migrations/markdown-signals/{index.js → index.cjs} +7 -6
  69. package/schematics/migrations/modal-dialog-semantics/{index.js → index.cjs} +5 -4
  70. package/schematics/migrations/modal-signals/index.cjs +20 -0
  71. package/schematics/migrations/navbar-signals-and-aria/{index.js → index.cjs} +7 -6
  72. package/schematics/migrations/new-icons-pack/{index.js → index.cjs} +8 -7
  73. package/schematics/migrations/notification-center-signals/{index.js → index.cjs} +5 -4
  74. package/schematics/migrations/option-signals/index.cjs +20 -0
  75. package/schematics/migrations/popover-leave-delay/{index.js → index.cjs} +5 -4
  76. package/schematics/migrations/progress-bar-percentage-and-aria/{index.js → index.cjs} +5 -4
  77. package/schematics/migrations/progress-spinner-signals/{index.js → index.cjs} +7 -6
  78. package/schematics/migrations/query-list-signals/index.cjs +18 -0
  79. package/schematics/migrations/radio-signals-and-aria/{index.js → index.cjs} +9 -8
  80. package/schematics/migrations/read-state-dwell-handlers/{index.js → index.cjs} +5 -4
  81. package/schematics/migrations/scrollbar-deprecated-path/{index.js → index.cjs} +4 -3
  82. package/schematics/migrations/select-signal-inputs/{index.js → index.cjs} +5 -4
  83. package/schematics/migrations/sidepanel-non-modal-behavior/{index.js → index.cjs} +5 -4
  84. package/schematics/migrations/signal-inputs-subclasses/index.cjs +18 -0
  85. package/schematics/migrations/split-button-optional-disabled/{index.js → index.cjs} +5 -4
  86. package/schematics/migrations/splitter-deprecated-path/{index.js → index.cjs} +4 -3
  87. package/schematics/migrations/state-saving-default/{index.js → index.cjs} +5 -4
  88. package/schematics/migrations/table-cell-content/{index.js → index.cjs} +5 -4
  89. package/schematics/migrations/tabs-signals-and-aria/{index.js → index.cjs} +5 -4
  90. package/schematics/migrations/tag-list-cleaner/{index.js → index.cjs} +7 -6
  91. package/schematics/migrations/tag-slots/{index.js → index.cjs} +6 -5
  92. package/schematics/migrations/tags-signals/{index.js → index.cjs} +7 -6
  93. package/schematics/migrations/textarea-signals/{index.js → index.cjs} +7 -6
  94. package/schematics/migrations/time-range-title-for/{index.js → index.cjs} +5 -4
  95. package/schematics/migrations/timepicker-signals/{index.js → index.cjs} +7 -6
  96. package/schematics/migrations/timezone-grouping-utils/{index.js → index.cjs} +5 -4
  97. package/schematics/migrations/title-encapsulation/{index.js → index.cjs} +5 -4
  98. package/schematics/migrations/toast-stack-and-defaults/{index.js → index.cjs} +5 -4
  99. package/schematics/migrations/tooltip-pointer-events-and-types/{index.js → index.cjs} +5 -4
  100. package/schematics/migrations/top-bar-container-selectors/{index.js → index.cjs} +5 -4
  101. package/schematics/migrations/tree-select-signals/{index.js → index.cjs} +5 -4
  102. package/schematics/migrations/tree-signals/{index.js → index.cjs} +5 -4
  103. package/schematics/migrations/tree-state-saving-default/{index.js → index.cjs} +5 -4
  104. package/schematics/migrations/username-pipe-injection/{index.js → index.cjs} +5 -4
  105. package/schematics/migrations/v20-upgrade/{data.js → data.cjs} +7 -7
  106. package/schematics/migrations/v20-upgrade/index.cjs +19 -0
  107. package/schematics/migrations/v21-upgrade/{data.js → data.cjs} +22 -22
  108. package/schematics/migrations/v21-upgrade/{index.js → index.cjs} +11 -10
  109. package/schematics/migrations/zoneless-change-detection/index.cjs +21 -0
  110. package/schematics/migrations.json +87 -82
  111. package/schematics/ng-add/{index.js → index.cjs} +12 -286
  112. package/schematics/utils/{angular-parsing.js → angular-parsing.cjs} +2 -2
  113. package/schematics/utils/{deprecated-path-migration.js → deprecated-path-migration.cjs} +4 -3
  114. package/schematics/utils/html-config.cjs +148 -0
  115. package/schematics/utils/{icon-migration.js → icon-migration.cjs} +2 -2
  116. package/schematics/utils/list-item-pattern.cjs +16 -0
  117. package/schematics/utils/package-config.cjs +80 -0
  118. package/schematics/utils/project-files.cjs +25 -0
  119. package/schematics/utils/replacement-driver.cjs +337 -0
  120. package/schematics/utils/safe-rule.cjs +23 -0
  121. package/schematics/utils/signal-members-migration.cjs +779 -0
  122. package/schematics/utils/tag-patterns.cjs +21 -0
  123. package/schematics/utils/template-ref-access.cjs +155 -0
  124. package/schematics/utils/{typescript.js → typescript.cjs} +1 -1
  125. package/schematics/utils/warn-only-migration.cjs +52 -0
  126. package/schematics/utils/workspace-styles.cjs +123 -0
  127. package/types/koobiq-components-code-block.d.ts +38 -11
  128. package/types/koobiq-components-filter-bar.d.ts +2 -2
  129. package/types/koobiq-components-tree-select.d.ts +11 -8
  130. package/types/koobiq-components-tree.d.ts +108 -59
  131. package/schematics/migrations/angular-animations-removal/index.js +0 -20
  132. package/schematics/migrations/color-signals/index.js +0 -19
  133. package/schematics/migrations/core-mixins-removal/index.js +0 -17
  134. package/schematics/migrations/datepicker-signals/index.js +0 -17
  135. package/schematics/migrations/dropdown-signals/index.js +0 -19
  136. package/schematics/migrations/input-signals/index.js +0 -17
  137. package/schematics/migrations/list-signals/index.js +0 -17
  138. package/schematics/migrations/modal-signals/index.js +0 -19
  139. package/schematics/migrations/option-signals/index.js +0 -19
  140. package/schematics/migrations/query-list-signals/index.js +0 -17
  141. package/schematics/migrations/signal-inputs-subclasses/index.js +0 -17
  142. package/schematics/migrations/v20-upgrade/index.js +0 -18
  143. package/schematics/migrations/zoneless-change-detection/index.js +0 -20
  144. package/schematics/utils/package-config.js +0 -13
  145. /package/schematics/migrations/accordion-state-saving-default/{data.js → data.cjs} +0 -0
  146. /package/schematics/migrations/actions-panel-overlay-container/{data.js → data.cjs} +0 -0
  147. /package/schematics/migrations/alert-signals/{data.js → data.cjs} +0 -0
  148. /package/schematics/migrations/angular-animations-removal/{data.js → data.cjs} +0 -0
  149. /package/schematics/migrations/app-switcher-signals/{data.js → data.cjs} +0 -0
  150. /package/schematics/migrations/autocomplete-signals/{data.js → data.cjs} +0 -0
  151. /package/schematics/migrations/badge-signals/{data.js → data.cjs} +0 -0
  152. /package/schematics/migrations/button-state-and-styles/{data.js → data.cjs} +0 -0
  153. /package/schematics/migrations/button-supported-colors/{data.js → data.cjs} +0 -0
  154. /package/schematics/migrations/button-toggle-signals-and-aria/{data.js → data.cjs} +0 -0
  155. /package/schematics/migrations/button-truncation/{data.js → data.cjs} +0 -0
  156. /package/schematics/migrations/checkbox-signals/{data.js → data.cjs} +0 -0
  157. /package/schematics/migrations/clamped-text-disclosure/{data.js → data.cjs} +0 -0
  158. /package/schematics/migrations/code-block-signals/{data.js → data.cjs} +0 -0
  159. /package/schematics/migrations/color-signals/{data.js → data.cjs} +0 -0
  160. /package/schematics/migrations/core-mixins-removal/{data.js → data.cjs} +0 -0
  161. /package/schematics/migrations/css-selectors/{data.js → data.cjs} +0 -0
  162. /package/schematics/migrations/datepicker-signals/{data.js → data.cjs} +0 -0
  163. /package/schematics/migrations/deprecated-icons/{data.js → data.cjs} +0 -0
  164. /package/schematics/migrations/divider-signals-and-aria/{data.js → data.cjs} +0 -0
  165. /package/schematics/migrations/dl-attribute-coercion/{data.js → data.cjs} +0 -0
  166. /package/schematics/migrations/dl-standalone-names/{data.js → data.cjs} +0 -0
  167. /package/schematics/migrations/dropdown-demote-overlay/{data.js → data.cjs} +0 -0
  168. /package/schematics/migrations/dropdown-signals/{data.js → data.cjs} +0 -0
  169. /package/schematics/migrations/empty-state-error-color/{data.js → data.cjs} +0 -0
  170. /package/schematics/migrations/file-upload-cva-and-primitives/{data.js → data.cjs} +0 -0
  171. /package/schematics/migrations/file-upload-deprecated-outputs/{data.js → data.cjs} +0 -0
  172. /package/schematics/migrations/filter-bar-rename-action/{data.js → data.cjs} +0 -0
  173. /package/schematics/migrations/filter-bar-signals/{data.js → data.cjs} +0 -0
  174. /package/schematics/migrations/filter-bar-state-saving-default/{data.js → data.cjs} +0 -0
  175. /package/schematics/migrations/flag-inner-html/{data.js → data.cjs} +0 -0
  176. /package/schematics/migrations/form-field-signals/{data.js → data.cjs} +0 -0
  177. /package/schematics/migrations/icons-replacement/{data.js → data.cjs} +0 -0
  178. /package/schematics/migrations/inline-edit-a11y-and-types/{data.js → data.cjs} +0 -0
  179. /package/schematics/migrations/input-number-surface/{data.js → data.cjs} +0 -0
  180. /package/schematics/migrations/input-signals/{data.js → data.cjs} +0 -0
  181. /package/schematics/migrations/link-signals/{data.js → data.cjs} +0 -0
  182. /package/schematics/migrations/list-signals/{data.js → data.cjs} +0 -0
  183. /package/schematics/migrations/list-tree-multiple-input/{data.js → data.cjs} +0 -0
  184. /package/schematics/migrations/loader-overlay-signals/{data.js → data.cjs} +0 -0
  185. /package/schematics/migrations/locale-configuration-providers/{data.js → data.cjs} +0 -0
  186. /package/schematics/migrations/markdown-signals/{data.js → data.cjs} +0 -0
  187. /package/schematics/migrations/modal-dialog-semantics/{data.js → data.cjs} +0 -0
  188. /package/schematics/migrations/modal-signals/{data.js → data.cjs} +0 -0
  189. /package/schematics/migrations/navbar-signals-and-aria/{data.js → data.cjs} +0 -0
  190. /package/schematics/migrations/new-icons-pack/{data.js → data.cjs} +0 -0
  191. /package/schematics/migrations/notification-center-signals/{data.js → data.cjs} +0 -0
  192. /package/schematics/migrations/option-signals/{data.js → data.cjs} +0 -0
  193. /package/schematics/migrations/popover-leave-delay/{data.js → data.cjs} +0 -0
  194. /package/schematics/migrations/progress-bar-percentage-and-aria/{data.js → data.cjs} +0 -0
  195. /package/schematics/migrations/progress-spinner-signals/{data.js → data.cjs} +0 -0
  196. /package/schematics/migrations/query-list-signals/{data.js → data.cjs} +0 -0
  197. /package/schematics/migrations/radio-signals-and-aria/{data.js → data.cjs} +0 -0
  198. /package/schematics/migrations/read-state-dwell-handlers/{data.js → data.cjs} +0 -0
  199. /package/schematics/migrations/select-signal-inputs/{data.js → data.cjs} +0 -0
  200. /package/schematics/migrations/sidepanel-non-modal-behavior/{data.js → data.cjs} +0 -0
  201. /package/schematics/migrations/signal-inputs-subclasses/{data.js → data.cjs} +0 -0
  202. /package/schematics/migrations/split-button-optional-disabled/{data.js → data.cjs} +0 -0
  203. /package/schematics/migrations/state-saving-default/{data.js → data.cjs} +0 -0
  204. /package/schematics/migrations/table-cell-content/{data.js → data.cjs} +0 -0
  205. /package/schematics/migrations/tabs-signals-and-aria/{data.js → data.cjs} +0 -0
  206. /package/schematics/migrations/tag-list-cleaner/{data.js → data.cjs} +0 -0
  207. /package/schematics/migrations/tags-signals/{data.js → data.cjs} +0 -0
  208. /package/schematics/migrations/textarea-signals/{data.js → data.cjs} +0 -0
  209. /package/schematics/migrations/time-range-title-for/{data.js → data.cjs} +0 -0
  210. /package/schematics/migrations/timepicker-signals/{data.js → data.cjs} +0 -0
  211. /package/schematics/migrations/timezone-grouping-utils/{data.js → data.cjs} +0 -0
  212. /package/schematics/migrations/title-encapsulation/{data.js → data.cjs} +0 -0
  213. /package/schematics/migrations/toast-stack-and-defaults/{data.js → data.cjs} +0 -0
  214. /package/schematics/migrations/tooltip-pointer-events-and-types/{data.js → data.cjs} +0 -0
  215. /package/schematics/migrations/top-bar-container-selectors/{data.js → data.cjs} +0 -0
  216. /package/schematics/migrations/tree-select-signals/{data.js → data.cjs} +0 -0
  217. /package/schematics/migrations/tree-signals/{data.js → data.cjs} +0 -0
  218. /package/schematics/migrations/tree-state-saving-default/{data.js → data.cjs} +0 -0
  219. /package/schematics/migrations/username-pipe-injection/{data.js → data.cjs} +0 -0
  220. /package/schematics/migrations/zoneless-change-detection/{data.js → data.cjs} +0 -0
  221. /package/schematics/utils/{ast.js → ast.cjs} +0 -0
  222. /package/schematics/utils/{messages.js → messages.cjs} +0 -0
@@ -2,9 +2,9 @@ import { SelectionModel, DataSource } from '@angular/cdk/collections';
2
2
  import { BehaviorSubject, merge, Observable, of, Subject, asyncScheduler } from 'rxjs';
3
3
  import * as i1 from '@koobiq/components/core';
4
4
  import { escapeRegExp, KbqStateSaving, KbqPseudoCheckbox, KbqOptionActionComponent, kbqFocusAndReveal, kbqGetElementHeight, kbqFocusOptionActionOnTab, hasModifierKey, KbqActionContainer, KBQ_TITLE_TEXT_REF, KBQ_OPTION_ACTION_PARENT, MultipleMode, getSelectAllState, kbqQueryListFrom, KbqLocaleOverridesDirective, KBQ_SELECT_LOCALE_CONFIGURATION, kbqOptionalNumberAttribute, kbqSelectAllHandlerTransform, resolveMultipleMode, FocusKeyManager, kbqLeaveTabOrderOnTabOut, SPACE, LEFT_ARROW, RIGHT_ARROW, isVerticalMovement, isSelectAll, isCopy, TAB, DOWN_ARROW, UP_ARROW, ENTER, HOME, END, PAGE_UP, PAGE_DOWN, toggleSelectAll, getKbqSelectNonArrayValueError, KbqPseudoCheckboxModule } from '@koobiq/components/core';
5
- import { take, map, delay } from 'rxjs/operators';
5
+ import { take, map, delay, filter } from 'rxjs/operators';
6
6
  import * as i0 from '@angular/core';
7
- import { signal, inject, TemplateRef, input, Directive, ViewContainerRef, IterableDiffers, ChangeDetectorRef, viewChild, contentChildren, DestroyRef, effect, untracked, isDevMode, ElementRef, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, EnvironmentInjector, contentChild, linkedSignal, output, EventEmitter, afterNextRender, Renderer2, forwardRef, Injector, QueryList, NgModule } from '@angular/core';
7
+ import { signal, inject, TemplateRef, input, Directive, ViewContainerRef, IterableDiffers, ChangeDetectorRef, viewChild, contentChildren, DestroyRef, effect, untracked, isDevMode, ElementRef, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, EnvironmentInjector, NgZone, Renderer2, contentChild, linkedSignal, output, EventEmitter, afterNextRender, forwardRef, Injector, QueryList, NgModule } from '@angular/core';
8
8
  import { Directionality } from '@angular/cdk/bidi';
9
9
  import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
10
10
  import { takeUntilDestroyed, toSignal, outputFromObservable } from '@angular/core/rxjs-interop';
@@ -21,7 +21,7 @@ import { AsyncScheduler } from 'rxjs/internal/scheduler/AsyncScheduler';
21
21
  /** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */
22
22
  class BaseTreeControl {
23
23
  constructor() {
24
- /** A selection model with multi-selection to track expansion status. */
24
+ /** A selection model with multi-selection to track expansion status: it holds the key of every expanded node. */
25
25
  this.expansionModel = new SelectionModel(true);
26
26
  this.filterModel = new SelectionModel(true);
27
27
  this.filterValue = new BehaviorSubject('');
@@ -31,29 +31,29 @@ class BaseTreeControl {
31
31
  if (this.filterValue.value?.length) {
32
32
  return;
33
33
  }
34
- this.expansionModel.toggle(dataNode);
34
+ this.expansionModel.toggle(this.expansionKey(dataNode));
35
35
  }
36
36
  /** Expands one single data node. */
37
37
  expand(dataNode) {
38
38
  if (this.filterValue.value?.length) {
39
39
  return;
40
40
  }
41
- this.expansionModel.select(dataNode);
41
+ this.expansionModel.select(this.expansionKey(dataNode));
42
42
  }
43
43
  /** Collapses one single data node. */
44
44
  collapse(dataNode) {
45
45
  if (this.filterValue.value?.length) {
46
46
  return;
47
47
  }
48
- this.expansionModel.deselect(dataNode);
48
+ this.expansionModel.deselect(this.expansionKey(dataNode));
49
49
  }
50
50
  /** Whether a given data node is expanded or not. Returns true if the data node is expanded. */
51
51
  isExpanded(dataNode) {
52
- return this.expansionModel.isSelected(dataNode);
52
+ return this.expansionModel.isSelected(this.expansionKey(dataNode));
53
53
  }
54
54
  /** Toggles a subtree rooted at `node` recursively. */
55
55
  toggleDescendants(dataNode) {
56
- if (this.expansionModel.isSelected(dataNode)) {
56
+ if (this.isExpanded(dataNode)) {
57
57
  this.collapseDescendants(dataNode);
58
58
  }
59
59
  else {
@@ -68,13 +68,17 @@ class BaseTreeControl {
68
68
  expandDescendants(dataNode) {
69
69
  const toBeProcessed = [dataNode];
70
70
  toBeProcessed.push(...this.getDescendants(dataNode));
71
- this.expansionModel.select(...toBeProcessed);
71
+ this.expansionModel.select(...toBeProcessed.map((node) => this.expansionKey(node)));
72
72
  }
73
73
  /** Collapses a subtree rooted at given data node recursively. */
74
74
  collapseDescendants(dataNode) {
75
75
  const toBeProcessed = [dataNode];
76
76
  toBeProcessed.push(...this.getDescendants(dataNode));
77
- this.expansionModel.deselect(...toBeProcessed);
77
+ this.expansionModel.deselect(...toBeProcessed.map((node) => this.expansionKey(node)));
78
+ }
79
+ /** The key `expansionModel` holds for a data node. */
80
+ expansionKey(dataNode) {
81
+ return this.trackBy ? this.trackBy(dataNode) : dataNode;
78
82
  }
79
83
  }
80
84
 
@@ -161,7 +165,9 @@ class FlatTreeControl extends BaseTreeControl {
161
165
  /** compareValues will be used to comparing values. */
162
166
  compareViewValues = defaultCompareViewValues,
163
167
  /** isDisabled will be used to determine if the node is disabled. */
164
- isDisabled = () => false) {
168
+ isDisabled = () => false,
169
+ /** Further options: the `trackBy` the nodes are expanded by. */
170
+ options) {
165
171
  super();
166
172
  this.getLevel = getLevel;
167
173
  this.isExpandable = isExpandable;
@@ -170,6 +176,10 @@ class FlatTreeControl extends BaseTreeControl {
170
176
  this.compareValues = compareValues;
171
177
  this.compareViewValues = compareViewValues;
172
178
  this.isDisabled = isDisabled;
179
+ // Positions in each `dataNodes` array: a tree looks up every rendered row, and `indexOf` would make that
180
+ // rows × nodes. Keyed weakly by the array, so replacing `dataNodes` releases the old nodes.
181
+ this.nodeIndexes = new WeakMap();
182
+ this.trackBy = options?.trackBy;
173
183
  this.setFilters(new FilterByViewValue(this), new FilterParentsForNodes(this));
174
184
  }
175
185
  // TODO: nothing in the library uses it; kept as public API at the DS-2196 review. Document or deprecate it.
@@ -186,7 +196,7 @@ class FlatTreeControl extends BaseTreeControl {
186
196
  * with correct levels.
187
197
  */
188
198
  getDescendants(dataNode) {
189
- const startIndex = this.dataNodes.indexOf(dataNode);
199
+ const startIndex = this.indexOfNode(dataNode);
190
200
  const results = [];
191
201
  // Goes through flattened tree nodes in the `dataNodes` array, and get all descendants.
192
202
  // The level of descendants of a tree node must be greater than the level of the given
@@ -206,7 +216,7 @@ class FlatTreeControl extends BaseTreeControl {
206
216
  * data nodes of the tree.
207
217
  */
208
218
  expandAll() {
209
- this.expansionModel.select(...this.dataNodes);
219
+ this.expansionModel.select(...this.dataNodes.map((node) => this.expansionKey(node)));
210
220
  }
211
221
  /**
212
222
  * Ancestors of a data node, outermost first.
@@ -220,7 +230,7 @@ class FlatTreeControl extends BaseTreeControl {
220
230
  * an existing array; a fresh one is used when omitted.
221
231
  */
222
232
  getParents(node, result = []) {
223
- const startIndex = this.dataNodes?.indexOf(node) ?? -1;
233
+ const startIndex = this.indexOfNode(node);
224
234
  if (startIndex === -1) {
225
235
  return result;
226
236
  }
@@ -248,7 +258,7 @@ class FlatTreeControl extends BaseTreeControl {
248
258
  this.filterModel.clear();
249
259
  this.filterModel.select(...result);
250
260
  // set current expansion state according to filtered tree
251
- this.expansionModel.setSelection(...result.filter((node) => this.isExpandable(node)));
261
+ this.expansionModel.setSelection(...result.filter((node) => this.isExpandable(node)).map((node) => this.expansionKey(node)));
252
262
  this.updateFilterValue(value || result);
253
263
  Promise.resolve().then(() => this.restoreExpansionState());
254
264
  }
@@ -265,6 +275,34 @@ class FlatTreeControl extends BaseTreeControl {
265
275
  this.expansionModel.setSelection(...this.expandedItemsBeforeFiltration);
266
276
  }
267
277
  }
278
+ /**
279
+ * Position of the node in `dataNodes`, constant-time while the index matches the array. With `trackBy` a node
280
+ * rebuilt as a new object since is found by its key, as the expansion methods accept it too.
281
+ */
282
+ indexOfNode(node) {
283
+ const { dataNodes, trackBy } = this;
284
+ if (!dataNodes)
285
+ return -1;
286
+ const cached = this.nodeIndexes.get(dataNodes)?.get(node);
287
+ // Checked against the array: `dataNodes` can be mutated in place.
288
+ if (cached !== undefined && dataNodes[cached] === node)
289
+ return cached;
290
+ const index = dataNodes.indexOf(node);
291
+ // A node that is not there leaves the index as it is.
292
+ if (index === -1) {
293
+ if (!trackBy)
294
+ return index;
295
+ const key = trackBy(node);
296
+ return dataNodes.findIndex((candidate) => trackBy(candidate) === key);
297
+ }
298
+ const positions = new Map();
299
+ dataNodes.forEach((dataNode, position) => {
300
+ if (!positions.has(dataNode))
301
+ positions.set(dataNode, position);
302
+ });
303
+ this.nodeIndexes.set(dataNodes, positions);
304
+ return index;
305
+ }
268
306
  }
269
307
 
270
308
  /** Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type. */
@@ -276,9 +314,10 @@ class NestedTreeControl extends BaseTreeControl {
276
314
  * `getDescendants` walks the tree with `take(1)` and returns as soon as the recursion unwinds, so
277
315
  * an asynchronous source yields an incomplete — usually empty — descendant list.
278
316
  */
279
- constructor(getChildren) {
317
+ constructor(getChildren, options) {
280
318
  super();
281
319
  this.getChildren = getChildren;
320
+ this.trackBy = options?.trackBy;
282
321
  }
283
322
  /**
284
323
  * Expands all dataNodes in the tree.
@@ -289,7 +328,7 @@ class NestedTreeControl extends BaseTreeControl {
289
328
  expandAll() {
290
329
  this.expansionModel.clear();
291
330
  const allNodes = this.dataNodes.reduce((accumulator, dataNode) => [...accumulator, ...this.getDescendants(dataNode), dataNode], []);
292
- this.expansionModel.select(...allNodes);
331
+ this.expansionModel.select(...allNodes.map((node) => this.expansionKey(node)));
293
332
  }
294
333
  /** Gets a list of descendant dataNodes of a subtree rooted at given data node recursively. */
295
334
  getDescendants(dataNode) {
@@ -398,6 +437,8 @@ class KbqTreeFlattener {
398
437
  /**
399
438
  * Expand flattened node with current expansion status.
400
439
  * The returned list may have different length.
440
+ *
441
+ * Reads only `isExpanded` of the tree control, so it takes a control of any key type.
401
442
  */
402
443
  expandFlattenedNodes(nodes, treeControl) {
403
444
  const results = [];
@@ -436,8 +477,10 @@ class KbqTreeFlatDataSource extends DataSource {
436
477
  }
437
478
  set data(value) {
438
479
  this._data.set(value);
439
- this.flattenedData.next(this.treeFlattener.flattenNodes(this.data));
440
- this.treeControl.dataNodes = this.flattenedData.value;
480
+ const flattened = this.treeFlattener.flattenNodes(this.data);
481
+ // Assigned before emitting: the tree renders inside `next`, and its rows look themselves up in `dataNodes`.
482
+ this.treeControl.dataNodes = flattened;
483
+ this.flattenedData.next(flattened);
441
484
  }
442
485
  constructor(treeControl, treeFlattener, initialData = []) {
443
486
  super();
@@ -611,6 +654,8 @@ const normalizeTreeState = (parsed) => {
611
654
  * ported by hand.
612
655
  *
613
656
  * `C` narrows the tree control a concrete tree accepts, and so the type its `treeControl` binding is checked against.
657
+ * The bound leaves out `expansionModel` and `trackBy`, the members typed by the control's key, so that it admits
658
+ * a control of any key type: the model makes a control invariant in that type.
614
659
  */
615
660
  class KbqTreeBase {
616
661
  /** Controls the expanded state and holds the data nodes of the tree. */
@@ -652,6 +697,8 @@ class KbqTreeBase {
652
697
  *
653
698
  * Views are reused for nodes the function reports as unchanged, which a data source that rebuilds
654
699
  * its node objects on every emission — a flattener among them — otherwise cannot get.
700
+ *
701
+ * Defaults to the `trackBy` of the tree control, read when the tree initializes and when the data source changes.
655
702
  */
656
703
  this.trackBy = input(undefined, ...(ngDevMode ? [{ debugName: "trackBy" }] : /* istanbul ignore next */ []));
657
704
  this.nodeOutletQuery = viewChild(KbqTreeNodeOutlet, ...(ngDevMode ? [{ debugName: "nodeOutletQuery" }] : /* istanbul ignore next */ []));
@@ -948,7 +995,9 @@ class KbqTreeBase {
948
995
  // Straight at the model rather than through `expand()`, which is a no-op while a filter is
949
996
  // active — restoring must not depend on what the user happens to be searching for.
950
997
  if (found.length) {
951
- this.stateSaving.applying(() => treeControl.expansionModel.select(...found));
998
+ const { trackBy } = treeControl;
999
+ const keys = trackBy ? found.map((node) => trackBy(node)) : found;
1000
+ this.stateSaving.applying(() => treeControl.expansionModel.select(...keys));
952
1001
  }
953
1002
  }
954
1003
  /** The values of the nodes that are currently expanded, plus the ones still waiting to appear. */
@@ -956,7 +1005,11 @@ class KbqTreeBase {
956
1005
  const treeControl = this.flatTreeControl;
957
1006
  const expanded = [];
958
1007
  let skipped = false;
959
- for (const node of treeControl.expansionModel.selected) {
1008
+ // With `trackBy` the model holds keys, which have no value: only the current nodes map back to them.
1009
+ const expandedNodes = treeControl.trackBy
1010
+ ? (treeControl.dataNodes ?? []).filter((node) => treeControl.isExpanded(node))
1011
+ : treeControl.expansionModel.selected;
1012
+ for (const node of expandedNodes) {
960
1013
  const value = treeControl.getValue(node);
961
1014
  if (typeof value === 'string') {
962
1015
  expanded.push(value);
@@ -1039,7 +1092,10 @@ class KbqTreeBase {
1039
1092
  }
1040
1093
  }
1041
1094
  createDataDiffer() {
1042
- return this.differs.find([]).create(this.trackBy());
1095
+ // The bound of `C` leaves `trackBy` out; the key type is the control's own, so it is read as `unknown` here.
1096
+ const controlTrackBy = this.treeControl?.trackBy;
1097
+ const trackBy = this.trackBy() ?? (controlTrackBy && ((_, node) => controlTrackBy(node)));
1098
+ return this.differs.find([]).create(trackBy);
1043
1099
  }
1044
1100
  /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: KbqTreeBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1045
1101
  /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.2.25", type: KbqTreeBase, isStandalone: true, inputs: { treeControlInput: { classPropertyName: "treeControlInput", publicName: "treeControl", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, dataSourceInput: { classPropertyName: "dataSourceInput", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "nodeDefs", predicate: KbqTreeNodeDef, isSignal: true }], viewQueries: [{ propertyName: "nodeOutletQuery", first: true, predicate: KbqTreeNodeOutlet, descendants: true, isSignal: true }], ngImport: i0 }); }
@@ -1141,7 +1197,9 @@ class KbqTreeNodeToggleBaseDirective {
1141
1197
  return this.disabledState();
1142
1198
  }
1143
1199
  get iconState() {
1144
- return this.tree.treeControl.isExpanded(this.node());
1200
+ // The node `toggle()` acts on when `node` is not bound: an unbound `node` would reach the control's
1201
+ // `trackBy` as `undefined`.
1202
+ return this.tree.treeControl.isExpanded(this.node() ?? this.treeNode.data);
1145
1203
  }
1146
1204
  toggle(event) {
1147
1205
  if (this.disabled)
@@ -1332,6 +1390,8 @@ class KbqTreeOption extends KbqTreeNode {
1332
1390
  this.changeDetectorRef = inject(ChangeDetectorRef);
1333
1391
  this.environmentInjector = inject(EnvironmentInjector);
1334
1392
  this.destroyRef = inject(DestroyRef);
1393
+ this.ngZone = inject(NgZone);
1394
+ this.renderer = inject(Renderer2);
1335
1395
  this.focusEvents = new Subject();
1336
1396
  this.blurEvents = new Subject();
1337
1397
  /** Emits when the option takes focus. */
@@ -1396,6 +1456,9 @@ class KbqTreeOption extends KbqTreeNode {
1396
1456
  this.selectedState = signal(false, ...(ngDevMode ? [{ debugName: "selectedState" }] : /* istanbul ignore next */ []));
1397
1457
  this._id = `kbq-tree-option-${uniqueIdCounter++}`;
1398
1458
  this.focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1459
+ // Raised from reporting a focus until `hasFocus` is: `kbqFocusAndReveal` fires `focusin` synchronously, and its
1460
+ // listener calls back into `focus()` while `hasFocus` is still false.
1461
+ this.focusReported = false;
1399
1462
  this.checkbox = signal('unchecked', ...(ngDevMode ? [{ debugName: "checkbox" }] : /* istanbul ignore next */ []));
1400
1463
  this.updateCheckboxState = () => {
1401
1464
  if (this.selectAllRow())
@@ -1404,6 +1467,16 @@ class KbqTreeOption extends KbqTreeNode {
1404
1467
  this.updateParentsCheckboxState(this.data);
1405
1468
  };
1406
1469
  this.tree = tree;
1470
+ // Not a host listener: one would check the views on every row the pointer crosses, and only a tree in a select
1471
+ // reacts to it.
1472
+ const unlisten = this.ngZone.runOutsideAngular(() => this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', () => this.onMouseenter()));
1473
+ this.destroyRef.onDestroy(unlisten);
1474
+ }
1475
+ ngOnInit() {
1476
+ // Selected from its first check: the tree's sweep reaches a new row a macrotask later, and a row it marked
1477
+ // would render unselected first and then report a selection nobody made.
1478
+ if (!this.selectAllRow())
1479
+ this.selectedState.set(!!this.tree.selectionModel?.isSelected(this.data));
1407
1480
  }
1408
1481
  ngDoCheck() {
1409
1482
  // Asked once per check, for a predicate reading plain state; a set to an equal value does nothing.
@@ -1497,11 +1570,16 @@ class KbqTreeOption extends KbqTreeNode {
1497
1570
  return;
1498
1571
  // With the pointer already on this node, revealing it would shift the tree out from under it.
1499
1572
  kbqFocusAndReveal(this.elementRef.nativeElement, focusOrigin === 'mouse');
1500
- if (!this.hasFocus) {
1501
- this.focusEvents.next({ option: this });
1573
+ if (!this.hasFocus && !this.focusReported) {
1574
+ const event = { option: this };
1575
+ this.focusReported = true;
1576
+ this.focusEvents.next(event);
1577
+ this.tree.reportOptionFocus?.(event);
1502
1578
  Promise.resolve().then(() => {
1579
+ this.focusReported = false;
1503
1580
  this.hasFocus = true;
1504
- this.markForCheck();
1581
+ // The view only: focus changes no checkbox state.
1582
+ this.changeDetectorRef.markForCheck();
1505
1583
  });
1506
1584
  }
1507
1585
  });
@@ -1522,7 +1600,9 @@ class KbqTreeOption extends KbqTreeNode {
1522
1600
  return;
1523
1601
  }
1524
1602
  this.hasFocus = false;
1525
- this.blurEvents.next({ option: this });
1603
+ const event = { option: this };
1604
+ this.blurEvents.next(event);
1605
+ this.tree.reportOptionBlur?.(event);
1526
1606
  },
1527
1607
  // The environment injector, not the option's: an option removed while focused is blurred as its view is
1528
1608
  // destroyed, and the tree still has to hear about it to move focus.
@@ -1552,7 +1632,8 @@ class KbqTreeOption extends KbqTreeNode {
1552
1632
  if (setFocus && !this.hasFocus) {
1553
1633
  this.focus();
1554
1634
  }
1555
- this.updateCheckboxState();
1635
+ // Its own state only: an ancestor's reads the selection model, which this leaves as it is.
1636
+ this.syncCheckboxState();
1556
1637
  this.emitSelectionChangeEvent();
1557
1638
  });
1558
1639
  }
@@ -1562,7 +1643,8 @@ class KbqTreeOption extends KbqTreeNode {
1562
1643
  return;
1563
1644
  }
1564
1645
  this.selectedState.set(false);
1565
- this.updateCheckboxState();
1646
+ // Its own state only, as in `select`.
1647
+ this.syncCheckboxState();
1566
1648
  this.emitSelectionChangeEvent();
1567
1649
  });
1568
1650
  }
@@ -1594,8 +1676,9 @@ class KbqTreeOption extends KbqTreeNode {
1594
1676
  }
1595
1677
  markForCheck() {
1596
1678
  this.changeDetectorRef.markForCheck();
1597
- // Its own state only: after a selection, data or mode change the tree calls this on every option.
1598
- if (this.showCheckbox) {
1679
+ // Its own state only: after a selection, data or mode change the tree calls this on every option. A projected
1680
+ // checkbox shows the state while the tree's own is hidden.
1681
+ if (this.showCheckbox || this.externalPseudoCheckbox) {
1599
1682
  this.syncCheckboxState();
1600
1683
  }
1601
1684
  }
@@ -1603,15 +1686,18 @@ class KbqTreeOption extends KbqTreeNode {
1603
1686
  onMouseenter() {
1604
1687
  if (this.disabled || !this.tree.inSelect)
1605
1688
  return;
1606
- this.focus('mouse');
1689
+ // Focusing the option it already holds would only run change detection.
1690
+ if (this.hasFocus && _getFocusedElementPierceShadowDom() === this.elementRef.nativeElement)
1691
+ return;
1692
+ this.ngZone.run(() => this.focus('mouse'));
1607
1693
  }
1608
1694
  /** Recomputes the option's own checkbox state, leaving its ancestors as they are. */
1609
1695
  syncCheckboxState() {
1610
1696
  if (this.selectAllRow())
1611
1697
  return;
1612
1698
  if (this.checkboxThirdState() && this.isExpandable) {
1613
- // One descendant walk, not two: `getDescendants` is a linear scan of the whole data set, and
1614
- // the roll-up runs this method for every ancestor.
1699
+ // One descendant walk, not two: each walk covers the whole subtree, and the roll-up runs this method for
1700
+ // every ancestor.
1615
1701
  const descendants = this.tree.treeControl.getDescendants(this.data);
1616
1702
  const selectedCount = descendants.filter((child) => this.tree.selectionModel.isSelected(child)).length;
1617
1703
  if (selectedCount === descendants.length) {
@@ -1636,7 +1722,7 @@ class KbqTreeOption extends KbqTreeNode {
1636
1722
  return { disabled: this.disabled, expanded: branch ? this.isExpanded : null };
1637
1723
  }
1638
1724
  /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: KbqTreeOption, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1639
- /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: KbqTreeOption, isStandalone: true, selector: "kbq-tree-option", inputs: { checkboxThirdState: { classPropertyName: "checkboxThirdState", publicName: "checkboxThirdState", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, selectAllRow: { classPropertyName: "selectAllRow", publicName: "selectAllRow", isSignal: true, isRequired: false, transformFunction: null }, showCheckboxInput: { classPropertyName: "showCheckboxInput", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", onSelectionChange: "onSelectionChange" }, host: { attributes: { "role": "treeitem" }, listeners: { "focusin": "focus()", "mouseenter": "onMouseenter()", "blur": "blur()", "click": "selectViaInteraction($event)", "keydown": "onKeydown($event)" }, properties: { "class.kbq-tree-option_multiple": "tree.multiple", "class.kbq-selected": "selected", "class.kbq-focused": "hasFocus", "class.kbq-disabled": "checkedDisabled", "class.kbq-action-button-focused": "actionButton()?.active", "attr.id": "id", "attr.tabindex": "-1", "attr.aria-disabled": "checkedDisabled || null", "attr.aria-expanded": "ariaExpanded", "attr.aria-checked": "ariaChecked", "attr.aria-selected": "ariaSelected" }, classAttribute: "kbq-tree-option" }, providers: [
1725
+ /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: KbqTreeOption, isStandalone: true, selector: "kbq-tree-option", inputs: { checkboxThirdState: { classPropertyName: "checkboxThirdState", publicName: "checkboxThirdState", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, selectAllRow: { classPropertyName: "selectAllRow", publicName: "selectAllRow", isSignal: true, isRequired: false, transformFunction: null }, showCheckboxInput: { classPropertyName: "showCheckboxInput", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", onSelectionChange: "onSelectionChange" }, host: { attributes: { "role": "treeitem" }, listeners: { "focusin": "focus()", "blur": "blur()", "click": "selectViaInteraction($event)", "keydown": "onKeydown($event)" }, properties: { "class.kbq-tree-option_multiple": "tree.multiple", "class.kbq-selected": "selected", "class.kbq-focused": "hasFocus", "class.kbq-disabled": "checkedDisabled", "class.kbq-action-button-focused": "actionButton()?.active", "attr.id": "id", "attr.tabindex": "-1", "attr.aria-disabled": "checkedDisabled || null", "attr.aria-expanded": "ariaExpanded", "attr.aria-checked": "ariaChecked", "attr.aria-selected": "ariaSelected" }, classAttribute: "kbq-tree-option" }, providers: [
1640
1726
  { provide: KbqTreeNode, useExisting: KbqTreeOption },
1641
1727
  { provide: KBQ_TITLE_TEXT_REF, useExisting: KbqTreeOption },
1642
1728
  { provide: KBQ_OPTION_ACTION_PARENT, useExisting: KbqTreeOption }
@@ -1669,7 +1755,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
1669
1755
  '[attr.aria-checked]': 'ariaChecked',
1670
1756
  '[attr.aria-selected]': 'ariaSelected',
1671
1757
  '(focusin)': 'focus()',
1672
- '(mouseenter)': 'onMouseenter()',
1673
1758
  '(blur)': 'blur()',
1674
1759
  '(click)': 'selectViaInteraction($event)',
1675
1760
  '(keydown)': 'onKeydown($event)'
@@ -1861,11 +1946,13 @@ class KbqTreeSelection extends KbqTreeBase {
1861
1946
  set autoSelect(value) {
1862
1947
  this.autoSelectState.set(coerceBooleanProperty(value));
1863
1948
  }
1949
+ /** Emits when an option of the tree takes focus. */
1864
1950
  get optionFocusChanges() {
1865
- return merge(...this.renderedOptions.map((option) => option.onFocus));
1951
+ return this.optionFocusEvents.asObservable();
1866
1952
  }
1953
+ /** Emits when an option of the tree loses focus. */
1867
1954
  get optionBlurChanges() {
1868
- return merge(...this.renderedOptions.map((option) => option.onBlur));
1955
+ return this.optionBlurEvents.asObservable();
1869
1956
  }
1870
1957
  /**
1871
1958
  * Selection mode of the tree.
@@ -1965,16 +2052,16 @@ class KbqTreeSelection extends KbqTreeBase {
1965
2052
  if (!this.treeControl) {
1966
2053
  return [];
1967
2054
  }
1968
- const nonSelectableDataNodes = this.renderedOptions
2055
+ const nonSelectableDataNodes = new Set(this.renderedOptions
1969
2056
  .filter((option) => option.disabled || !option.selectable())
1970
- .map((option) => option.data);
2057
+ .map((option) => option.data));
1971
2058
  const candidates = this.treeControl.filterValue.value?.length
1972
2059
  ? this.renderedOptions.map((option) => option.data)
1973
2060
  : (this.treeControl.dataNodes ?? []);
1974
- // The `includes` check comes first on purpose: the "select all" row is itself a non-selectable
2061
+ // The `has` check comes first on purpose: the "select all" row is itself a non-selectable
1975
2062
  // rendered option with no `data`, and `isDisabled` belongs to the consumer — it must never be
1976
2063
  // handed a node their data source has never seen.
1977
- return candidates.filter((node) => !nonSelectableDataNodes.includes(node) && !this.treeControl.isDisabled(node));
2064
+ return candidates.filter((node) => !nonSelectableDataNodes.has(node) && !this.treeControl.isDisabled(node));
1978
2065
  }
1979
2066
  /** Adapter shared by the master checkbox and the Ctrl/Cmd + A handler, so the two cannot drift apart. */
1980
2067
  get selectAllAdapter() {
@@ -2084,6 +2171,9 @@ class KbqTreeSelection extends KbqTreeBase {
2084
2171
  transform: booleanAttribute });
2085
2172
  /** `undefined` while the value is derived from the mode. Written by `KbqTreeSelect` as well. */
2086
2173
  this.autoSelectState = linkedSignal(() => this.autoSelectInput(), ...(ngDevMode ? [{ debugName: "autoSelectState" }] : /* istanbul ignore next */ []));
2174
+ // Options report here, so a subscriber also hears the rows rendered after it subscribed.
2175
+ this.optionFocusEvents = new Subject();
2176
+ this.optionBlurEvents = new Subject();
2087
2177
  /** @docs-private */
2088
2178
  this.multipleInput = input(undefined, { ...(ngDevMode ? { debugName: "multipleInput" } : /* istanbul ignore next */ {}), alias: 'multiple' });
2089
2179
  /**
@@ -2117,6 +2207,9 @@ class KbqTreeSelection extends KbqTreeBase {
2117
2207
  transform: kbqOptionalNumberAttribute });
2118
2208
  this.tabIndexState = signal(0, ...(ngDevMode ? [{ debugName: "tabIndexState" }] : /* istanbul ignore next */ []));
2119
2209
  this.selectionModelSubscription = null;
2210
+ // A selection change asks for a sweep here, not through `renderedOptions.changes`: the key manager and an
2211
+ // embedding select listen to that, and no row changed.
2212
+ this.selectionSweeps = new Subject();
2120
2213
  /** Whether a value report is queued for the end of the current tick. Cleared by `writeValue` to cancel it. */
2121
2214
  this.pendingValueReport = false;
2122
2215
  /** Backs {@link syncingSelection}: a change of the selection made by code is no change to report back. */
@@ -2153,11 +2246,19 @@ class KbqTreeSelection extends KbqTreeBase {
2153
2246
  if (selectAllOption) {
2154
2247
  orderedOptions.push(selectAllOption);
2155
2248
  }
2249
+ // Indexed once per render, as a search per node is quadratic in the rows. The first option wins in both maps.
2250
+ const optionsByNode = new Map();
2251
+ const optionsByValue = new Map();
2252
+ this.unorderedOptions.forEach((option) => {
2253
+ if (!optionsByNode.has(option.data))
2254
+ optionsByNode.set(option.data, option);
2255
+ if (!optionsByValue.has(option.value))
2256
+ optionsByValue.set(option.value, option);
2257
+ });
2156
2258
  this.sortedNodes.forEach((node) => {
2157
2259
  // By node first: nodes do not have to carry distinct values, and a value match would hand every node with
2158
2260
  // the same value the first option holding it.
2159
- const found = this.unorderedOptions.find((option) => option.data === node) ??
2160
- this.unorderedOptions.find((option) => option.value === this.treeControl.getValue(node));
2261
+ const found = optionsByNode.get(node) ?? optionsByValue.get(this.treeControl.getValue(node));
2161
2262
  if (found) {
2162
2263
  orderedOptions.push(found);
2163
2264
  }
@@ -2330,7 +2431,7 @@ class KbqTreeSelection extends KbqTreeBase {
2330
2431
  if (!this.writingValue) {
2331
2432
  this.reportValue();
2332
2433
  }
2333
- this.renderedOptions.notifyOnChanges();
2434
+ this.selectionSweeps.next();
2334
2435
  // The "select all" row renders the selection state.
2335
2436
  this.changeDetectorRef.markForCheck();
2336
2437
  });
@@ -2360,16 +2461,20 @@ class KbqTreeSelection extends KbqTreeBase {
2360
2461
  });
2361
2462
  kbqLeaveTabOrderOnTabOut(this.keyManager.tabOut, this.elementRef.nativeElement, () => this.tabIndex, this.destroyRef);
2362
2463
  this.bindSelectionModel();
2363
- this.renderedOptions.changes
2364
- .pipe(delay(0, this.scheduler), takeUntilDestroyed(this.destroyRef))
2365
- .subscribe((options) => {
2366
- this.resetOptions();
2464
+ this.listenToOptionsFocus();
2465
+ // A render and the selection changes of one task each notify, each with its own timer: the first to fire sweeps
2466
+ // for all of them and the rest are dropped. `auditTime` would tie the later ones to the first one's timer.
2467
+ let sweeps = 0;
2468
+ merge(this.renderedOptions.changes, this.selectionSweeps)
2469
+ .pipe(map(() => sweeps), delay(0, this.scheduler), filter((sweepsWhenNotified) => sweepsWhenNotified === sweeps), takeUntilDestroyed(this.destroyRef))
2470
+ .subscribe(() => {
2471
+ sweeps++;
2367
2472
  // Check to see if we need to update our tab index
2368
2473
  this.updateTabIndex();
2369
2474
  this.syncSelectionModelToDataNodes();
2370
- const selectedValues = this.multiple ? this.getSelectedValues() : [this.getSelectedValues()];
2371
- options.forEach((option) => {
2372
- if (selectedValues.includes(option.value)) {
2475
+ this.renderedOptions.forEach((option) => {
2476
+ // By node rather than by value: rows may share a value.
2477
+ if (this.selectionModel.isSelected(option.data)) {
2373
2478
  option.select(false);
2374
2479
  }
2375
2480
  else {
@@ -2410,7 +2515,8 @@ class KbqTreeSelection extends KbqTreeBase {
2410
2515
  }
2411
2516
  ngOnDestroy() {
2412
2517
  super.ngOnDestroy();
2413
- this.dropSubscriptions();
2518
+ this.optionFocusEvents.complete();
2519
+ this.optionBlurEvents.complete();
2414
2520
  this.focusMonitor.stopMonitoring(this.elementRef);
2415
2521
  }
2416
2522
  focus($event) {
@@ -2576,6 +2682,14 @@ class KbqTreeSelection extends KbqTreeBase {
2576
2682
  }
2577
2683
  }
2578
2684
  /** @docs-private */
2685
+ reportOptionFocus(event) {
2686
+ this.optionFocusEvents.next(event);
2687
+ }
2688
+ /** @docs-private */
2689
+ reportOptionBlur(event) {
2690
+ this.optionBlurEvents.next(event);
2691
+ }
2692
+ /** @docs-private */
2579
2693
  setSelectedOptionsByClick(option, shiftKey, ctrlKey) {
2580
2694
  if (option.disabled || !option.selectable()) {
2581
2695
  return;
@@ -2607,7 +2721,8 @@ class KbqTreeSelection extends KbqTreeBase {
2607
2721
  const options = this.renderedOptions.toArray();
2608
2722
  let fromIndex = this.keyManager.previousActiveItemIndex;
2609
2723
  let toIndex = (this.keyManager.previousActiveItemIndex = this.keyManager.activeItemIndex);
2610
- const selectedOptionState = options[fromIndex]?.selected;
2724
+ // Asked of the model: an option's own state lags it until the next sweep.
2725
+ const selectedOptionState = this.selectionModel.isSelected(options[fromIndex]?.data);
2611
2726
  if (toIndex === fromIndex || fromIndex === -1) {
2612
2727
  if (options[toIndex]?.selectable()) {
2613
2728
  options[toIndex].toggle();
@@ -2617,15 +2732,21 @@ class KbqTreeSelection extends KbqTreeBase {
2617
2732
  if (fromIndex > toIndex) {
2618
2733
  [fromIndex, toIndex] = [toIndex, fromIndex];
2619
2734
  }
2620
- options
2735
+ const nodes = options
2621
2736
  .slice(fromIndex, toIndex + 1)
2622
2737
  .filter((item) => !item.disabled && item.selectable())
2623
- .forEach((renderedOption) => {
2624
- if (!selectedOptionState && this.noUnselectLast && this.selectionModel.selected.length === 1) {
2625
- return;
2626
- }
2627
- renderedOption.setSelected(selectedOptionState);
2628
- });
2738
+ .map((item) => item.data);
2739
+ // One write for the range, so the selection changes once rather than once per row.
2740
+ if (selectedOptionState) {
2741
+ this.selectionModel.select(...nodes);
2742
+ return;
2743
+ }
2744
+ const selected = nodes.filter((node) => this.selectionModel.isSelected(node));
2745
+ // With `noUnselectLast` the last selected node stays, the range giving up its own in order until then.
2746
+ const released = this.noUnselectLast
2747
+ ? Math.max(0, Math.min(selected.length, this.selectionModel.selected.length - 1))
2748
+ : selected.length;
2749
+ this.selectionModel.deselect(...selected.slice(0, released));
2629
2750
  }
2630
2751
  /** @docs-private */
2631
2752
  toggleFocusedOption() {
@@ -2651,12 +2772,16 @@ class KbqTreeSelection extends KbqTreeBase {
2651
2772
  const changed = this.applyNodeChanges(data, dataDiffer, viewContainer, parentData);
2652
2773
  this.sortedNodes = this.getSortedNodes(viewContainer);
2653
2774
  this.changeDetectorRef.detectChanges();
2775
+ // The options query lists the options in the order their views were created, not in the order of the rows, so
2776
+ // rows that only moved (reused through `trackBy`) never make it emit.
2777
+ if (changed && this.renderedOptionsOutOfOrder())
2778
+ this.updateRenderedOptions();
2654
2779
  this.scheduleCheckboxSync(false);
2655
2780
  // The "select all" row is rendered ahead of what its state reads: new rows bind their inputs after it and join
2656
- // `renderedOptions` only once bound, and `KbqTreeFlatDataSource` assigns `dataNodes` after it emits. Nothing
2657
- // else in the tree reads that state but the checkbox sync above, so a tree without the row is done in this one
2658
- // pass. Marked for check, which reaches the views above too: a template reading `allOptionsSelected` by
2659
- // reference reads the same state.
2781
+ // `renderedOptions` only once bound, and a consumer's data source can assign `dataNodes` after it emits.
2782
+ // Nothing else in the tree reads that state but the checkbox sync above, so a tree without the row is done in
2783
+ // this one pass. Marked for check, which reaches the views above too: a template reading `allOptionsSelected`
2784
+ // by reference reads the same state.
2660
2785
  if (!this.showSelectAll)
2661
2786
  return;
2662
2787
  const dataNodes = this.treeControl.dataNodes;
@@ -2668,7 +2793,7 @@ class KbqTreeSelection extends KbqTreeBase {
2668
2793
  /**
2669
2794
  * Recomputes every option's checkbox state where no selection change follows to do it: after `dataNodes` is
2670
2795
  * replaced, which moves the descendants a branch counts, and once a hidden checkbox appears. A microtask later, as
2671
- * a flat data source assigns `dataNodes` only after it emits.
2796
+ * a consumer's data source can assign `dataNodes` after it emits.
2672
2797
  */
2673
2798
  scheduleCheckboxSync(force) {
2674
2799
  Promise.resolve().then(() => {
@@ -2711,11 +2836,23 @@ class KbqTreeSelection extends KbqTreeBase {
2711
2836
  // Selection is applied at the data-node level (incl. collapsed/non-rendered nodes unless a filter
2712
2837
  // is active), while the emitted events carry the selectable rendered options.
2713
2838
  const selectableOptions = this.renderedOptions.filter((option) => !option.disabled && option.selectable());
2839
+ // A multiple selection takes the nodes in one write, so it changes once rather than once per node. A single
2840
+ // one cannot hold them, and keeps the node-by-node writes it always had.
2841
+ const batched = this.selectionModel.isMultipleSelection();
2842
+ const adapter = this.selectAllAdapter;
2714
2843
  // `toggleSelectAll` returns the data nodes whose selection actually flipped — the source of
2715
2844
  // truth, unlike the cached `option.selected` which lags until change detection.
2716
- const changed = toggleSelectAll(this.selectAllAdapter, {
2845
+ const changed = toggleSelectAll(batched ? { ...adapter, setSelected: () => { } } : adapter, {
2717
2846
  allowDeselect: allowDeselect ?? (this.selectAll || this.selectAllToggle())
2718
2847
  });
2848
+ if (batched && changed.length) {
2849
+ if (this.selectionModel.isSelected(changed[0])) {
2850
+ this.selectionModel.deselect(...changed);
2851
+ }
2852
+ else {
2853
+ this.selectionModel.select(...changed);
2854
+ }
2855
+ }
2719
2856
  const changedData = new Set(changed);
2720
2857
  const changedOptions = selectableOptions.filter((option) => changedData.has(option.data));
2721
2858
  // Skip `selectionChange` on a no-op (e.g. Ctrl+A while everything is already selected and
@@ -2924,6 +3061,17 @@ class KbqTreeSelection extends KbqTreeBase {
2924
3061
  updateTabIndex() {
2925
3062
  this.tabIndexState.set(this.userTabIndex || (this.renderedOptions.length === 0 ? -1 : 0));
2926
3063
  }
3064
+ /**
3065
+ * Whether the same options as before now stand for the rows in another order. A different count means rows were
3066
+ * added or removed, which the options query reports itself.
3067
+ */
3068
+ renderedOptionsOutOfOrder() {
3069
+ const options = this.renderedOptions.toArray();
3070
+ const offset = this.selectAllOption() ? 1 : 0;
3071
+ if (options.length - offset !== this.sortedNodes.length)
3072
+ return false;
3073
+ return this.sortedNodes.some((node, index) => options[index + offset].data !== node);
3074
+ }
2927
3075
  getSortedNodes(viewContainer) {
2928
3076
  const array = [];
2929
3077
  for (let i = 0; i < viewContainer.length; i++) {
@@ -2932,22 +3080,9 @@ class KbqTreeSelection extends KbqTreeBase {
2932
3080
  }
2933
3081
  return array;
2934
3082
  }
2935
- resetOptions() {
2936
- this.dropSubscriptions();
2937
- this.listenToOptionsFocus();
2938
- }
2939
- dropSubscriptions() {
2940
- if (this.optionFocusSubscription) {
2941
- this.optionFocusSubscription.unsubscribe();
2942
- this.optionFocusSubscription = null;
2943
- }
2944
- if (this.optionBlurSubscription) {
2945
- this.optionBlurSubscription.unsubscribe();
2946
- this.optionBlurSubscription = null;
2947
- }
2948
- }
2949
3083
  listenToOptionsFocus() {
2950
- this.optionFocusSubscription = this.optionFocusChanges.subscribe((event) => {
3084
+ // Both streams complete with the tree.
3085
+ this.optionFocusEvents.subscribe((event) => {
2951
3086
  const index = this.renderedOptions.toArray().indexOf(event.option);
2952
3087
  this.renderedOptions.filter((option) => option.hasFocus).forEach((option) => (option.hasFocus = false));
2953
3088
  if (this.isValidIndex(index)) {
@@ -2959,7 +3094,7 @@ class KbqTreeSelection extends KbqTreeBase {
2959
3094
  // set, because `KbqTreeOption.hasFocus` is cleared synchronously but only raised a microtask later, so
2960
3095
  // `hasFocusedOption()` reports nothing focused for the whole move. Only a blur of the option the
2961
3096
  // key manager still points at is the focus actually leaving the options.
2962
- this.optionBlurSubscription = this.optionBlurChanges.subscribe(({ option }) => {
3097
+ this.optionBlurEvents.subscribe(({ option }) => {
2963
3098
  if (option === this.keyManager.activeItem && !this.hasFocusedOption() && this.resetFocusedItemOnBlur) {
2964
3099
  this.keyManager.setActiveItem(-1);
2965
3100
  }