ngwr 15.2.0 → 15.4.0

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 (127) hide show
  1. package/alert/styles/_index.scss +13 -5
  2. package/bits/border-glow/styles/_rules.scss +60 -49
  3. package/bits/click-spark/click-spark.scss +5 -1
  4. package/bits/click-spark/styles/_index.scss +10 -15
  5. package/bits/click-spark/styles/_rules.scss +20 -0
  6. package/fesm2022/ngwr-alert.mjs +2 -2
  7. package/fesm2022/ngwr-alert.mjs.map +1 -1
  8. package/fesm2022/ngwr-bits-aurora.mjs +108 -52
  9. package/fesm2022/ngwr-bits-aurora.mjs.map +1 -1
  10. package/fesm2022/ngwr-bits-border-glow-testing.mjs +8 -8
  11. package/fesm2022/ngwr-bits-border-glow-testing.mjs.map +1 -1
  12. package/fesm2022/ngwr-bits-border-glow.mjs +29 -26
  13. package/fesm2022/ngwr-bits-border-glow.mjs.map +1 -1
  14. package/fesm2022/ngwr-bits-dot-grid.mjs +11 -14
  15. package/fesm2022/ngwr-bits-dot-grid.mjs.map +1 -1
  16. package/fesm2022/ngwr-bits-fuzzy-text.mjs +18 -4
  17. package/fesm2022/ngwr-bits-fuzzy-text.mjs.map +1 -1
  18. package/fesm2022/ngwr-bits-ripple-grid.mjs +14 -13
  19. package/fesm2022/ngwr-bits-ripple-grid.mjs.map +1 -1
  20. package/fesm2022/ngwr-bits-splash-cursor.mjs +21 -10
  21. package/fesm2022/ngwr-bits-splash-cursor.mjs.map +1 -1
  22. package/fesm2022/ngwr-bits-waves.mjs +10 -15
  23. package/fesm2022/ngwr-bits-waves.mjs.map +1 -1
  24. package/fesm2022/ngwr-breadcrumbs.mjs +18 -2
  25. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  26. package/fesm2022/ngwr-hotkey.mjs +23 -1
  27. package/fesm2022/ngwr-hotkey.mjs.map +1 -1
  28. package/fesm2022/ngwr-i18n-ar.mjs +2 -0
  29. package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
  30. package/fesm2022/ngwr-i18n-cs.mjs +2 -0
  31. package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
  32. package/fesm2022/ngwr-i18n-de.mjs +2 -0
  33. package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
  34. package/fesm2022/ngwr-i18n-en.mjs +2 -0
  35. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  36. package/fesm2022/ngwr-i18n-es.mjs +2 -0
  37. package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
  38. package/fesm2022/ngwr-i18n-fr.mjs +2 -0
  39. package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
  40. package/fesm2022/ngwr-i18n-he.mjs +2 -0
  41. package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
  42. package/fesm2022/ngwr-i18n-hi.mjs +2 -0
  43. package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
  44. package/fesm2022/ngwr-i18n-id.mjs +2 -0
  45. package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
  46. package/fesm2022/ngwr-i18n-it.mjs +2 -0
  47. package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
  48. package/fesm2022/ngwr-i18n-ja.mjs +2 -0
  49. package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
  50. package/fesm2022/ngwr-i18n-ko.mjs +2 -0
  51. package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
  52. package/fesm2022/ngwr-i18n-nl.mjs +2 -0
  53. package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
  54. package/fesm2022/ngwr-i18n-pl.mjs +2 -0
  55. package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
  56. package/fesm2022/ngwr-i18n-pt.mjs +2 -0
  57. package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
  58. package/fesm2022/ngwr-i18n-ru.mjs +2 -0
  59. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  60. package/fesm2022/ngwr-i18n-sv.mjs +2 -0
  61. package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
  62. package/fesm2022/ngwr-i18n-tr.mjs +2 -0
  63. package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
  64. package/fesm2022/ngwr-i18n-uk.mjs +2 -0
  65. package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
  66. package/fesm2022/ngwr-i18n-vi.mjs +2 -0
  67. package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
  68. package/fesm2022/ngwr-i18n-zh-tw.mjs +2 -0
  69. package/fesm2022/ngwr-i18n-zh-tw.mjs.map +1 -1
  70. package/fesm2022/ngwr-i18n-zh.mjs +2 -0
  71. package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
  72. package/fesm2022/ngwr-pagination.mjs +3 -0
  73. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  74. package/fesm2022/ngwr-pipes.mjs +163 -49
  75. package/fesm2022/ngwr-pipes.mjs.map +1 -1
  76. package/fesm2022/ngwr-schema-form.mjs +3 -0
  77. package/fesm2022/ngwr-schema-form.mjs.map +1 -1
  78. package/fesm2022/ngwr-select-testing.mjs +16 -0
  79. package/fesm2022/ngwr-select-testing.mjs.map +1 -1
  80. package/fesm2022/ngwr-select.mjs +218 -38
  81. package/fesm2022/ngwr-select.mjs.map +1 -1
  82. package/fesm2022/ngwr-statistic.mjs +20 -21
  83. package/fesm2022/ngwr-statistic.mjs.map +1 -1
  84. package/fesm2022/ngwr-version.mjs +1 -1
  85. package/fesm2022/ngwr-version.mjs.map +1 -1
  86. package/i18n/ar.json +2 -0
  87. package/i18n/cs.json +2 -0
  88. package/i18n/de.json +2 -0
  89. package/i18n/en.json +2 -0
  90. package/i18n/es.json +2 -0
  91. package/i18n/fr.json +2 -0
  92. package/i18n/he.json +2 -0
  93. package/i18n/hi.json +2 -0
  94. package/i18n/id.json +2 -0
  95. package/i18n/it.json +2 -0
  96. package/i18n/ja.json +2 -0
  97. package/i18n/ko.json +2 -0
  98. package/i18n/nl.json +2 -0
  99. package/i18n/pl.json +2 -0
  100. package/i18n/pt.json +2 -0
  101. package/i18n/ru.json +2 -0
  102. package/i18n/sv.json +2 -0
  103. package/i18n/tr.json +2 -0
  104. package/i18n/uk.json +2 -0
  105. package/i18n/vi.json +2 -0
  106. package/i18n/zh-TW.json +2 -0
  107. package/i18n/zh.json +2 -0
  108. package/llms-full.txt +5 -5
  109. package/package.json +1 -1
  110. package/schematics/use/symbol-map.json +2 -0
  111. package/select/styles/_index.scss +23 -1
  112. package/skills/ngwr/SKILL.md +3 -1
  113. package/skills/ngwr/references/catalog.md +1 -1
  114. package/types/ngwr-alert.d.ts +3 -1
  115. package/types/ngwr-bits-aurora.d.ts +24 -2
  116. package/types/ngwr-bits-border-glow-testing.d.ts +9 -5
  117. package/types/ngwr-bits-border-glow.d.ts +10 -4
  118. package/types/ngwr-bits-fuzzy-text.d.ts +2 -1
  119. package/types/ngwr-bits-ripple-grid.d.ts +7 -2
  120. package/types/ngwr-bits-splash-cursor.d.ts +2 -1
  121. package/types/ngwr-breadcrumbs.d.ts +15 -0
  122. package/types/ngwr-hotkey.d.ts +14 -2
  123. package/types/ngwr-pipes.d.ts +142 -32
  124. package/types/ngwr-select-testing.d.ts +9 -0
  125. package/types/ngwr-select.d.ts +133 -2
  126. package/types/ngwr-sidebar.d.ts +1 -1
  127. package/types/ngwr-version.d.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-pipes.mjs","names":[],"sources":["../../../projects/lib/pipes/bytes.ts","../../../projects/lib/pipes/date.ts","../../../projects/lib/pipes/mark.ts","../../../projects/lib/pipes/number.ts","../../../projects/lib/pipes/plural.ts","../../../projects/lib/pipes/range.ts","../../../projects/lib/pipes/truncate.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\n/**\n * Binary magnitudes, as the `Intl` unit that labels each one. The COUNTING is\n * 1024-based and these names are decimal — which is the same thing the old\n * hand-written `['B', 'KB', 'MB', …]` table did, and what almost every file\n * manager does. Strictly, 1024 bytes is a kibibyte; `Intl` has no unit for it.\n */\nconst UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'] as const;\n\n/**\n * Humanise a byte count using binary (1024-based) units, in the reader's locale.\n *\n * @example\n * ```html\n * {{ 1234 | wrBytes }} <!-- en: \"1.2 kB\" de: \"1,2 kB\" ru: \"1,2 кБ\" -->\n * {{ 1234567 | wrBytes: 0 }} <!-- en: \"1 MB\" fr: \"1 Mo\" -->\n * {{ 0 | wrBytes }} <!-- en: \"0 B\" ru: \"0 Б\" -->\n * ```\n *\n * **Both halves come from `Intl`, and the unit table is not the catalog's.** The\n * number always did need `LOCALE_ID` — it went through `toFixed`, which writes a\n * full stop in every language, so a German page read `1.2 KB` beside its own\n * `1,2` everywhere else — and the three formatting pipes beside this one\n * (`wrDate`, `wrNumber`, `wrPlural`) had injected it since they shipped.\n *\n * The unit could not follow them into `ngwr/i18n`, and the reason is the pipe\n * rather than the strings: a PURE pipe is re-invoked only when its argument\n * changes, so a catalog that lands after the first render — which is every\n * `provideWrI18nHttpLoader` app — would never reach it, and the pipe would be\n * correct under the static loader and silently English under the other one.\n * `LOCALE_ID` is a constant, so `Intl` has no such failure mode, and it knows\n * every locale ICU ships rather than the twenty-two this package translates.\n *\n * One visible change in English and it is the only one: `KB` became `kB`, which\n * is what CLDR calls that unit. `B`, the space before it and every other\n * magnitude read exactly as they did — see `format` for the part of that which\n * is not free.\n *\n * The other consequence, kept here so nobody rediscovers it as a bug:\n * `<wr-file-upload>` prints its own sizes from `fileUpload.unitKb` and friends,\n * which are public catalog keys a consumer may have overridden, so it renders\n * `KB` where this pipe renders `kB`. Retiring those keys would silently break\n * every override that had been translating them, which is a major-version move.\n */\n@Pipe({ name: 'wrBytes' })\nexport class WrBytes implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(value: number | string | null | undefined, decimals = 1): string {\n if (value === null || value === undefined || value === '') return '';\n const n = Number(value);\n if (!Number.isFinite(n) || n <= 0) return this.format(0, 0, 0);\n\n // Floored as well as capped: for `0 < n < 1` the log is negative, so an\n // unclamped index reads `UNITS[-1]` (`undefined`) and divides by `1024 ** -1`,\n // which rendered `0.5` as \"512.0 undefined\".\n const i = Math.min(Math.max(0, Math.floor(Math.log(n) / Math.log(1024))), UNITS.length - 1);\n // Whole bytes never show a fractional part.\n return this.format(n / 1024 ** i, i, i === 0 ? 0 : Math.max(0, decimals));\n }\n\n /**\n * `Intl`'s NARROW unit, with one space put back between it and the number.\n *\n * Neither display mode is right on its own. `short` spells the smallest unit\n * as a word — \"512 byte\", \"0 byte\" in English, \"0 Byte\" in German — which is\n * both worse copy than the `B` this pipe has always rendered and a bigger\n * change than the fix is worth. `narrow` gives `B` / `Б` / `o` correctly and\n * then closes the gap: \"1.2kB\".\n *\n * So the parts are reassembled rather than the string. Going through\n * `formatToParts` is what keeps the ORDER the locale's own — Hebrew puts the\n * unit first, and hand-joining `${n} ${unit}` would silently straighten every\n * such locale into English order.\n */\n private format(value: number, unit: number, fractionDigits: number): string {\n const parts = new Intl.NumberFormat(this.locale, {\n style: 'unit',\n unit: UNITS[unit],\n unitDisplay: 'narrow',\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n }).formatToParts(value);\n\n const at = parts.findIndex(part => part.type === 'unit');\n if (at === -1) return parts.map(part => part.value).join('');\n\n const out = parts.map(part => part.value);\n // The slot on the number's side of the unit: already a separator in some\n // locales, absent in others.\n const gap = at === 0 ? 1 : at - 1;\n if (parts[gap]?.type === 'literal') out[gap] = ' ';\n else out.splice(at === 0 ? 1 : at, 0, ' ');\n return out.join('');\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport { type WrDateFormat, WrDateAdapter, toLocalDate } from 'ngwr/date';\n\n/**\n * Locale-aware date formatting. Delegates to `WrDateAdapter` — the native one\n * unless the app provides its own — so both the named format keys\n * (`'shortDate'`, `'longDate'`, …) and raw token strings work with nothing to\n * set up. A date-only string (`'2026-01-01'`) is that day in the local zone,\n * as `DatePipe` reads it, not UTC midnight.\n *\n * @example\n * ```html\n * {{ now | wrDate }} <!-- shortDate -->\n * {{ now | wrDate: 'mediumDateTime' }} <!-- named key -->\n * {{ now | wrDate: 'dd.MM.yyyy' }} <!-- raw token string -->\n * ```\n */\n@Pipe({ name: 'wrDate' })\nexport class WrDate implements PipeTransform {\n private readonly adapter = inject<WrDateAdapter<Date>>(WrDateAdapter);\n\n transform(\n value: Date | string | number | null | undefined,\n format: WrDateFormat | (string & {}) = 'shortDate'\n ): string {\n if (value === null || value === undefined || value === '') return '';\n const date = value instanceof Date ? value : toLocalDate(value);\n if (Number.isNaN(date.getTime())) return '';\n\n return this.adapter.format(date, format);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe, type PipeTransform, inject } from '@angular/core';\nimport { DomSanitizer, type SafeHtml } from '@angular/platform-browser';\n\n/** Escapes RegExp metacharacters so user input is safe to inline. @internal */\nconst RE_META = /[\\\\^$.*+?()[\\]{}|]/g;\n\n/** HTML-entities for the four chars that matter inside our `<mark>` wrap. @internal */\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n/**\n * Wrap every occurrence of `query` in `<mark>` tags. Useful for highlighting\n * search-term matches in autocomplete results, lists, and table cells.\n *\n * The output is `SafeHtml` — bind with `[innerHTML]`. Input is escaped first\n * so user-supplied values can't inject markup.\n *\n * @example\n * ```html\n * <span [innerHTML]=\"row.name | wrMark: query()\"></span>\n *\n * <!-- Case-sensitive: -->\n * <span [innerHTML]=\"row.name | wrMark: query() : true\"></span>\n * ```\n */\n@Pipe({ name: 'wrMark' })\nexport class WrMark implements PipeTransform {\n private readonly sanitizer = inject(DomSanitizer);\n\n /**\n * @param value Source text.\n * @param query Search term to highlight. Empty/null skips the wrap.\n * @param caseSensitive Match exactly. @default false\n */\n transform(value: string | null | undefined, query: string | null | undefined, caseSensitive = false): SafeHtml {\n if (value === null || value === undefined) return '';\n const text = String(value);\n // Escaped on this path too. The output is bound with `[innerHTML]`, so\n // handing back the raw string let markup in `value` render as elements the\n // moment the search box was cleared — the same value drawn as text while a\n // query was typed, as an `<img>` when it was not.\n if (!query) return escapeHtml(text);\n\n // Match the RAW text and escape the pieces afterwards, never the other way\n // round. Escaping first rewrites `&` as `&amp;`, and a query of `m` then lands\n // INSIDE that entity: the `<mark>` splits it and the browser paints the\n // entity's own letters, so `Tom & Jerry` renders as `Tom &amp; Jerry`. The\n // mirror failure is a match that can never happen — `<`, `>` and `\"` no longer\n // exist in escaped text. The pipe's promise is untouched: every piece that came\n // from `value` still goes through `escapeHtml`, matched or not, so the only\n // tags in the output are the `<mark>`s built here.\n const pattern = new RegExp(`(${query.replace(RE_META, '\\\\$&')})`, caseSensitive ? 'g' : 'gi');\n const html = text\n .split(pattern)\n .map((part, index) => (index % 2 ? `<mark>${escapeHtml(part)}</mark>` : escapeHtml(part)))\n .join('');\n\n return this.sanitizer.bypassSecurityTrustHtml(html);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport type { WrNumberStyle } from './types';\n\n/**\n * Locale-aware number formatting via `Intl.NumberFormat`. Uses Angular's\n * `LOCALE_ID` as the default locale.\n *\n * Accepts either a style shortcut (with optional currency code) or a full\n * `Intl.NumberFormatOptions` object for fine-grained control.\n *\n * @example\n * ```html\n * {{ 1234.5 | wrNumber }} <!-- \"1,234.5\" -->\n * {{ 0.875 | wrNumber: 'percent' }} <!-- \"87.5%\" -->\n * {{ 19.99 | wrNumber: 'currency': 'USD' }} <!-- \"$19.99\" -->\n * {{ 19.99 | wrNumber: { minimumFractionDigits: 2 } }}\n * ```\n */\n@Pipe({ name: 'wrNumber' })\nexport class WrNumber implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(\n value: number | string | null | undefined,\n styleOrOptions: WrNumberStyle | Intl.NumberFormatOptions = 'decimal',\n currency = 'USD'\n ): string {\n if (value === null || value === undefined || value === '') return '';\n const n = Number(value);\n if (!Number.isFinite(n)) return '';\n\n const options: Intl.NumberFormatOptions =\n typeof styleOrOptions === 'string' ? this.toOptions(styleOrOptions, currency) : styleOrOptions;\n\n return new Intl.NumberFormat(this.locale, options).format(n);\n }\n\n private toOptions(style: WrNumberStyle, currency: string): Intl.NumberFormatOptions {\n switch (style) {\n case 'percent':\n return { style: 'percent', maximumFractionDigits: 2 };\n case 'currency':\n return { style: 'currency', currency };\n case 'decimal':\n default:\n return { style: 'decimal' };\n }\n }\n}\n\nexport type { WrNumberStyle } from './types';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport type { WrPluralForms, WrPluralOptions } from './types';\n\n/**\n * Locale-aware pluralization via `Intl.PluralRules`. Picks the right word\n * form for the count — including languages with more than two forms\n * (Russian, Polish, Arabic, …) — without hand-rolled `n % 10` rules.\n *\n * Categories follow CLDR: `zero` / `one` / `two` / `few` / `many` /\n * `other`. Missing categories fall back to `other`.\n *\n * @example\n * ```html\n * {{ 1 | wrPlural: { one: 'comment', other: 'comments' } }}\n * <!-- \"1 comment\" -->\n *\n * {{ 5 | wrPlural: { one: 'файл', few: 'файла', other: 'файлов' } }}\n * <!-- \"5 файлов\" (with LOCALE_ID = ru) -->\n *\n * {{ count() | wrPlural: forms : { includeValue: false } }}\n * <!-- word only -->\n * ```\n */\n@Pipe({ name: 'wrPlural' })\nexport class WrPlural implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(value: number | null | undefined, forms: WrPluralForms, options: WrPluralOptions = {}): string {\n if (value === null || value === undefined || !Number.isFinite(value)) return '';\n const locale = options.locale ?? this.locale;\n const category = new Intl.PluralRules(locale).select(value);\n const word = forms[category] ?? forms.other;\n if (options.includeValue === false) return word;\n return `${new Intl.NumberFormat(locale).format(value)} ${word}`;\n }\n}\n\nexport type { WrPluralForms, WrPluralOptions } from './types';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\n/**\n * Creates an array of sequential indices `[0, 1, 2, ..., n-1]`.\n *\n * Useful for repeating template blocks a fixed number of times.\n *\n * @example\n * ```html\n * @for (i of (5 | wrRange); track i) {\n * <div>Item {{ i }}</div>\n * }\n * ```\n */\n@Pipe({ name: 'wrRange' })\nexport class WrRange implements PipeTransform {\n transform(length: number): number[] {\n return Array.from({ length }, (_, i) => i);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nlet segmenter: Intl.Segmenter | undefined;\n\n/**\n * Clamp a string to `length` characters, appending `ellipsis` when truncated.\n * A character is what a reader sees as one — a grapheme cluster — so an emoji,\n * a flag or a letter with a combining accent is kept whole or dropped whole,\n * never cut into half a surrogate pair that renders as a replacement box.\n *\n * @example\n * ```html\n * {{ 'Hello, world' | wrTruncate: 5 }} <!-- \"Hello…\" -->\n * {{ 'Hello, world' | wrTruncate: 8: '...' }} <!-- \"Hello, w...\" -->\n * ```\n */\n@Pipe({ name: 'wrTruncate' })\nexport class WrTruncate implements PipeTransform {\n transform(value: string | null | undefined, length = 80, ellipsis = '…'): string {\n if (value === null || value === undefined) return '';\n const s = String(value);\n // No more graphemes than UTF-16 units, so a string this short is never cut.\n if (s.length <= length) return s;\n // `slice`'s own coercion of the count, which is what this pipe did before it\n // counted graphemes: a fraction truncates, and NaN or a negative keeps nothing.\n const limit = Math.max(0, Math.trunc(length) || 0);\n segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n let count = 0;\n for (const { index } of segmenter.segment(s)) {\n if (count === limit) return s.slice(0, index) + ellipsis;\n count++;\n }\n return s;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-pipes.mjs","names":[],"sources":["../../../projects/lib/pipes/bytes.ts","../../../projects/lib/pipes/date.ts","../../../projects/lib/pipes/duration.ts","../../../projects/lib/pipes/mark.ts","../../../projects/lib/pipes/number.ts","../../../projects/lib/pipes/plural.ts","../../../projects/lib/pipes/range.ts","../../../projects/lib/pipes/truncate.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\n/**\n * Binary magnitudes, as the `Intl` unit that labels each one. The COUNTING is\n * 1024-based and these names are decimal — which is the same thing the old\n * hand-written `['B', 'KB', 'MB', …]` table did, and what almost every file\n * manager does. Strictly, 1024 bytes is a kibibyte; `Intl` has no unit for it.\n */\nconst UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'] as const;\n\n/**\n * `Intl`'s NARROW unit, with one space put back between it and the number.\n *\n * Neither display mode is right on its own. `short` spells the smallest unit\n * as a word — \"512 byte\", \"0 byte\" in English, \"0 Byte\" in German — which is\n * both worse copy than the `B` this pipe has always rendered and a bigger\n * change than the fix is worth. `narrow` gives `B` / `Б` / `o` correctly and\n * then closes the gap: \"1.2kB\".\n *\n * So the parts are reassembled rather than the string. Going through\n * `formatToParts` is what keeps the ORDER the locale's own — Hebrew puts the\n * unit first, and hand-joining `${n} ${unit}` would silently straighten every\n * such locale into English order.\n */\nfunction format(locale: string, value: number, unit: number, fractionDigits: number): string {\n const parts = new Intl.NumberFormat(locale, {\n style: 'unit',\n unit: UNITS[unit],\n unitDisplay: 'narrow',\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n }).formatToParts(value);\n\n const at = parts.findIndex(part => part.type === 'unit');\n if (at === -1) return parts.map(part => part.value).join('');\n\n const out = parts.map(part => part.value);\n // The slot on the number's side of the unit: already a separator in some\n // locales, absent in others.\n const gap = at === 0 ? 1 : at - 1;\n if (parts[gap]?.type === 'literal') out[gap] = ' ';\n else out.splice(at === 0 ? 1 : at, 0, ' ');\n return out.join('');\n}\n\n/**\n * Humanise a byte count using binary (1024-based) units, in the reader's locale.\n *\n * @example\n * ```html\n * {{ 1234 | wrBytes }} <!-- en: \"1.2 kB\" de: \"1,2 kB\" ru: \"1,2 кБ\" -->\n * {{ 1234567 | wrBytes: 0 }} <!-- en: \"1 MB\" fr: \"1 Mo\" -->\n * {{ 0 | wrBytes }} <!-- en: \"0 B\" ru: \"0 Б\" -->\n * ```\n *\n * **Both halves come from `Intl`, and the unit table is not the catalog's.** The\n * number always did need `LOCALE_ID` — it went through `toFixed`, which writes a\n * full stop in every language, so a German page read `1.2 KB` beside its own\n * `1,2` everywhere else — and the three formatting pipes beside this one\n * (`wrDate`, `wrNumber`, `wrPlural`) had injected it since they shipped.\n *\n * The unit could not follow them into `ngwr/i18n`, and the reason is the pipe\n * rather than the strings: a PURE pipe is re-invoked only when its argument\n * changes, so a catalog that lands after the first render — which is every\n * `provideWrI18nHttpLoader` app — would never reach it, and the pipe would be\n * correct under the static loader and silently English under the other one.\n * `LOCALE_ID` is a constant, so `Intl` has no such failure mode, and it knows\n * every locale ICU ships rather than the twenty-two this package translates.\n *\n * One visible change in English and it is the only one: `KB` became `kB`, which\n * is what CLDR calls that unit. `B`, the space before it and every other\n * magnitude read exactly as they did — see `format` for the part of that which\n * is not free.\n *\n * The other consequence, kept here so nobody rediscovers it as a bug:\n * `<wr-file-upload>` prints its own sizes from `fileUpload.unitKb` and friends,\n * which are public catalog keys a consumer may have overridden, so it renders\n * `KB` where this pipe renders `kB`. Retiring those keys would silently break\n * every override that had been translating them, which is a major-version move.\n *\n * {@link wrFormatBytes} is the same output as a function.\n */\n@Pipe({ name: 'wrBytes' })\nexport class WrBytes implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(value: number | string | null | undefined, decimals = 1): string {\n return wrFormatBytes(value, this.locale, decimals);\n }\n}\n\n/**\n * A byte count in binary (1024-based) units, in the given locale: the same\n * output as the `wrBytes` pipe, for a string built in TypeScript. `null`,\n * `undefined` and an empty string give an empty string; zero, a negative\n * number and a value that is not a number give `0 B`.\n *\n * @example\n * ```ts\n * wrFormatBytes(1234, 'en-US'); // \"1.2 kB\"\n * wrFormatBytes(1234567, 'fr-FR', 0); // \"1 Mo\"\n * ```\n */\nexport function wrFormatBytes(value: number | string | null | undefined, locale: string, decimals = 1): string {\n if (value === null || value === undefined || value === '') return '';\n const n = Number(value);\n if (!Number.isFinite(n) || n <= 0) return format(locale, 0, 0, 0);\n\n // Floored as well as capped: for `0 < n < 1` the log is negative, so an\n // unclamped index reads `UNITS[-1]` (`undefined`) and divides by `1024 ** -1`,\n // which rendered `0.5` as \"512.0 undefined\".\n const i = Math.min(Math.max(0, Math.floor(Math.log(n) / Math.log(1024))), UNITS.length - 1);\n // Whole bytes never show a fractional part.\n return format(locale, n / 1024 ** i, i, i === 0 ? 0 : Math.max(0, decimals));\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport { type WrDateFormat, WrDateAdapter, toLocalDate } from 'ngwr/date';\n\n/**\n * A date through a `WrDateAdapter`: the same output as the `wrDate` pipe, for a\n * string built in TypeScript. A date-only string (`'2026-01-01'`) is that day in\n * the local zone, as `DatePipe` reads it. `null`, `undefined`, an empty string\n * and a date the engine refuses give an empty string.\n *\n * The adapter stands where `wrFormatNumber` takes a locale, because the output\n * is the adapter's: the native one formats in `WR_DATE_LOCALE`, and an app's own\n * adapter formats however it formats. Neither can be rebuilt from a locale\n * string, so the caller passes `inject(WrDateAdapter)` once.\n *\n * @example\n * ```ts\n * private readonly dates = inject(WrDateAdapter);\n *\n * wrFormatDate(row.createdAt, this.dates); // shortDate\n * wrFormatDate(row.createdAt, this.dates, 'mediumDateTime'); // named key\n * wrFormatDate(row.createdAt, this.dates, 'dd.MM.yyyy'); // raw token string\n * ```\n */\nexport function wrFormatDate(\n value: Date | string | number | null | undefined,\n adapter: WrDateAdapter<Date>,\n format: WrDateFormat | (string & {}) = 'shortDate'\n): string {\n if (value === null || value === undefined || value === '') return '';\n const date = value instanceof Date ? value : toLocalDate(value);\n if (Number.isNaN(date.getTime())) return '';\n\n return adapter.format(date, format);\n}\n\n/**\n * Locale-aware date formatting. Delegates to `WrDateAdapter` — the native one\n * unless the app provides its own — so both the named format keys\n * (`'shortDate'`, `'longDate'`, …) and raw token strings work with nothing to\n * set up. A date-only string (`'2026-01-01'`) is that day in the local zone,\n * as `DatePipe` reads it, not UTC midnight. {@link wrFormatDate} is the same\n * output as a function.\n *\n * @example\n * ```html\n * {{ now | wrDate }} <!-- shortDate -->\n * {{ now | wrDate: 'mediumDateTime' }} <!-- named key -->\n * {{ now | wrDate: 'dd.MM.yyyy' }} <!-- raw token string -->\n * ```\n */\n@Pipe({ name: 'wrDate' })\nexport class WrDate implements PipeTransform {\n private readonly adapter = inject<WrDateAdapter<Date>>(WrDateAdapter);\n\n transform(\n value: Date | string | number | null | undefined,\n format: WrDateFormat | (string & {}) = 'shortDate'\n ): string {\n return wrFormatDate(value, this.adapter, format);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport type { WrDurationStyle, WrDurationUnit } from './types';\n\n/** Each unit's length in milliseconds, largest first. */\nconst UNIT_MS: readonly (readonly [WrDurationUnit, number])[] = [\n ['day', 86_400_000],\n ['hour', 3_600_000],\n ['minute', 60_000],\n ['second', 1000],\n ['millisecond', 1],\n];\n\nconst CLOCK_UNITS: ReadonlySet<WrDurationUnit> = new Set(['hour', 'minute', 'second']);\nconst LIST_UNITS: ReadonlySet<WrDurationUnit> = new Set(['day', 'hour', 'minute', 'second']);\n\nfunction formatClock(ms: number, locale: string): string {\n const { hour = 0, minute = 0, second = 0 } = wrSplitDuration(ms, CLOCK_UNITS);\n const digits = (n: number, width: number): string =>\n new Intl.NumberFormat(locale, { useGrouping: false, minimumIntegerDigits: width }).format(n);\n const tail = `${digits(minute, hour > 0 ? 2 : 1)}:${digits(second, 2)}`;\n return hour > 0 ? `${digits(hour, 1)}:${tail}` : tail;\n}\n\nfunction formatUnits(ms: number, locale: string): string {\n const parts = wrSplitDuration(ms, LIST_UNITS);\n const unit = (name: WrDurationUnit, n: number): string =>\n new Intl.NumberFormat(locale, { style: 'unit', unit: name, unitDisplay: 'short' }).format(n);\n\n const words = (Object.entries(parts) as [WrDurationUnit, number][])\n .filter(([, n]) => n > 0)\n .map(([name, n]) => unit(name, n));\n if (words.length === 0) return unit('second', 0);\n return new Intl.ListFormat(locale, { style: 'narrow', type: 'unit' }).format(words);\n}\n\n/**\n * Split a span of milliseconds into the units asked for. A unit left out is\n * carried into the next larger one that is kept, so `['hour', 'minute',\n * 'second']` over three days and an hour reads 73 hours; the smallest unit kept\n * floors whatever remains. Shared with `wr-statistic-countdown`, whose format\n * tokens pick the units.\n *\n * @internal\n */\nexport function wrSplitDuration(\n ms: number,\n units: ReadonlySet<WrDurationUnit>\n): Readonly<Partial<Record<WrDurationUnit, number>>> {\n const out: Partial<Record<WrDurationUnit, number>> = {};\n let rest = ms;\n for (const [unit, size] of UNIT_MS) {\n if (!units.has(unit)) continue;\n const n = Math.floor(rest / size);\n out[unit] = n;\n rest -= n * size;\n }\n return out;\n}\n\n/**\n * A span of milliseconds as text, in the given locale.\n *\n * Two styles. `'clock'` is digits and colons: `1:30:00`, and `5:07` under an\n * hour; days are carried into the hours, since a clock has no day digit, so two\n * days read `48:00:00`. `'units'` is every non-zero unit as a word the locale\n * writes, `1 hr 30 min` in English and `1 h 30 min` in French, through\n * `Intl.NumberFormat`'s unit style and `Intl.ListFormat` — `Intl.DurationFormat`\n * is above the browser floor. The digits are the locale's own in both styles.\n *\n * Whole seconds: a fraction of a second is floored in both styles. Zero reads\n * `0:00` and `0 sec`. A negative span is its absolute value with the locale's\n * minus sign in front, `-5:07`. `null`, `undefined`, an empty string and a value\n * that is not a finite number give an empty string, as `wrFormatNumber` does.\n *\n * The same output as the `wrDuration` pipe, for a string built in TypeScript.\n *\n * @example\n * ```ts\n * wrFormatDuration(5_400_000, 'en-US'); // \"1:30:00\"\n * wrFormatDuration(5_400_000, 'en-US', 'units'); // \"1 hr 30 min\"\n * wrFormatDuration(307_000, 'fr-FR', 'units'); // \"5 min 7 s\"\n * ```\n */\nexport function wrFormatDuration(\n value: number | string | null | undefined,\n locale: string,\n style: WrDurationStyle = 'clock'\n): string {\n if (value === null || value === undefined || value === '') return '';\n const ms = Number(typeof value === 'string' ? value.trim() : value);\n if (!Number.isFinite(ms)) return '';\n\n const span = Math.abs(ms);\n const body = style === 'units' ? formatUnits(span, locale) : formatClock(span, locale);\n // A span that floors to zero carries no sign: `-0:00` states nothing.\n if (ms >= 0 || span < 1000) return body;\n\n const minus = new Intl.NumberFormat(locale).formatToParts(-1).find(part => part.type === 'minusSign');\n return `${minus?.value ?? '-'}${body}`;\n}\n\n/**\n * A span of milliseconds as clock digits or as units, in Angular's `LOCALE_ID`.\n * See {@link wrFormatDuration} for the two styles and the edge cases.\n *\n * @example\n * ```html\n * {{ 5400000 | wrDuration }} <!-- \"1:30:00\" -->\n * {{ 5400000 | wrDuration: 'units' }} <!-- \"1 hr 30 min\" -->\n * {{ 307000 | wrDuration }} <!-- \"5:07\" -->\n * ```\n */\n@Pipe({ name: 'wrDuration' })\nexport class WrDuration implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(value: number | string | null | undefined, style: WrDurationStyle = 'clock'): string {\n return wrFormatDuration(value, this.locale, style);\n }\n}\n\nexport type { WrDurationStyle, WrDurationUnit } from './types';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe, type PipeTransform, inject } from '@angular/core';\nimport { DomSanitizer, type SafeHtml } from '@angular/platform-browser';\n\n/** Escapes RegExp metacharacters so user input is safe to inline. @internal */\nconst RE_META = /[\\\\^$.*+?()[\\]{}|]/g;\n\n/** HTML-entities for the four chars that matter inside our `<mark>` wrap. @internal */\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n/**\n * Wrap every occurrence of `query` in `<mark>` tags. Useful for highlighting\n * search-term matches in autocomplete results, lists, and table cells.\n *\n * The output is `SafeHtml` — bind with `[innerHTML]`. Input is escaped first\n * so user-supplied values can't inject markup.\n *\n * @example\n * ```html\n * <span [innerHTML]=\"row.name | wrMark: query()\"></span>\n *\n * <!-- Case-sensitive: -->\n * <span [innerHTML]=\"row.name | wrMark: query() : true\"></span>\n * ```\n */\n@Pipe({ name: 'wrMark' })\nexport class WrMark implements PipeTransform {\n private readonly sanitizer = inject(DomSanitizer);\n\n /**\n * @param value Source text.\n * @param query Search term to highlight. Empty/null skips the wrap.\n * @param caseSensitive Match exactly. @default false\n */\n transform(value: string | null | undefined, query: string | null | undefined, caseSensitive = false): SafeHtml {\n if (value === null || value === undefined) return '';\n const text = String(value);\n // Escaped on this path too. The output is bound with `[innerHTML]`, so\n // handing back the raw string let markup in `value` render as elements the\n // moment the search box was cleared — the same value drawn as text while a\n // query was typed, as an `<img>` when it was not.\n if (!query) return escapeHtml(text);\n\n // Match the RAW text and escape the pieces afterwards, never the other way\n // round. Escaping first rewrites `&` as `&amp;`, and a query of `m` then lands\n // INSIDE that entity: the `<mark>` splits it and the browser paints the\n // entity's own letters, so `Tom & Jerry` renders as `Tom &amp; Jerry`. The\n // mirror failure is a match that can never happen — `<`, `>` and `\"` no longer\n // exist in escaped text. The pipe's promise is untouched: every piece that came\n // from `value` still goes through `escapeHtml`, matched or not, so the only\n // tags in the output are the `<mark>`s built here.\n const pattern = new RegExp(`(${query.replace(RE_META, '\\\\$&')})`, caseSensitive ? 'g' : 'gi');\n const html = text\n .split(pattern)\n .map((part, index) => (index % 2 ? `<mark>${escapeHtml(part)}</mark>` : escapeHtml(part)))\n .join('');\n\n return this.sanitizer.bypassSecurityTrustHtml(html);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport type { WrNumberStyle } from './types';\n\n/** Digits with one optional point and a sign: the form `Intl` reads exactly. */\nconst PLAIN_DECIMAL = /^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)$/;\n\nfunction toOptions(style: WrNumberStyle, currency: string): Intl.NumberFormatOptions {\n switch (style) {\n case 'percent':\n return { style: 'percent', maximumFractionDigits: 2 };\n case 'currency':\n return { style: 'currency', currency };\n case 'decimal':\n default:\n return { style: 'decimal' };\n }\n}\n\n/**\n * A number through `Intl.NumberFormat`, in the given locale: the same output as\n * the `wrNumber` pipe, for a string built in TypeScript. Takes a style shortcut\n * (with an optional currency code) or a full `Intl.NumberFormatOptions` object.\n * `null`, `undefined`, an empty string and a value that is not a finite number\n * give an empty string.\n *\n * @example\n * ```ts\n * wrFormatNumber(1234.5, 'en-US'); // \"1,234.5\"\n * wrFormatNumber(0.875, 'en-US', 'percent'); // \"87.5%\"\n * wrFormatNumber(19.99, 'de-DE', 'currency', 'EUR'); // \"19,99 €\"\n * wrFormatNumber(19.99, 'en-US', { minimumFractionDigits: 2 });\n * ```\n */\nexport function wrFormatNumber(\n value: number | string | null | undefined,\n locale: string,\n styleOrOptions: WrNumberStyle | Intl.NumberFormatOptions = 'decimal',\n currency = 'USD'\n): string {\n if (value === null || value === undefined || value === '') return '';\n const text = typeof value === 'string' ? value.trim() : null;\n const n = Number(text ?? value);\n if (!Number.isFinite(n)) return '';\n\n const options: Intl.NumberFormatOptions =\n typeof styleOrOptions === 'string' ? toOptions(styleOrOptions, currency) : styleOrOptions;\n\n // A plain decimal string goes to `Intl` as it is: through `Number()`,\n // `'12345678901234567.89'` printed as `12,345,678,901,234,568`. Any other\n // form keeps the number, since ICU throws on an exponent of a billion or more.\n // `format()` has read a string since ES2023 (Chrome 106, Firefox 116, Safari\n // 15.4); the ES2022 lib this builds against types it as a number only.\n const exact = text !== null && PLAIN_DECIMAL.test(text) ? (text as unknown as number) : n;\n return new Intl.NumberFormat(locale, options).format(exact);\n}\n\n/**\n * Locale-aware number formatting via `Intl.NumberFormat`. Uses Angular's\n * `LOCALE_ID` as the default locale.\n *\n * Accepts either a style shortcut (with optional currency code) or a full\n * `Intl.NumberFormatOptions` object for fine-grained control.\n * {@link wrFormatNumber} is the same output as a function.\n *\n * @example\n * ```html\n * {{ 1234.5 | wrNumber }} <!-- \"1,234.5\" -->\n * {{ 0.875 | wrNumber: 'percent' }} <!-- \"87.5%\" -->\n * {{ 19.99 | wrNumber: 'currency': 'USD' }} <!-- \"$19.99\" -->\n * {{ 19.99 | wrNumber: { minimumFractionDigits: 2 } }}\n * ```\n */\n@Pipe({ name: 'wrNumber' })\nexport class WrNumber implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(\n value: number | string | null | undefined,\n styleOrOptions: WrNumberStyle | Intl.NumberFormatOptions = 'decimal',\n currency = 'USD'\n ): string {\n return wrFormatNumber(value, this.locale, styleOrOptions, currency);\n }\n}\n\nexport type { WrNumberStyle } from './types';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { LOCALE_ID, Pipe, inject } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nimport type { WrPluralForms, WrPluralOptions } from './types';\n\n/**\n * A count and its word form through `Intl.PluralRules`, in the given locale:\n * the same output as the `wrPlural` pipe, for a string built in TypeScript.\n * `options.locale` still wins over `locale`, as it wins over `LOCALE_ID` in the\n * pipe. `null`, `undefined` and a value that is not a finite number give an\n * empty string.\n *\n * @example\n * ```ts\n * wrFormatPlural(1, 'en', { one: 'comment', other: 'comments' }); // \"1 comment\"\n * wrFormatPlural(5, 'ru', { one: 'файл', few: 'файла', other: 'файлов' }); // \"5 файлов\"\n * wrFormatPlural(5, 'en', forms, { includeValue: false }); // \"comments\"\n * ```\n */\nexport function wrFormatPlural(\n value: number | null | undefined,\n locale: string,\n forms: WrPluralForms,\n options: WrPluralOptions = {}\n): string {\n if (value === null || value === undefined || !Number.isFinite(value)) return '';\n const tag = options.locale ?? locale;\n const category = new Intl.PluralRules(tag).select(value);\n const word = forms[category] ?? forms.other;\n if (options.includeValue === false) return word;\n return `${new Intl.NumberFormat(tag).format(value)} ${word}`;\n}\n\n/**\n * Locale-aware pluralization via `Intl.PluralRules`. Picks the right word\n * form for the count — including languages with more than two forms\n * (Russian, Polish, Arabic, …) — without hand-rolled `n % 10` rules.\n *\n * Categories follow CLDR: `zero` / `one` / `two` / `few` / `many` /\n * `other`. Missing categories fall back to `other`. {@link wrFormatPlural} is\n * the same output as a function.\n *\n * @example\n * ```html\n * {{ 1 | wrPlural: { one: 'comment', other: 'comments' } }}\n * <!-- \"1 comment\" -->\n *\n * {{ 5 | wrPlural: { one: 'файл', few: 'файла', other: 'файлов' } }}\n * <!-- \"5 файлов\" (with LOCALE_ID = ru) -->\n *\n * {{ count() | wrPlural: forms : { includeValue: false } }}\n * <!-- word only -->\n * ```\n */\n@Pipe({ name: 'wrPlural' })\nexport class WrPlural implements PipeTransform {\n private readonly locale = inject(LOCALE_ID);\n\n transform(value: number | null | undefined, forms: WrPluralForms, options: WrPluralOptions = {}): string {\n return wrFormatPlural(value, this.locale, forms, options);\n }\n}\n\nexport type { WrPluralForms, WrPluralOptions } from './types';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\n/**\n * Creates an array of sequential indices `[0, 1, 2, ..., n-1]`.\n *\n * Useful for repeating template blocks a fixed number of times.\n *\n * @example\n * ```html\n * @for (i of (5 | wrRange); track i) {\n * <div>Item {{ i }}</div>\n * }\n * ```\n */\n@Pipe({ name: 'wrRange' })\nexport class WrRange implements PipeTransform {\n transform(length: number): number[] {\n return Array.from({ length }, (_, i) => i);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Pipe } from '@angular/core';\nimport type { PipeTransform } from '@angular/core';\n\nlet segmenter: Intl.Segmenter | undefined;\n\n/**\n * Clamp a string to `length` characters, appending `ellipsis` when truncated.\n * A character is what a reader sees as one — a grapheme cluster — so an emoji,\n * a flag or a letter with a combining accent is kept whole or dropped whole,\n * never cut into half a surrogate pair that renders as a replacement box.\n *\n * @example\n * ```html\n * {{ 'Hello, world' | wrTruncate: 5 }} <!-- \"Hello…\" -->\n * {{ 'Hello, world' | wrTruncate: 8: '...' }} <!-- \"Hello, w...\" -->\n * ```\n */\n@Pipe({ name: 'wrTruncate' })\nexport class WrTruncate implements PipeTransform {\n transform(value: string | null | undefined, length = 80, ellipsis = '…'): string {\n if (value === null || value === undefined) return '';\n const s = String(value);\n // No more graphemes than UTF-16 units, so a string this short is never cut.\n if (s.length <= length) return s;\n // `slice`'s own coercion of the count, which is what this pipe did before it\n // counted graphemes: a fraction truncates, and NaN or a negative keeps nothing.\n const limit = Math.max(0, Math.trunc(length) || 0);\n segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n let count = 0;\n for (const { index } of segmenter.segment(s)) {\n if (count === limit) return s.slice(0, index) + ellipsis;\n count++;\n }\n return s;\n }\n}\n"],"mappings":"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"}
@@ -264,6 +264,9 @@ var WrSchemaForm = class WrSchemaForm {
264
264
  "overscan",
265
265
  "noResultsText",
266
266
  "loadingText",
267
+ "loadErrorText",
268
+ "retryable",
269
+ "retryText",
267
270
  "loading",
268
271
  "separators",
269
272
  "allowDuplicates",
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-schema-form.mjs","names":[],"sources":["../../../projects/lib/schema-form/schema-form.ts","../../../projects/lib/schema-form/schema-form.html"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Component, ViewEncapsulation, computed, input, isDevMode } from '@angular/core';\nimport { FormField, type Field, type FieldTree, isFieldTree } from '@angular/forms/signals';\n\nimport { WrCheckbox } from 'ngwr/checkbox';\nimport { WrDatePicker } from 'ngwr/date-picker';\nimport { WR_FIELD, WrFormField, wrFieldLabel, type WrFieldSpec } from 'ngwr/form';\nimport { WrInput } from 'ngwr/input';\nimport { WrInputNumber } from 'ngwr/input-number';\nimport { WrRadio, WrRadioGroup } from 'ngwr/radio';\nimport { WrOption, WrSelect } from 'ngwr/select';\nimport { WrSlider } from 'ngwr/slider';\nimport { WrSwitch } from 'ngwr/switch';\nimport { WrTextarea } from 'ngwr/textarea';\nimport { numAttr } from 'ngwr/utils';\n\n/** One field the template draws, resolved once per schema change. */\ninterface WrRenderedField {\n readonly key: string;\n readonly field: Field<never>;\n readonly spec: WrFieldSpec;\n /** `spec.label`, or the key de-camel-cased. */\n readonly label: string;\n /**\n * The same text, or `''` for the two kinds that carry their own label inside\n * the control. Resolved here rather than in the template: a method call in a\n * template is re-run on every pass, and this is decided once per schema.\n */\n readonly labelAbove: string;\n /** `grid-column`, already clamped to the form's own `columns`. */\n readonly column: string;\n}\n\n/**\n * The two kinds that carry their own label. Everything else gets one above the\n * control, from `<wr-form-field>`.\n */\nconst SELF_LABELLING = new Set<WrFieldSpec['kind']>(['checkbox', 'switch']);\n\n/** What each kind writes into the model, for the dev-mode mismatch warning. */\nconst EXPECTED_TYPE: Partial<Record<WrFieldSpec['kind'], string>> = {\n input: 'string',\n textarea: 'string',\n number: 'number',\n checkbox: 'boolean',\n switch: 'boolean',\n slider: 'number',\n};\n\n/**\n * Warn when a group's described fields are about to be drawn by nothing.\n *\n * A field tree is iterated one level deep, so an object-valued field with no\n * `WR_FIELD` of its own is skipped exactly like an undescribed leaf — and its\n * children go with it. That is the right default (a model nests for reasons\n * that have nothing to do with a screen), but it is indistinguishable from a\n * half-drawn form: nothing is missing from the DOM, so nothing looks wrong.\n *\n * Describing the children and getting none of them is never intentional, so\n * that exact shape is what this reports. A group whose children carry no spec\n * either is silent, because it is a model detail this screen does not render.\n */\nfunction warnOnUndrawnGroup(key: string, group: unknown): void {\n // A field tree over a primitive is not iterable, and every undescribed leaf\n // arrives here — so the guard is what keeps this from throwing on the common\n // case rather than a defence against an exotic one.\n const subfields = (group as Partial<Iterable<[string, unknown]>>)[Symbol.iterator];\n if (typeof subfields !== 'function') return;\n\n let described = 0;\n for (const [, child] of group as Iterable<[string, unknown]>) {\n if (isFieldTree(child) && (child as Field<never>)().metadata(WR_FIELD)?.()) described++;\n }\n if (described === 0) return;\n\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] <wr-schema-form>: \"${key}\" is a group holding ${described} described ` +\n `field(s), and none of them are drawn — a field tree is walked one level deep. ` +\n `Draw the group with a second <wr-schema-form [field]=\"form.${key}\" />, or give ` +\n `\"${key}\" a WR_FIELD of its own if this screen is meant to skip it.`\n );\n}\n\n/**\n * Warn when a `kind` contradicts what the field actually holds.\n *\n * This is the one failure mode of the whole component that nothing else\n * reports: `kind` is a string on a metadata value, so TypeScript cannot hold it\n * against the field's own type, and a `kind: 'number'` on a `string` field\n * renders a `<wr-input-number>` that writes numbers into it forever. The runtime\n * value is the only evidence available, so an empty or nullish one says nothing\n * and is left alone.\n */\nfunction warnOnKindMismatch(f: WrRenderedField): void {\n const expected = EXPECTED_TYPE[f.spec.kind];\n const value = f.field().value();\n if (!expected || value === null || value === undefined || value === '') return;\n if (typeof value === expected) return;\n\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] <wr-schema-form>: field \"${f.key}\" is described as kind '${f.spec.kind}', ` +\n `which writes a ${expected}, but the field holds a ${typeof value}. ` +\n `The control will overwrite the model with the wrong type.`\n );\n}\n\n/**\n * A form drawn from a Signal Forms schema — one `<wr-form-field>`, one control\n * and one set of error messages per described field, laid out on a grid.\n *\n * The presentation rides on the SAME schema as the validation, through the\n * `WR_FIELD` metadata key from `ngwr/form`:\n *\n * ```ts\n * const profile = form(model, path => {\n * required(path.email, { message: 'Enter your email.' });\n * email(path.email, { message: 'That is not an email address.' });\n * metadata(path.email, WR_FIELD, () => ({ kind: 'input', type: 'email' }));\n * metadata(path.role, WR_FIELD, () => ({ kind: 'select', options: ROLES }));\n * });\n * ```\n * ```html\n * <wr-schema-form [field]=\"profile\" [columns]=\"2\" />\n * ```\n *\n * The error messages are the rules' own: each field's `<wr-form-field>` shows a\n * validator's `message`, else `validation.<key>` from the app's i18n catalog.\n * ngwr ships no error texts, so a rule with neither renders no message.\n *\n * **A field with no `WR_FIELD` metadata is skipped**, silently and on purpose: a\n * schema may describe more than one screen renders, and guessing a control from\n * a field's type would draw a `wr-switch` for every boolean flag in the model.\n * Order is the model's own key order.\n *\n * **`required`, `disabled`, `readonly` and `hidden` are NOT in the spec.** They\n * are Angular's own schema functions, `[formField]` already applies the first\n * three to the control, and this component reads `hidden()` to decide whether to\n * draw the field at all — which Angular's docs say a template has to do itself.\n * Re-declaring any of them here would be a second source of truth for state the\n * form already owns.\n *\n * **Why its own entry point rather than `ngwr/form`.** Drawing nine kinds means\n * depending on nine controls, and `ngwr/form` is the layout primitive every\n * form imports — folding this in would put a date picker and a slider into the\n * bundle of an app that only wanted `<wr-form-field>`.\n *\n * @see https://ngwr.dev/reference/components/schema-form\n */\n@Component({\n selector: 'wr-schema-form',\n templateUrl: './schema-form.html',\n encapsulation: ViewEncapsulation.None,\n imports: [\n FormField,\n WrCheckbox,\n WrDatePicker,\n WrFormField,\n WrInput,\n WrInputNumber,\n WrOption,\n WrRadio,\n WrRadioGroup,\n WrSelect,\n WrSlider,\n WrSwitch,\n WrTextarea,\n ],\n host: {\n class: 'wr-schema-form',\n '[style.grid-template-columns]': 'template()',\n },\n})\nexport class WrSchemaForm<T extends object> {\n // Generic, rather than `FieldTree<Record<string, unknown>>`, because a model\n // declared as an `interface` has no index signature and is not assignable to\n // that: every consumer would have had to declare their model as a type alias\n // just to bind it. `T extends object` still refuses a field tree over a\n // primitive, which has no subfields and would draw nothing.\n /**\n * The form to draw — the value `form()` returned, or any object-valued field\n * inside one.\n */\n readonly field = input.required<FieldTree<T>>();\n\n /**\n * Grid columns. A field spans one of them unless its spec says otherwise.\n * @default 1\n */\n readonly columns = input(1, { transform: numAttr(1) });\n\n /**\n * The track list, written as a style rather than published as a\n * `--wr-schema-form-columns` hook: `columns` always has a value, so the\n * inline style would win over any declaration a consumer wrote and the hook\n * would be one nobody could use.\n */\n protected readonly template = computed(() => `repeat(${Math.max(1, this.columns())}, minmax(0, 1fr))`);\n\n /**\n * The described fields, in the model's key order.\n *\n * A field tree over an object is iterable as `[key, field]` pairs, which is\n * the whole reason this component needs no field list of its own. `isFieldTree`\n * guards the pair: a model carrying a method or a non-field property would\n * otherwise be read as a field and answer `undefined` to everything.\n */\n protected readonly fields = computed<readonly WrRenderedField[]>(() => {\n const columns = this.columns();\n const out: WrRenderedField[] = [];\n\n for (const [key, child] of this.field() as unknown as Iterable<[string, unknown]>) {\n if (!isFieldTree(child)) continue;\n\n const field = child as Field<never>;\n const spec = field().metadata(WR_FIELD)?.();\n if (!spec) {\n if (isDevMode()) warnOnUndrawnGroup(key, child);\n continue;\n }\n\n const label = spec.label ?? wrFieldLabel(key);\n const span = Math.min(Math.max(Math.round(spec.span ?? 1), 1), columns);\n out.push({\n key,\n field,\n spec,\n label,\n labelAbove: SELF_LABELLING.has(spec.kind) ? '' : label,\n column: `span ${span}`,\n });\n }\n\n if (isDevMode()) out.forEach(f => warnOnKindMismatch(f));\n return out;\n });\n}\n","@for (f of fields(); track f.key) {\n @let state = f.field();\n\n <!-- Angular's `hidden()` does not hide anything on its own; the docs are\n explicit that the template has to honour it. -->\n @if (!state.hidden()) {\n <div class=\"wr-schema-form__field\" [style.grid-column]=\"f.column\">\n <wr-form-field [label]=\"f.labelAbove\" [hint]=\"f.spec.hint ?? ''\" [required]=\"state.required()\">\n @switch (f.spec.kind) {\n @case ('textarea') {\n <wr-textarea [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? ''\" [ariaLabel]=\"f.label\" />\n }\n @case ('number') {\n <wr-input-number\n [formField]=\"f.field\"\n [placeholder]=\"f.spec.placeholder ?? ''\"\n [step]=\"f.spec.step ?? 1\"\n [ariaLabel]=\"f.label\"\n />\n }\n @case ('select') {\n <wr-select [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? null\" [ariaLabel]=\"f.label\">\n @for (option of f.spec.options ?? []; track option.value) {\n <wr-option [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">{{ option.label }}</wr-option>\n }\n </wr-select>\n }\n @case ('checkbox') {\n <wr-checkbox [formField]=\"f.field\">{{ f.label }}</wr-checkbox>\n }\n @case ('switch') {\n <wr-switch [formField]=\"f.field\">{{ f.label }}</wr-switch>\n }\n @case ('radio') {\n <!-- `wr-radio-group` has no `ariaLabel` input, and needs none: its\n host is a `role=\"radiogroup\"`, where `aria-label` is legal. -->\n <wr-radio-group [formField]=\"f.field\" [attr.aria-label]=\"f.label\">\n @for (option of f.spec.options ?? []; track option.value) {\n <wr-radio [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">{{ option.label }}</wr-radio>\n }\n </wr-radio-group>\n }\n @case ('date') {\n <wr-date-picker [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? ''\" [ariaLabel]=\"f.label\" />\n }\n @case ('slider') {\n <wr-slider [formField]=\"f.field\" [step]=\"f.spec.step ?? 1\" [ariaLabel]=\"f.label\" />\n }\n @default {\n <input\n wrInput\n [formField]=\"f.field\"\n [type]=\"f.spec.type ?? 'text'\"\n [placeholder]=\"f.spec.placeholder ?? ''\"\n />\n }\n }\n </wr-form-field>\n </div>\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-schema-form.mjs","names":[],"sources":["../../../projects/lib/schema-form/schema-form.ts","../../../projects/lib/schema-form/schema-form.html"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Component, ViewEncapsulation, computed, input, isDevMode } from '@angular/core';\nimport { FormField, type Field, type FieldTree, isFieldTree } from '@angular/forms/signals';\n\nimport { WrCheckbox } from 'ngwr/checkbox';\nimport { WrDatePicker } from 'ngwr/date-picker';\nimport { WR_FIELD, WrFormField, wrFieldLabel, type WrFieldSpec } from 'ngwr/form';\nimport { WrInput } from 'ngwr/input';\nimport { WrInputNumber } from 'ngwr/input-number';\nimport { WrRadio, WrRadioGroup } from 'ngwr/radio';\nimport { WrOption, WrSelect } from 'ngwr/select';\nimport { WrSlider } from 'ngwr/slider';\nimport { WrSwitch } from 'ngwr/switch';\nimport { WrTextarea } from 'ngwr/textarea';\nimport { numAttr } from 'ngwr/utils';\n\n/** One field the template draws, resolved once per schema change. */\ninterface WrRenderedField {\n readonly key: string;\n readonly field: Field<never>;\n readonly spec: WrFieldSpec;\n /** `spec.label`, or the key de-camel-cased. */\n readonly label: string;\n /**\n * The same text, or `''` for the two kinds that carry their own label inside\n * the control. Resolved here rather than in the template: a method call in a\n * template is re-run on every pass, and this is decided once per schema.\n */\n readonly labelAbove: string;\n /** `grid-column`, already clamped to the form's own `columns`. */\n readonly column: string;\n}\n\n/**\n * The two kinds that carry their own label. Everything else gets one above the\n * control, from `<wr-form-field>`.\n */\nconst SELF_LABELLING = new Set<WrFieldSpec['kind']>(['checkbox', 'switch']);\n\n/** What each kind writes into the model, for the dev-mode mismatch warning. */\nconst EXPECTED_TYPE: Partial<Record<WrFieldSpec['kind'], string>> = {\n input: 'string',\n textarea: 'string',\n number: 'number',\n checkbox: 'boolean',\n switch: 'boolean',\n slider: 'number',\n};\n\n/**\n * Warn when a group's described fields are about to be drawn by nothing.\n *\n * A field tree is iterated one level deep, so an object-valued field with no\n * `WR_FIELD` of its own is skipped exactly like an undescribed leaf — and its\n * children go with it. That is the right default (a model nests for reasons\n * that have nothing to do with a screen), but it is indistinguishable from a\n * half-drawn form: nothing is missing from the DOM, so nothing looks wrong.\n *\n * Describing the children and getting none of them is never intentional, so\n * that exact shape is what this reports. A group whose children carry no spec\n * either is silent, because it is a model detail this screen does not render.\n */\nfunction warnOnUndrawnGroup(key: string, group: unknown): void {\n // A field tree over a primitive is not iterable, and every undescribed leaf\n // arrives here — so the guard is what keeps this from throwing on the common\n // case rather than a defence against an exotic one.\n const subfields = (group as Partial<Iterable<[string, unknown]>>)[Symbol.iterator];\n if (typeof subfields !== 'function') return;\n\n let described = 0;\n for (const [, child] of group as Iterable<[string, unknown]>) {\n if (isFieldTree(child) && (child as Field<never>)().metadata(WR_FIELD)?.()) described++;\n }\n if (described === 0) return;\n\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] <wr-schema-form>: \"${key}\" is a group holding ${described} described ` +\n `field(s), and none of them are drawn — a field tree is walked one level deep. ` +\n `Draw the group with a second <wr-schema-form [field]=\"form.${key}\" />, or give ` +\n `\"${key}\" a WR_FIELD of its own if this screen is meant to skip it.`\n );\n}\n\n/**\n * Warn when a `kind` contradicts what the field actually holds.\n *\n * This is the one failure mode of the whole component that nothing else\n * reports: `kind` is a string on a metadata value, so TypeScript cannot hold it\n * against the field's own type, and a `kind: 'number'` on a `string` field\n * renders a `<wr-input-number>` that writes numbers into it forever. The runtime\n * value is the only evidence available, so an empty or nullish one says nothing\n * and is left alone.\n */\nfunction warnOnKindMismatch(f: WrRenderedField): void {\n const expected = EXPECTED_TYPE[f.spec.kind];\n const value = f.field().value();\n if (!expected || value === null || value === undefined || value === '') return;\n if (typeof value === expected) return;\n\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] <wr-schema-form>: field \"${f.key}\" is described as kind '${f.spec.kind}', ` +\n `which writes a ${expected}, but the field holds a ${typeof value}. ` +\n `The control will overwrite the model with the wrong type.`\n );\n}\n\n/**\n * A form drawn from a Signal Forms schema — one `<wr-form-field>`, one control\n * and one set of error messages per described field, laid out on a grid.\n *\n * The presentation rides on the SAME schema as the validation, through the\n * `WR_FIELD` metadata key from `ngwr/form`:\n *\n * ```ts\n * const profile = form(model, path => {\n * required(path.email, { message: 'Enter your email.' });\n * email(path.email, { message: 'That is not an email address.' });\n * metadata(path.email, WR_FIELD, () => ({ kind: 'input', type: 'email' }));\n * metadata(path.role, WR_FIELD, () => ({ kind: 'select', options: ROLES }));\n * });\n * ```\n * ```html\n * <wr-schema-form [field]=\"profile\" [columns]=\"2\" />\n * ```\n *\n * The error messages are the rules' own: each field's `<wr-form-field>` shows a\n * validator's `message`, else `validation.<key>` from the app's i18n catalog.\n * ngwr ships no error texts, so a rule with neither renders no message.\n *\n * **A field with no `WR_FIELD` metadata is skipped**, silently and on purpose: a\n * schema may describe more than one screen renders, and guessing a control from\n * a field's type would draw a `wr-switch` for every boolean flag in the model.\n * Order is the model's own key order.\n *\n * **`required`, `disabled`, `readonly` and `hidden` are NOT in the spec.** They\n * are Angular's own schema functions, `[formField]` already applies the first\n * three to the control, and this component reads `hidden()` to decide whether to\n * draw the field at all — which Angular's docs say a template has to do itself.\n * Re-declaring any of them here would be a second source of truth for state the\n * form already owns.\n *\n * **Why its own entry point rather than `ngwr/form`.** Drawing nine kinds means\n * depending on nine controls, and `ngwr/form` is the layout primitive every\n * form imports — folding this in would put a date picker and a slider into the\n * bundle of an app that only wanted `<wr-form-field>`.\n *\n * @see https://ngwr.dev/reference/components/schema-form\n */\n@Component({\n selector: 'wr-schema-form',\n templateUrl: './schema-form.html',\n encapsulation: ViewEncapsulation.None,\n imports: [\n FormField,\n WrCheckbox,\n WrDatePicker,\n WrFormField,\n WrInput,\n WrInputNumber,\n WrOption,\n WrRadio,\n WrRadioGroup,\n WrSelect,\n WrSlider,\n WrSwitch,\n WrTextarea,\n ],\n host: {\n class: 'wr-schema-form',\n '[style.grid-template-columns]': 'template()',\n },\n})\nexport class WrSchemaForm<T extends object> {\n // Generic, rather than `FieldTree<Record<string, unknown>>`, because a model\n // declared as an `interface` has no index signature and is not assignable to\n // that: every consumer would have had to declare their model as a type alias\n // just to bind it. `T extends object` still refuses a field tree over a\n // primitive, which has no subfields and would draw nothing.\n /**\n * The form to draw — the value `form()` returned, or any object-valued field\n * inside one.\n */\n readonly field = input.required<FieldTree<T>>();\n\n /**\n * Grid columns. A field spans one of them unless its spec says otherwise.\n * @default 1\n */\n readonly columns = input(1, { transform: numAttr(1) });\n\n /**\n * The track list, written as a style rather than published as a\n * `--wr-schema-form-columns` hook: `columns` always has a value, so the\n * inline style would win over any declaration a consumer wrote and the hook\n * would be one nobody could use.\n */\n protected readonly template = computed(() => `repeat(${Math.max(1, this.columns())}, minmax(0, 1fr))`);\n\n /**\n * The described fields, in the model's key order.\n *\n * A field tree over an object is iterable as `[key, field]` pairs, which is\n * the whole reason this component needs no field list of its own. `isFieldTree`\n * guards the pair: a model carrying a method or a non-field property would\n * otherwise be read as a field and answer `undefined` to everything.\n */\n protected readonly fields = computed<readonly WrRenderedField[]>(() => {\n const columns = this.columns();\n const out: WrRenderedField[] = [];\n\n for (const [key, child] of this.field() as unknown as Iterable<[string, unknown]>) {\n if (!isFieldTree(child)) continue;\n\n const field = child as Field<never>;\n const spec = field().metadata(WR_FIELD)?.();\n if (!spec) {\n if (isDevMode()) warnOnUndrawnGroup(key, child);\n continue;\n }\n\n const label = spec.label ?? wrFieldLabel(key);\n const span = Math.min(Math.max(Math.round(spec.span ?? 1), 1), columns);\n out.push({\n key,\n field,\n spec,\n label,\n labelAbove: SELF_LABELLING.has(spec.kind) ? '' : label,\n column: `span ${span}`,\n });\n }\n\n if (isDevMode()) out.forEach(f => warnOnKindMismatch(f));\n return out;\n });\n}\n","@for (f of fields(); track f.key) {\n @let state = f.field();\n\n <!-- Angular's `hidden()` does not hide anything on its own; the docs are\n explicit that the template has to honour it. -->\n @if (!state.hidden()) {\n <div class=\"wr-schema-form__field\" [style.grid-column]=\"f.column\">\n <wr-form-field [label]=\"f.labelAbove\" [hint]=\"f.spec.hint ?? ''\" [required]=\"state.required()\">\n @switch (f.spec.kind) {\n @case ('textarea') {\n <wr-textarea [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? ''\" [ariaLabel]=\"f.label\" />\n }\n @case ('number') {\n <wr-input-number\n [formField]=\"f.field\"\n [placeholder]=\"f.spec.placeholder ?? ''\"\n [step]=\"f.spec.step ?? 1\"\n [ariaLabel]=\"f.label\"\n />\n }\n @case ('select') {\n <wr-select [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? null\" [ariaLabel]=\"f.label\">\n @for (option of f.spec.options ?? []; track option.value) {\n <wr-option [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">{{ option.label }}</wr-option>\n }\n </wr-select>\n }\n @case ('checkbox') {\n <wr-checkbox [formField]=\"f.field\">{{ f.label }}</wr-checkbox>\n }\n @case ('switch') {\n <wr-switch [formField]=\"f.field\">{{ f.label }}</wr-switch>\n }\n @case ('radio') {\n <!-- `wr-radio-group` has no `ariaLabel` input, and needs none: its\n host is a `role=\"radiogroup\"`, where `aria-label` is legal. -->\n <wr-radio-group [formField]=\"f.field\" [attr.aria-label]=\"f.label\">\n @for (option of f.spec.options ?? []; track option.value) {\n <wr-radio [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">{{ option.label }}</wr-radio>\n }\n </wr-radio-group>\n }\n @case ('date') {\n <wr-date-picker [formField]=\"f.field\" [placeholder]=\"f.spec.placeholder ?? ''\" [ariaLabel]=\"f.label\" />\n }\n @case ('slider') {\n <wr-slider [formField]=\"f.field\" [step]=\"f.spec.step ?? 1\" [ariaLabel]=\"f.label\" />\n }\n @default {\n <input\n wrInput\n [formField]=\"f.field\"\n [type]=\"f.spec.type ?? 'text'\"\n [placeholder]=\"f.spec.placeholder ?? ''\"\n />\n }\n }\n </wr-form-field>\n </div>\n }\n}\n"],"mappings":"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"}
@@ -114,6 +114,22 @@ var WrSelectHarness = class WrSelectHarness extends ComponentHarness {
114
114
  const empty = await this.inPanel(".wr-select-panel__empty");
115
115
  return empty ? empty.text() : null;
116
116
  }
117
+ async getLoadErrorText() {
118
+ const row = await this.inPanel(".wr-select-panel__error");
119
+ return row ? row.text() : null;
120
+ }
121
+ async retryLoad() {
122
+ const row = await this.inPanel(".wr-select-panel__error");
123
+ const input = await this.searchInput();
124
+ const rowId = row ? await row.getAttribute("id") : null;
125
+ if (rowId && input && await input.getAttribute("aria-activedescendant") === rowId) {
126
+ await input.sendKeys(TestKey.ENTER);
127
+ return;
128
+ }
129
+ const button = await this.inPanel(".wr-select-panel__retry");
130
+ if (!button) throw new Error("WrSelectHarness: no retry to run. The loader has not failed, `retryable` is off, other options are on offer, or a wrSelectLoadError template draws its own control under `freeText`, where only a click reaches it.");
131
+ await button.click();
132
+ }
117
133
  async isLoading() {
118
134
  return await this.inPanel(".wr-select-panel__loading") !== null;
119
135
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-select-testing.mjs","names":[],"sources":["../../../projects/lib/select/testing/wr-option-harness.ts","../../../projects/lib/select/testing/wr-select-harness.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrOptionHarnessFilters } from './types';\n\n/**\n * Test harness for one option inside a `<wr-select>` panel.\n *\n * The selector is the CLASS, not the `wr-option` element: a select rendering a\n * virtualized list emits plain `<div role=\"option\" class=\"wr-option\">` rows\n * instead of components, and a spec should not have to care which one it got.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrOptionHarness extends ComponentHarness {\n /** The projected component and the virtualized row both carry this class. */\n static hostSelector = '.wr-option';\n\n /** Build a predicate that narrows the query. */\n static with(options: WrOptionHarnessFilters = {}): HarnessPredicate<WrOptionHarness> {\n return new HarnessPredicate(WrOptionHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n /**\n * The option's text as drawn, trimmed — without its leading visual.\n *\n * A `wrOptionLeading` template renders INSIDE the row, so reading the whole\n * host would hand back an avatar's initials as the first word of the name. It\n * is excluded here for the same reason the select excludes it from the label.\n *\n * This is the drawn text, not the `label` input: an option whose `label` is\n * shorter than what it draws still answers with what it draws. What the select\n * REPORTS for it is on the select harness, as `getValueText()` or\n * `getChipLabels()`.\n */\n async getText(): Promise<string> {\n return (await this.host()).text({ exclude: '.wr-option__leading' });\n }\n\n /**\n * Whether the row is drawing a leading visual right now. Reads the slot the\n * select renders the template into, so it answers `false` for an option that\n * declares a template while the panel is closed — which is the contract.\n */\n async hasLeading(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-option__leading')()) !== null;\n }\n\n /** Whether the option is currently selected. */\n async isSelected(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-selected')) === 'true';\n }\n\n /** Whether the option refuses selection. */\n async isDisabled(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-disabled')) === 'true';\n }\n\n /** Whether the option is the keyboard cursor target. */\n async isActive(): Promise<boolean> {\n return (await this.host()).hasClass('wr-option--active');\n }\n\n /**\n * Whether a search query has filtered this option out.\n *\n * Filtered options stay in the DOM — registration order has to survive a\n * query — and collapse via CSS, so a spec that counts options while a search\n * is active gets them back unless it asks. `getOptions()` on the select\n * harness already drops them.\n */\n async isHidden(): Promise<boolean> {\n return (await this.host()).hasClass('wr-option--hidden');\n }\n\n /** Click the option. */\n async click(): Promise<void> {\n return (await this.host()).click();\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport {\n ComponentHarness,\n type HarnessLoader,\n HarnessPredicate,\n TestKey,\n type TestElement,\n} from '@angular/cdk/testing';\n\nimport type { WrOptionHarnessFilters, WrSelectHarnessFilters } from './types';\nimport { WrOptionHarness } from './wr-option-harness';\n\n/**\n * Test harness for `<wr-select>` — single, multi, tag and search modes.\n *\n * The panel is NOT inside the select: it is a template portal in the overlay\n * container, a sibling of the whole app. So the options are reached through the\n * document root, scoped by the `aria-controls` id the trigger already publishes\n * — which is what keeps two selects on one page from reading each other's\n * options.\n *\n * @example\n * ```ts\n * const select = await loader.getHarness(WrSelectHarness);\n *\n * await select.open();\n * await select.selectOption({ text: 'Medium' });\n *\n * expect(await select.getValueText()).toBe('Medium');\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSelectHarness extends ComponentHarness {\n static hostSelector = 'wr-select';\n\n /** Build a predicate that narrows the query. */\n static with(options: WrSelectHarnessFilters = {}): HarnessPredicate<WrSelectHarness> {\n return new HarnessPredicate(WrSelectHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getValueText(), text))\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)\n .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open);\n }\n\n /** Whether the panel is showing. */\n async isOpen(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--open');\n }\n\n /** Whether the select refuses interaction. */\n async isDisabled(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--disabled');\n }\n\n /** Whether the select takes more than one value (multi or tag mode). */\n async isMultiple(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--multi');\n }\n\n /**\n * What the trigger currently shows as the selection: the chip labels joined by\n * `', '` in multi and tag modes, the label in single mode, the input's text in\n * search mode, and `''` when nothing is selected (the placeholder is showing).\n */\n async getValueText(): Promise<string> {\n const chips = await this.getChipLabels();\n if (chips.length > 0) return chips.join(', ');\n\n const value = await this.locatorForOptional('.wr-select__value')();\n if (value) return value.text();\n\n // Search mode keeps the display text in the input's value, not in the DOM.\n const search = await this.locatorForOptional('.wr-select__search-input')();\n return search ? search.getProperty<string>('value') : '';\n }\n\n /** The placeholder, or `null` when a selection is hiding it. */\n async getPlaceholder(): Promise<string | null> {\n const placeholder = await this.locatorForOptional('.wr-select__placeholder')();\n if (placeholder) return placeholder.text();\n\n const input = await this.searchInput();\n return input ? input.getProperty<string>('placeholder') : null;\n }\n\n /** The visible chip labels, in order. The `+N more` overflow chip is not one. */\n async getChipLabels(): Promise<string[]> {\n const labels = await this.locatorForAll('.wr-select__chip-label')();\n return Promise.all(labels.map(label => label.text()));\n }\n\n /**\n * Open the panel.\n *\n * Throws rather than resolving quietly when the panel does not appear, because\n * the two reasons it would not are both worth failing on: a tag-mode select\n * has no panel at all, and a searchable one with `minChars` opens on the query\n * rather than on focus.\n */\n async open(): Promise<void> {\n if (await this.isOpen()) return;\n\n await (await this.locatorFor('.wr-select__trigger')()).click();\n if (await this.isOpen()) return;\n\n throw new Error(\n 'WrSelectHarness.open(): the panel did not open. A tag-mode select has no panel, and a ' +\n 'searchable select with `minChars` opens on the query — call typeSearch() instead.'\n );\n }\n\n /** Close the panel. A closed select is left alone. */\n async close(): Promise<void> {\n if (!(await this.isOpen())) return;\n // Escape reaches the select through the CDK's keyboard dispatcher, so it\n // works for the button trigger and the search input alike.\n await (await this.locatorFor('.wr-select__trigger')()).sendKeys(TestKey.ESCAPE);\n }\n\n /**\n * The options currently offered, in DOM order.\n *\n * Filtered-out options are dropped: a client-side search leaves them in the\n * DOM (registration order has to survive a query) and collapses them with CSS,\n * so counting raw elements would answer with the unfiltered list.\n */\n async getOptions(filters: WrOptionHarnessFilters = {}): Promise<WrOptionHarness[]> {\n const loader = await this.panelLoader();\n const all = await loader.getAllHarnesses(WrOptionHarness.with(filters));\n\n const visible: WrOptionHarness[] = [];\n for (const option of all) {\n if (!(await option.isHidden())) visible.push(option);\n }\n return visible;\n }\n\n /** The labels of the options currently offered, in DOM order. */\n async getOptionLabels(): Promise<string[]> {\n const options = await this.getOptions();\n return Promise.all(options.map(option => option.getText()));\n }\n\n /** Open the panel if needed, then click the first option matching the filters. */\n async selectOption(filters: WrOptionHarnessFilters): Promise<void> {\n await this.open();\n\n const [option] = await this.getOptions(filters);\n if (!option) throw new Error(`WrSelectHarness.selectOption(): no option matched ${JSON.stringify(filters)}.`);\n\n await option.click();\n }\n\n /** Type into a search or tag select's input, replacing whatever is there. */\n async typeSearch(query: string): Promise<void> {\n const input = await this.searchInput();\n if (!input) throw new Error('WrSelectHarness.typeSearch(): this select has no text input.');\n\n await input.clear();\n if (query) await input.sendKeys(query);\n }\n\n /** Click the clear (×) control. It only exists on a `clearable` select with a selection. */\n async clear(): Promise<void> {\n await (await this.locatorFor('.wr-select__clear')()).click();\n }\n\n /** Remove one chip by its label. */\n async removeChip(label: string): Promise<void> {\n const labels = await this.getChipLabels();\n const index = labels.indexOf(label);\n if (index < 0) throw new Error(`WrSelectHarness.removeChip(): no chip labelled \"${label}\".`);\n\n // Label and remove control are emitted once per chip by the same loop, so\n // the two lists line up by index.\n const removes = await this.locatorForAll('.wr-select__chip-remove')();\n await removes[index].click();\n }\n\n /** The \"no results\" message, or `null` when the panel is showing options. */\n async getNoResultsText(): Promise<string | null> {\n const empty = await this.inPanel('.wr-select-panel__empty');\n return empty ? empty.text() : null;\n }\n\n /** Whether the panel is showing its async loading row. */\n async isLoading(): Promise<boolean> {\n return (await this.inPanel('.wr-select-panel__loading')) !== null;\n }\n\n /** Move keyboard focus to the select. */\n async focus(): Promise<void> {\n const input = await this.searchInput();\n return (input ?? (await this.locatorFor('.wr-select__trigger')())).focus();\n }\n\n /** The text input of a search or tag select, or `null` for the button trigger. */\n private async searchInput(): Promise<TestElement | null> {\n return this.locatorForOptional('.wr-select__search-input, .wr-select__tag-input')();\n }\n\n /** One element inside THIS select's panel, or `null`. */\n private async inPanel(selector: string): Promise<TestElement | null> {\n return this.documentRootLocatorFactory().locatorForOptional(`#${await this.panelId()} ${selector}`)();\n }\n\n /**\n * A loader scoped to THIS select's panel.\n *\n * Scoped by the `aria-controls` id rather than by `.wr-select-panel`: the\n * overlay container is shared, so a bare class query would happily return the\n * options of a different select that happens to be open.\n */\n private async panelLoader(): Promise<HarnessLoader> {\n return this.documentRootLocatorFactory().harnessLoaderFor(`#${await this.panelId()}`);\n }\n\n /** The id the trigger publishes as `aria-controls`, which the panel carries. */\n private async panelId(): Promise<string> {\n const combobox = await this.locatorForOptional('[role=\"combobox\"]')();\n const id = combobox ? await combobox.getAttribute('aria-controls') : null;\n\n if (!id) {\n throw new Error('WrSelectHarness: this select has no panel — tag mode renders its options inline.');\n }\n if (!(await this.isOpen())) {\n throw new Error('WrSelectHarness: the panel is closed — call open() first.');\n }\n\n return id;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-select-testing.mjs","names":[],"sources":["../../../projects/lib/select/testing/wr-option-harness.ts","../../../projects/lib/select/testing/wr-select-harness.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrOptionHarnessFilters } from './types';\n\n/**\n * Test harness for one option inside a `<wr-select>` panel.\n *\n * The selector is the CLASS, not the `wr-option` element: a select rendering a\n * virtualized list emits plain `<div role=\"option\" class=\"wr-option\">` rows\n * instead of components, and a spec should not have to care which one it got.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrOptionHarness extends ComponentHarness {\n /** The projected component and the virtualized row both carry this class. */\n static hostSelector = '.wr-option';\n\n /** Build a predicate that narrows the query. */\n static with(options: WrOptionHarnessFilters = {}): HarnessPredicate<WrOptionHarness> {\n return new HarnessPredicate(WrOptionHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n /**\n * The option's text as drawn, trimmed — without its leading visual.\n *\n * A `wrOptionLeading` template renders INSIDE the row, so reading the whole\n * host would hand back an avatar's initials as the first word of the name. It\n * is excluded here for the same reason the select excludes it from the label.\n *\n * This is the drawn text, not the `label` input: an option whose `label` is\n * shorter than what it draws still answers with what it draws. What the select\n * REPORTS for it is on the select harness, as `getValueText()` or\n * `getChipLabels()`.\n */\n async getText(): Promise<string> {\n return (await this.host()).text({ exclude: '.wr-option__leading' });\n }\n\n /**\n * Whether the row is drawing a leading visual right now. Reads the slot the\n * select renders the template into, so it answers `false` for an option that\n * declares a template while the panel is closed — which is the contract.\n */\n async hasLeading(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-option__leading')()) !== null;\n }\n\n /** Whether the option is currently selected. */\n async isSelected(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-selected')) === 'true';\n }\n\n /** Whether the option refuses selection. */\n async isDisabled(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-disabled')) === 'true';\n }\n\n /** Whether the option is the keyboard cursor target. */\n async isActive(): Promise<boolean> {\n return (await this.host()).hasClass('wr-option--active');\n }\n\n /**\n * Whether a search query has filtered this option out.\n *\n * Filtered options stay in the DOM — registration order has to survive a\n * query — and collapse via CSS, so a spec that counts options while a search\n * is active gets them back unless it asks. `getOptions()` on the select\n * harness already drops them.\n */\n async isHidden(): Promise<boolean> {\n return (await this.host()).hasClass('wr-option--hidden');\n }\n\n /** Click the option. */\n async click(): Promise<void> {\n return (await this.host()).click();\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport {\n ComponentHarness,\n type HarnessLoader,\n HarnessPredicate,\n TestKey,\n type TestElement,\n} from '@angular/cdk/testing';\n\nimport type { WrOptionHarnessFilters, WrSelectHarnessFilters } from './types';\nimport { WrOptionHarness } from './wr-option-harness';\n\n/**\n * Test harness for `<wr-select>` — single, multi, tag and search modes.\n *\n * The panel is NOT inside the select: it is a template portal in the overlay\n * container, a sibling of the whole app. So the options are reached through the\n * document root, scoped by the `aria-controls` id the trigger already publishes\n * — which is what keeps two selects on one page from reading each other's\n * options.\n *\n * @example\n * ```ts\n * const select = await loader.getHarness(WrSelectHarness);\n *\n * await select.open();\n * await select.selectOption({ text: 'Medium' });\n *\n * expect(await select.getValueText()).toBe('Medium');\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSelectHarness extends ComponentHarness {\n static hostSelector = 'wr-select';\n\n /** Build a predicate that narrows the query. */\n static with(options: WrSelectHarnessFilters = {}): HarnessPredicate<WrSelectHarness> {\n return new HarnessPredicate(WrSelectHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getValueText(), text))\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)\n .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open);\n }\n\n /** Whether the panel is showing. */\n async isOpen(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--open');\n }\n\n /** Whether the select refuses interaction. */\n async isDisabled(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--disabled');\n }\n\n /** Whether the select takes more than one value (multi or tag mode). */\n async isMultiple(): Promise<boolean> {\n return (await this.host()).hasClass('wr-select--multi');\n }\n\n /**\n * What the trigger currently shows as the selection: the chip labels joined by\n * `', '` in multi and tag modes, the label in single mode, the input's text in\n * search mode, and `''` when nothing is selected (the placeholder is showing).\n */\n async getValueText(): Promise<string> {\n const chips = await this.getChipLabels();\n if (chips.length > 0) return chips.join(', ');\n\n const value = await this.locatorForOptional('.wr-select__value')();\n if (value) return value.text();\n\n // Search mode keeps the display text in the input's value, not in the DOM.\n const search = await this.locatorForOptional('.wr-select__search-input')();\n return search ? search.getProperty<string>('value') : '';\n }\n\n /** The placeholder, or `null` when a selection is hiding it. */\n async getPlaceholder(): Promise<string | null> {\n const placeholder = await this.locatorForOptional('.wr-select__placeholder')();\n if (placeholder) return placeholder.text();\n\n const input = await this.searchInput();\n return input ? input.getProperty<string>('placeholder') : null;\n }\n\n /** The visible chip labels, in order. The `+N more` overflow chip is not one. */\n async getChipLabels(): Promise<string[]> {\n const labels = await this.locatorForAll('.wr-select__chip-label')();\n return Promise.all(labels.map(label => label.text()));\n }\n\n /**\n * Open the panel.\n *\n * Throws rather than resolving quietly when the panel does not appear, because\n * the two reasons it would not are both worth failing on: a tag-mode select\n * has no panel at all, and a searchable one with `minChars` opens on the query\n * rather than on focus.\n */\n async open(): Promise<void> {\n if (await this.isOpen()) return;\n\n await (await this.locatorFor('.wr-select__trigger')()).click();\n if (await this.isOpen()) return;\n\n throw new Error(\n 'WrSelectHarness.open(): the panel did not open. A tag-mode select has no panel, and a ' +\n 'searchable select with `minChars` opens on the query — call typeSearch() instead.'\n );\n }\n\n /** Close the panel. A closed select is left alone. */\n async close(): Promise<void> {\n if (!(await this.isOpen())) return;\n // Escape reaches the select through the CDK's keyboard dispatcher, so it\n // works for the button trigger and the search input alike.\n await (await this.locatorFor('.wr-select__trigger')()).sendKeys(TestKey.ESCAPE);\n }\n\n /**\n * The options currently offered, in DOM order.\n *\n * Filtered-out options are dropped: a client-side search leaves them in the\n * DOM (registration order has to survive a query) and collapses them with CSS,\n * so counting raw elements would answer with the unfiltered list.\n */\n async getOptions(filters: WrOptionHarnessFilters = {}): Promise<WrOptionHarness[]> {\n const loader = await this.panelLoader();\n const all = await loader.getAllHarnesses(WrOptionHarness.with(filters));\n\n const visible: WrOptionHarness[] = [];\n for (const option of all) {\n if (!(await option.isHidden())) visible.push(option);\n }\n return visible;\n }\n\n /** The labels of the options currently offered, in DOM order. */\n async getOptionLabels(): Promise<string[]> {\n const options = await this.getOptions();\n return Promise.all(options.map(option => option.getText()));\n }\n\n /** Open the panel if needed, then click the first option matching the filters. */\n async selectOption(filters: WrOptionHarnessFilters): Promise<void> {\n await this.open();\n\n const [option] = await this.getOptions(filters);\n if (!option) throw new Error(`WrSelectHarness.selectOption(): no option matched ${JSON.stringify(filters)}.`);\n\n await option.click();\n }\n\n /** Type into a search or tag select's input, replacing whatever is there. */\n async typeSearch(query: string): Promise<void> {\n const input = await this.searchInput();\n if (!input) throw new Error('WrSelectHarness.typeSearch(): this select has no text input.');\n\n await input.clear();\n if (query) await input.sendKeys(query);\n }\n\n /** Click the clear (×) control. It only exists on a `clearable` select with a selection. */\n async clear(): Promise<void> {\n await (await this.locatorFor('.wr-select__clear')()).click();\n }\n\n /** Remove one chip by its label. */\n async removeChip(label: string): Promise<void> {\n const labels = await this.getChipLabels();\n const index = labels.indexOf(label);\n if (index < 0) throw new Error(`WrSelectHarness.removeChip(): no chip labelled \"${label}\".`);\n\n // Label and remove control are emitted once per chip by the same loop, so\n // the two lists line up by index.\n const removes = await this.locatorForAll('.wr-select__chip-remove')();\n await removes[index].click();\n }\n\n /** The \"no results\" message, or `null` when the panel is showing options. */\n async getNoResultsText(): Promise<string | null> {\n const empty = await this.inPanel('.wr-select-panel__empty');\n return empty ? empty.text() : null;\n }\n\n /** The error row's text after a failed load, retry label included, or `null` while there is none. */\n async getLoadErrorText(): Promise<string | null> {\n const row = await this.inPanel('.wr-select-panel__error');\n return row ? row.text() : null;\n }\n\n /**\n * Runs a failed load again: Enter in the field where the field names the error\n * row as active, which works for a `wrSelectLoadError` template too, else a\n * click on the default Try again button (under `freeText`, where Enter commits\n * the text). Throws when no retry is on offer.\n */\n async retryLoad(): Promise<void> {\n const row = await this.inPanel('.wr-select-panel__error');\n const input = await this.searchInput();\n const rowId = row ? await row.getAttribute('id') : null;\n if (rowId && input && (await input.getAttribute('aria-activedescendant')) === rowId) {\n await input.sendKeys(TestKey.ENTER);\n return;\n }\n const button = await this.inPanel('.wr-select-panel__retry');\n if (!button) {\n throw new Error(\n 'WrSelectHarness: no retry to run. The loader has not failed, `retryable` is off, other options are on offer, ' +\n 'or a wrSelectLoadError template draws its own control under `freeText`, where only a click reaches it.'\n );\n }\n await button.click();\n }\n\n /** Whether the panel is showing its async loading row. */\n async isLoading(): Promise<boolean> {\n return (await this.inPanel('.wr-select-panel__loading')) !== null;\n }\n\n /** Move keyboard focus to the select. */\n async focus(): Promise<void> {\n const input = await this.searchInput();\n return (input ?? (await this.locatorFor('.wr-select__trigger')())).focus();\n }\n\n /** The text input of a search or tag select, or `null` for the button trigger. */\n private async searchInput(): Promise<TestElement | null> {\n return this.locatorForOptional('.wr-select__search-input, .wr-select__tag-input')();\n }\n\n /** One element inside THIS select's panel, or `null`. */\n private async inPanel(selector: string): Promise<TestElement | null> {\n return this.documentRootLocatorFactory().locatorForOptional(`#${await this.panelId()} ${selector}`)();\n }\n\n /**\n * A loader scoped to THIS select's panel.\n *\n * Scoped by the `aria-controls` id rather than by `.wr-select-panel`: the\n * overlay container is shared, so a bare class query would happily return the\n * options of a different select that happens to be open.\n */\n private async panelLoader(): Promise<HarnessLoader> {\n return this.documentRootLocatorFactory().harnessLoaderFor(`#${await this.panelId()}`);\n }\n\n /** The id the trigger publishes as `aria-controls`, which the panel carries. */\n private async panelId(): Promise<string> {\n const combobox = await this.locatorForOptional('[role=\"combobox\"]')();\n const id = combobox ? await combobox.getAttribute('aria-controls') : null;\n\n if (!id) {\n throw new Error('WrSelectHarness: this select has no panel — tag mode renders its options inline.');\n }\n if (!(await this.isOpen())) {\n throw new Error('WrSelectHarness: the panel is closed — call open() first.');\n }\n\n return id;\n }\n}\n"],"mappings":"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"}