ngwr 15.2.0 → 15.3.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.
- package/alert/styles/_index.scss +13 -5
- package/fesm2022/ngwr-alert.mjs +2 -2
- package/fesm2022/ngwr-alert.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +18 -2
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-hotkey.mjs +23 -1
- package/fesm2022/ngwr-hotkey.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ar.mjs +2 -0
- package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-cs.mjs +2 -0
- package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-de.mjs +2 -0
- package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +2 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-es.mjs +2 -0
- package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-fr.mjs +2 -0
- package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-he.mjs +2 -0
- package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-hi.mjs +2 -0
- package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-id.mjs +2 -0
- package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-it.mjs +2 -0
- package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ja.mjs +2 -0
- package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ko.mjs +2 -0
- package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-nl.mjs +2 -0
- package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pl.mjs +2 -0
- package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pt.mjs +2 -0
- package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +2 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-sv.mjs +2 -0
- package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-tr.mjs +2 -0
- package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-uk.mjs +2 -0
- package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-vi.mjs +2 -0
- package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh-tw.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh-tw.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +3 -0
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-pipes.mjs +5 -2
- package/fesm2022/ngwr-pipes.mjs.map +1 -1
- package/fesm2022/ngwr-schema-form.mjs +3 -0
- package/fesm2022/ngwr-schema-form.mjs.map +1 -1
- package/fesm2022/ngwr-select-testing.mjs +16 -0
- package/fesm2022/ngwr-select-testing.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +218 -38
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +9 -5
- package/fesm2022/ngwr-statistic.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/i18n/ar.json +2 -0
- package/i18n/cs.json +2 -0
- package/i18n/de.json +2 -0
- package/i18n/en.json +2 -0
- package/i18n/es.json +2 -0
- package/i18n/fr.json +2 -0
- package/i18n/he.json +2 -0
- package/i18n/hi.json +2 -0
- package/i18n/id.json +2 -0
- package/i18n/it.json +2 -0
- package/i18n/ja.json +2 -0
- package/i18n/ko.json +2 -0
- package/i18n/nl.json +2 -0
- package/i18n/pl.json +2 -0
- package/i18n/pt.json +2 -0
- package/i18n/ru.json +2 -0
- package/i18n/sv.json +2 -0
- package/i18n/tr.json +2 -0
- package/i18n/uk.json +2 -0
- package/i18n/vi.json +2 -0
- package/i18n/zh-TW.json +2 -0
- package/i18n/zh.json +2 -0
- package/llms-full.txt +3 -3
- package/package.json +1 -1
- package/schematics/use/symbol-map.json +1 -0
- package/select/styles/_index.scss +23 -1
- package/skills/ngwr/references/catalog.md +1 -1
- package/types/ngwr-alert.d.ts +3 -1
- package/types/ngwr-breadcrumbs.d.ts +15 -0
- package/types/ngwr-hotkey.d.ts +14 -2
- package/types/ngwr-select-testing.d.ts +9 -0
- package/types/ngwr-select.d.ts +133 -2
- package/types/ngwr-sidebar.d.ts +1 -1
- package/types/ngwr-version.d.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-pagination.mjs","names":[],"sources":["../../../projects/lib/pagination/pagination.ts","../../../projects/lib/pagination/pagination.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 { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { Component, LOCALE_ID, ViewEncapsulation, computed, effect, inject, input, model } from '@angular/core';\n\nimport { WrButton } from 'ngwr/button';\nimport { useI18nFormatter, useI18nText } from 'ngwr/i18n';\nimport { WrOption, WrSelect } from 'ngwr/select';\nimport { numAttr } from 'ngwr/utils';\n\nimport type { WrPaginationAlign, WrPaginationShape, WrPaginationSize } from './types';\n\nconst ELLIPSIS = '…' as const;\ntype PageEntry = number | typeof ELLIPSIS;\n\n/**\n * Numbered page navigator with optional total / page-size controls.\n *\n * Two-way binds `page` and `pageSize` via signal `model()` inputs.\n *\n * @example\n * ```html\n * <wr-pagination\n * [total]=\"120\"\n * [(page)]=\"page\"\n * [(pageSize)]=\"size\"\n * showTotal\n * showSizeChanger\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/pagination\n */\n@Component({\n selector: 'wr-pagination',\n templateUrl: './pagination.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()', role: 'navigation', '[attr.aria-label]': 'navLabel()' },\n imports: [WrButton, WrSelect, WrOption],\n})\nexport class WrPagination {\n /**\n * Currently displayed page (1-based). Two-way bindable.\n *\n * `page` rather than `currentPage` since v14, so this and `<wr-table>` — which\n * renders one of these in its own footer — spell the two concepts they share\n * one way each. It also names the output: a `model()` called `X` forces\n * `(XChange)`, so the old name produced `(currentPageChange)` against the\n * table's `(pageChange)`.\n */\n readonly page = model<number>(1);\n\n /** Items per page. Two-way bindable. */\n readonly pageSize = model<number>(10);\n\n /** Total item count across all pages. */\n readonly total = input(0, { transform: numAttr(0) });\n\n /** Options shown in the page-size dropdown. */\n readonly pageSizeOptions = input<readonly number[]>([10, 20, 50, 100]);\n\n /** Render the page-size dropdown. @default false */\n readonly showSizeChanger = input(false, { transform: coerceBooleanProperty });\n\n /** Render the \"X–Y of Z\" total label. @default false */\n readonly showTotal = input(false, { transform: coerceBooleanProperty });\n\n /** Horizontal alignment. @default 'start' */\n readonly align = input<WrPaginationAlign>('start');\n\n /** Size variant — cascades to every internal button. @default 'sm' */\n readonly size = input<WrPaginationSize>('sm');\n\n /** Cell corner treatment. @default 'rounded' */\n readonly shape = input<WrPaginationShape>('rounded');\n\n /** Disable interaction. @default false */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n\n /**\n * Collapse to a compact `‹ page / total ›` pager when the control's own box\n * is too narrow for the full numbered strip (a container query on its own\n * width, not the viewport). @default false\n */\n readonly responsive = input(false, { transform: coerceBooleanProperty });\n\n /** Previous-page button aria-label. Falls back to `pagination.prev`. */\n readonly prevLabel = input<string | null>(null);\n\n /** Next-page button aria-label. Falls back to `pagination.next`. */\n readonly nextLabel = input<string | null>(null);\n\n /** \"Items per page\" label. Falls back to `pagination.itemsPerPage`. */\n readonly itemsPerPageLabel = input<string | null>(null);\n\n /** Accessible name for the `role=\"navigation\"` host. Falls back to `pagination.label`. */\n readonly label = input<string | null>(null);\n\n /** Resolved labels. */\n protected readonly resolvedPrev = useI18nText(this.prevLabel, 'pagination.prev', 'Previous page');\n protected readonly resolvedNext = useI18nText(this.nextLabel, 'pagination.next', 'Next page');\n protected readonly resolvedItemsPerPage = useI18nText(\n this.itemsPerPageLabel,\n 'pagination.itemsPerPage',\n 'Items per page'\n );\n\n /**\n * Page-size option text — interpolates `{{size}}`. Was the template literal\n * `{{ option }} / page`, which no catalog could reach, so a Russian UI read\n * \"25 / page\".\n */\n protected readonly perPage = useI18nFormatter('pagination.perPage', '{{size}} / page');\n\n /** Per-page-button aria-label — interpolates `{{page}}`. */\n protected readonly goToPage = useI18nFormatter('pagination.goToPage', 'Go to page {{page}}');\n\n /** The nav landmark's own accessible name — was a hardcoded English literal. */\n protected readonly navLabel = useI18nText(this.label, 'pagination.label', 'Pagination');\n\n /** Internal: total page count. */\n /**\n * A page size that is not a positive number yields ONE page, not `Infinity`.\n *\n * `pageSize` is a `model()`, so Angular offers no `transform` to coerce it and\n * whatever the host writes arrives raw. At 0 the division is `Infinity`,\n * `Math.max(1, …)` keeps it, and the item list below pushes `total` as the\n * last page — so the strip rendered a clickable cell reading \"Infinity\", and\n * clicking it locked the tab. `NaN` (a `pageSize` bound to an emptied number\n * field) came through untouched for the same reason.\n *\n * One page is the honest answer: a size of zero describes no paging at all.\n * Note this is about `pageSize` and does NOT touch the recorded contract\n * about `total` — pagination still refuses to pull the page DOWN while\n * `total` is at or below zero, because 0 is ambiguous with \"still loading\".\n */\n private readonly usablePageSize = computed(() => {\n const size = this.pageSize();\n if (Number.isFinite(size) && size >= 1) return size;\n // Everything on one page. Deliberately not `Infinity`, which gives the\n // right page COUNT by accident and then puts `Infinity` in the range label\n // the moment `page` is above 1.\n return Math.max(1, this.total());\n });\n\n protected readonly totalPages = computed(() => Math.max(1, Math.ceil(this.total() / this.usablePageSize())));\n\n /**\n * Internal: where the previous arrow goes. Clamped to the end of the valid\n * range rather than being a plain `page() - 1`, because the page can\n * legitimately sit past that end while `total` is 0 (see the guard effect).\n * From page 5 of one page, `goTo(4)` is refused as out of range, so the arrow\n * was enabled and did nothing — and under `responsive` on a narrow box the\n * numbered cells are hidden by a container query, which left no way back at\n * all. `Math.min` only ever moves the target INWARD, so `goTo`'s own\n * past-the-end refusal keeps working as the backstop it is.\n */\n protected readonly prevPage = computed(() => Math.min(this.page() - 1, this.totalPages()));\n\n /**\n * Internal: the \"1–10 of 235\" line, as ONE catalog template.\n *\n * It used to be `` `${start}-${end} ${of} ${total}` ``, which localised the\n * word in the middle and nothing else: the ASCII hyphen between the bounds and\n * the operand order were both frozen in TypeScript, so ja-JP and ar-SA got\n * English word order and no language could change the range separator. The\n * three numbers go through `Intl.NumberFormat` for the same reason the total\n * beside them does — a five-figure total needs the locale's grouping, and\n * ar-SA needs its own digits.\n *\n * The `ofLabel` input is gone with it: an input for the middle word is the\n * concatenation, re-offered as API.\n */\n protected readonly rangeLabel = computed(() => {\n // The same guarded size, or the range label reads \"1 – NaN of 100\".\n const size = this.usablePageSize();\n const start = this.total() === 0 ? 0 : (this.page() - 1) * size + 1;\n const end = Math.min(this.page() * size, this.total());\n return this.rangeText({\n from: this.number(start),\n to: this.number(end),\n total: this.number(this.total()),\n });\n });\n\n /** Internal: the compact pager's `1 / 24`, likewise a catalog template. */\n protected readonly compactLabel = computed(() =>\n this.compactText({ current: this.number(this.page()), total: this.number(this.totalPages()) })\n );\n\n private readonly rangeText = useI18nFormatter('pagination.range', '{{from}}–{{to}} of {{total}}');\n private readonly compactText = useI18nFormatter('pagination.compact', '{{current}} / {{total}}');\n\n private readonly locale = inject(LOCALE_ID);\n\n /**\n * Grouped per `LOCALE_ID`. Only the range and the compact pager go through\n * this — a page NUMBER on a button is an identifier, and `1,024` on a button\n * reads as two things.\n */\n private number(value: number): string {\n return new Intl.NumberFormat(this.locale).format(value);\n }\n\n /**\n * Internal: page list with ellipses. Window of 7 visible page slots.\n *\n * 1 … 5 6 [7] 8 9 … 20\n */\n protected readonly pages = computed<readonly PageEntry[]>(() => {\n const total = this.totalPages();\n const current = this.page();\n\n if (total <= 7) {\n return Array.from({ length: total }, (_, i) => i + 1);\n }\n\n const items: PageEntry[] = [1];\n if (current > 4) items.push(ELLIPSIS);\n\n let start: number;\n let end: number;\n if (current <= 4) {\n start = 2;\n end = 5;\n } else if (current >= total - 3) {\n start = total - 4;\n end = total - 1;\n } else {\n start = current - 2;\n end = current + 2;\n }\n\n for (let i = start; i <= end; i++) items.push(i);\n if (current < total - 3) items.push(ELLIPSIS);\n if (end !== total) items.push(total);\n\n return items;\n });\n\n protected readonly ellipsis = ELLIPSIS;\n\n protected readonly classes = computed(() => {\n const parts = [\n 'wr-pagination',\n `wr-pagination--${this.align()}`,\n `wr-pagination--${this.size()}`,\n `wr-pagination--${this.shape()}`,\n ];\n if (this.disabled()) parts.push('wr-pagination--disabled');\n if (this.responsive()) parts.push('wr-pagination--responsive');\n return parts.join(' ');\n });\n\n protected isCurrent(page: number): boolean {\n return this.page() === page;\n }\n\n protected goTo(page: number | typeof ELLIPSIS): void {\n if (typeof page !== 'number') return;\n if (this.disabled() || page < 1 || page > this.totalPages() || page === this.page()) return;\n this.page.set(page);\n }\n\n /**\n * `wr-select` carries an `unknown` value by design, so the size arrives\n * untyped and is narrowed here. It used to arrive as `any` through\n * `[ngModel]`, which hid the coercion rather than removing the need for it.\n */\n constructor() {\n // `page` is a plain `model` — a `model()` takes no transform — so a\n // shrinking `total` and a host `[(page)]` write both land in it\n // unchecked. The guard used to read and write the page inside `untracked()`\n // and pull DOWNWARDS only, so it fired when `total` / `pageSize` moved and\n // never when the host wrote: past the end no cell carries `aria-current`,\n // the arrow at that end stays enabled and inert (`goTo` refuses the step\n // rather than correcting the stored page), and below 1 `rangeLabel()` counts\n // backwards (\"-9-0 of 120\"). Two-sided and tracked, the same shape\n // `wr-stepper` and `wr-carousel` use for their own host-owned `model()`.\n //\n // Only the UPPER bound is conditional. A `total` at or below 0 is the one\n // value where pulling the page down is destructive rather than cosmetic:\n // there is no `loading` input, so 0 is genuinely ambiguous between \"the\n // list is empty\" and \"the page is still in flight\", and the second is the\n // ordinary state of a server-paged host — Angular's `resource` drops its\n // value whenever the params change, and a params function returning an\n // object literal is never reference-equal, so `total` reads 0 between the\n // click and the response. `totalPages()` is 1 there, so the guard pulled\n // every page above the first back to 1 and emitted a `pageChange`\n // the host could not tell from a click: the pager could not leave page 1,\n // and each click cost two events. A negative `total` is folded into the\n // same branch because it is as meaningless as a zero one.\n //\n // The LOWER bound is unconditional, and gating it on `total` was a mistake\n // in the first cut of this fix: no value of `total` makes page 0 correct,\n // and while it was skipped a host write of -3 survived — `rangeLabel()`\n // guards `start` for an empty total and not `end`, so it read \"0--30 of 0\",\n // and the compact pager read \"-3 / 1\". It is written as one expression\n // rather than an early return so the effect depends on `page()` and\n // `total()` on every run, whichever branch decides the result.\n //\n // What IS still given up while `total` is 0 and the held page is past the\n // first: no cell carries `aria-current`, because `pages` renders the single\n // page and that is not the page the host holds. `rangeLabel()` is not in\n // that list — it already special-cases an empty total and reads \"0-0 of 0\".\n // Nor are the arrows: they compare against the RANGE rather than its ends\n // (see the template), so past the end next renders disabled and prev stays\n // enabled and steps back INTO range via `prevPage`. That is a change from\n // the \"enabled and inert\" behaviour described above, which this component\n // used to accept at both ends.\n effect(() => {\n const page = this.page();\n const floored = Math.max(1, page);\n const clamped = this.total() <= 0 ? floored : Math.min(floored, this.totalPages());\n if (clamped !== page) this.page.set(clamped);\n });\n }\n\n protected onSizeChange(size: unknown): void {\n const next = Number(size);\n if (this.disabled() || !Number.isFinite(next) || next <= 0 || next === this.pageSize()) return;\n // The size and nothing else. This used to clamp the page against the new\n // size too, and it read a value that was already superseded: `pageSize.set`\n // emits `pageSizeChange` synchronously, so by the following statement the\n // host has run its own policy — most tables reset to the first page — and\n // that write has not reached the model yet, it arrives on the next binding\n // pass. So the clamp computed a cap from the PRE-change page and wrote the\n // result over the host's decision: from page 6 at size 10, choosing 25\n // emitted `pageChange(5)` on top of the host's reset to 1, which the\n // host cannot tell from a navigation, and a second request went out. The\n // guard effect in the constructor takes it over and runs once the bindings\n // have settled, so it clamps the page the host actually wants.\n //\n // For a SETTLED `total` that is the whole story. For the server-paged host\n // this fix exists for it is not: the size change invalidates the payload\n // too, so `total` reads 0 and the guard returns early. Measured, from page\n // 6 at size 10 choosing 25 with a host that does not reset — in flight:\n // `navigations []`, page 6, total 0; after settle: `navigations [5]`, page\n // 5. So a non-resetting host issues one doomed request for a page that no\n // longer exists and is corrected only when the answer lands. That is an\n // accepted cost of refusing to write back from transient state, not\n // something the guard covers.\n this.pageSize.set(next);\n }\n}\n","<div class=\"wr-pagination__inner\">\n @if (showTotal()) {\n <span class=\"wr-pagination__total\">{{ rangeLabel() }}</span>\n }\n\n <div class=\"wr-pagination__nav\">\n <!--\n `wr-pagination__nav-btn` exists so the stylesheet can mirror the arrow\n glyph under `dir=\"rtl\"`. A class rather than `:first-child` /\n `:last-child` on the row: those two positions do hold today — which is why\n the `--square` end caps use them — but only while the prev button, the\n compact label and the next button all stay unconditional and in this\n order. A mirror keyed to position would follow a reorder, or land on a\n page cell, without a word.\n\n The two `[disabled]` bindings compare against the RANGE (`<= 1`,\n `>= totalPages()`) rather than for equality with its ends. While `total`\n is 0 the guard effect stops enforcing the upper bound, so the page can\n legitimately sit past `totalPages()` — and `=== totalPages()` reported\n next as enabled there while `goTo` refused the step. Prev is still\n enabled in that state, correctly: `prevPage()` walks it back into range.\n -->\n <wr-btn\n class=\"wr-pagination__nav-btn\"\n [size]=\"size()\"\n outlined\n [attr.aria-label]=\"resolvedPrev()\"\n [disabled]=\"disabled() || page() <= 1\"\n (click)=\"goTo(prevPage())\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m12 19-7-7 7-7\" />\n <path d=\"M19 12H5\" />\n </svg>\n </wr-btn>\n\n @for (page of pages(); track $index) {\n @if (page === ellipsis) {\n <span class=\"wr-pagination__ellipsis\">{{ ellipsis }}</span>\n } @else {\n <wr-btn\n class=\"wr-pagination__page\"\n [size]=\"size()\"\n [outlined]=\"!isCurrent(page)\"\n [color]=\"isCurrent(page) ? 'primary' : null\"\n [attr.aria-current]=\"isCurrent(page) ? 'page' : null\"\n [attr.aria-label]=\"goToPage({ page })\"\n [disabled]=\"disabled()\"\n (click)=\"goTo(page)\"\n >\n {{ page }}\n </wr-btn>\n }\n }\n\n <!-- Compact pager — shown only in responsive mode on a narrow box (CSS). Not\n `aria-hidden`: `display: none` already keeps whichever of the two is\n hidden out of the accessibility tree, and on a narrow box this label is\n the only position on screen. -->\n <span class=\"wr-pagination__current\">{{ compactLabel() }}</span>\n\n <wr-btn\n class=\"wr-pagination__nav-btn\"\n [size]=\"size()\"\n outlined\n [attr.aria-label]=\"resolvedNext()\"\n [disabled]=\"disabled() || page() >= totalPages()\"\n (click)=\"goTo(page() + 1)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M5 12h14\" />\n <path d=\"m12 5 7 7-7 7\" />\n </svg>\n </wr-btn>\n </div>\n\n @if (showSizeChanger()) {\n <!--\n `ariaLabel`, not `[attr.aria-label]`: `<wr-select>`'s host carries no role,\n so an attribute there names nothing and never reaches the `role=\"combobox\"`\n button inside — the size changer announced itself as the catalog's generic\n \"Select\".\n -->\n <wr-select\n class=\"wr-pagination__size\"\n [ariaLabel]=\"resolvedItemsPerPage()\"\n [disabled]=\"disabled()\"\n [value]=\"pageSize()\"\n (valueChange)=\"onSizeChange($event)\"\n >\n @for (option of pageSizeOptions(); track option) {\n <wr-option [value]=\"option\">{{ perPage({ size: option }) }}</wr-option>\n }\n </wr-select>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-pagination.mjs","names":[],"sources":["../../../projects/lib/pagination/pagination.ts","../../../projects/lib/pagination/pagination.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 { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { Component, LOCALE_ID, ViewEncapsulation, computed, effect, inject, input, model } from '@angular/core';\n\nimport { WrButton } from 'ngwr/button';\nimport { useI18nFormatter, useI18nText } from 'ngwr/i18n';\nimport { WrOption, WrSelect } from 'ngwr/select';\nimport { numAttr } from 'ngwr/utils';\n\nimport type { WrPaginationAlign, WrPaginationShape, WrPaginationSize } from './types';\n\nconst ELLIPSIS = '…' as const;\ntype PageEntry = number | typeof ELLIPSIS;\n\n/**\n * Numbered page navigator with optional total / page-size controls.\n *\n * Two-way binds `page` and `pageSize` via signal `model()` inputs.\n *\n * @example\n * ```html\n * <wr-pagination\n * [total]=\"120\"\n * [(page)]=\"page\"\n * [(pageSize)]=\"size\"\n * showTotal\n * showSizeChanger\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/pagination\n */\n@Component({\n selector: 'wr-pagination',\n templateUrl: './pagination.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()', role: 'navigation', '[attr.aria-label]': 'navLabel()' },\n imports: [WrButton, WrSelect, WrOption],\n})\nexport class WrPagination {\n /**\n * Currently displayed page (1-based). Two-way bindable.\n *\n * `page` rather than `currentPage` since v14, so this and `<wr-table>` — which\n * renders one of these in its own footer — spell the two concepts they share\n * one way each. It also names the output: a `model()` called `X` forces\n * `(XChange)`, so the old name produced `(currentPageChange)` against the\n * table's `(pageChange)`.\n */\n readonly page = model<number>(1);\n\n /** Items per page. Two-way bindable. */\n readonly pageSize = model<number>(10);\n\n /** Total item count across all pages. */\n readonly total = input(0, { transform: numAttr(0) });\n\n /** Options shown in the page-size dropdown. */\n readonly pageSizeOptions = input<readonly number[]>([10, 20, 50, 100]);\n\n /** Render the page-size dropdown. @default false */\n readonly showSizeChanger = input(false, { transform: coerceBooleanProperty });\n\n /** Render the \"X–Y of Z\" total label. @default false */\n readonly showTotal = input(false, { transform: coerceBooleanProperty });\n\n /** Horizontal alignment. @default 'start' */\n readonly align = input<WrPaginationAlign>('start');\n\n /** Size variant — cascades to every internal button. @default 'sm' */\n readonly size = input<WrPaginationSize>('sm');\n\n /** Cell corner treatment. @default 'rounded' */\n readonly shape = input<WrPaginationShape>('rounded');\n\n /** Disable interaction. @default false */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n\n /**\n * Collapse to a compact `‹ page / total ›` pager when the control's own box\n * is too narrow for the full numbered strip (a container query on its own\n * width, not the viewport). @default false\n */\n readonly responsive = input(false, { transform: coerceBooleanProperty });\n\n /** Previous-page button aria-label. Falls back to `pagination.prev`. */\n readonly prevLabel = input<string | null>(null);\n\n /** Next-page button aria-label. Falls back to `pagination.next`. */\n readonly nextLabel = input<string | null>(null);\n\n /** \"Items per page\" label. Falls back to `pagination.itemsPerPage`. */\n readonly itemsPerPageLabel = input<string | null>(null);\n\n /** Accessible name for the `role=\"navigation\"` host. Falls back to `pagination.label`. */\n readonly label = input<string | null>(null);\n\n /** Resolved labels. */\n protected readonly resolvedPrev = useI18nText(this.prevLabel, 'pagination.prev', 'Previous page');\n protected readonly resolvedNext = useI18nText(this.nextLabel, 'pagination.next', 'Next page');\n protected readonly resolvedItemsPerPage = useI18nText(\n this.itemsPerPageLabel,\n 'pagination.itemsPerPage',\n 'Items per page'\n );\n\n /**\n * Page-size option text — interpolates `{{size}}`. Was the template literal\n * `{{ option }} / page`, which no catalog could reach, so a Russian UI read\n * \"25 / page\".\n */\n protected readonly perPage = useI18nFormatter('pagination.perPage', '{{size}} / page');\n\n /** Per-page-button aria-label — interpolates `{{page}}`. */\n protected readonly goToPage = useI18nFormatter('pagination.goToPage', 'Go to page {{page}}');\n\n /** The nav landmark's own accessible name — was a hardcoded English literal. */\n protected readonly navLabel = useI18nText(this.label, 'pagination.label', 'Pagination');\n\n /** Internal: total page count. */\n /**\n * A page size that is not a positive number yields ONE page, not `Infinity`.\n *\n * `pageSize` is a `model()`, so Angular offers no `transform` to coerce it and\n * whatever the host writes arrives raw. At 0 the division is `Infinity`,\n * `Math.max(1, …)` keeps it, and the item list below pushes `total` as the\n * last page — so the strip rendered a clickable cell reading \"Infinity\", and\n * clicking it locked the tab. `NaN` (a `pageSize` bound to an emptied number\n * field) came through untouched for the same reason.\n *\n * One page is the honest answer: a size of zero describes no paging at all.\n * Note this is about `pageSize` and does NOT touch the recorded contract\n * about `total` — pagination still refuses to pull the page DOWN while\n * `total` is at or below zero, because 0 is ambiguous with \"still loading\".\n */\n private readonly usablePageSize = computed(() => {\n const size = this.pageSize();\n if (Number.isFinite(size) && size >= 1) return size;\n // Everything on one page. Deliberately not `Infinity`, which gives the\n // right page COUNT by accident and then puts `Infinity` in the range label\n // the moment `page` is above 1.\n return Math.max(1, this.total());\n });\n\n protected readonly totalPages = computed(() => Math.max(1, Math.ceil(this.total() / this.usablePageSize())));\n\n /**\n * Internal: where the previous arrow goes. Clamped to the end of the valid\n * range rather than being a plain `page() - 1`, because the page can\n * legitimately sit past that end while `total` is 0 (see the guard effect).\n * From page 5 of one page, `goTo(4)` is refused as out of range, so the arrow\n * was enabled and did nothing — and under `responsive` on a narrow box the\n * numbered cells are hidden by a container query, which left no way back at\n * all. `Math.min` only ever moves the target INWARD, so `goTo`'s own\n * past-the-end refusal keeps working as the backstop it is.\n */\n protected readonly prevPage = computed(() => Math.min(this.page() - 1, this.totalPages()));\n\n /**\n * Internal: the \"1–10 of 235\" line, as ONE catalog template.\n *\n * It used to be `` `${start}-${end} ${of} ${total}` ``, which localised the\n * word in the middle and nothing else: the ASCII hyphen between the bounds and\n * the operand order were both frozen in TypeScript, so ja-JP and ar-SA got\n * English word order and no language could change the range separator. The\n * three numbers go through `Intl.NumberFormat` for the same reason the total\n * beside them does — a five-figure total needs the locale's grouping, and\n * ar-SA needs its own digits.\n *\n * The `ofLabel` input is gone with it: an input for the middle word is the\n * concatenation, re-offered as API.\n */\n protected readonly rangeLabel = computed(() => {\n // The same guarded size, or the range label reads \"1 – NaN of 100\".\n const size = this.usablePageSize();\n const start = this.total() === 0 ? 0 : (this.page() - 1) * size + 1;\n const end = Math.min(this.page() * size, this.total());\n return this.rangeText({\n from: this.number(start),\n to: this.number(end),\n total: this.number(this.total()),\n });\n });\n\n /** Internal: the compact pager's `1 / 24`, likewise a catalog template. */\n protected readonly compactLabel = computed(() =>\n this.compactText({ current: this.number(this.page()), total: this.number(this.totalPages()) })\n );\n\n private readonly rangeText = useI18nFormatter('pagination.range', '{{from}}–{{to}} of {{total}}');\n private readonly compactText = useI18nFormatter('pagination.compact', '{{current}} / {{total}}');\n\n private readonly locale = inject(LOCALE_ID);\n\n /**\n * Grouped per `LOCALE_ID`. Only the range and the compact pager go through\n * this — a page NUMBER on a button is an identifier, and `1,024` on a button\n * reads as two things.\n */\n private number(value: number): string {\n return new Intl.NumberFormat(this.locale).format(value);\n }\n\n /**\n * Internal: page list with ellipses. Window of 7 visible page slots.\n *\n * 1 … 5 6 [7] 8 9 … 20\n */\n protected readonly pages = computed<readonly PageEntry[]>(() => {\n const total = this.totalPages();\n const current = this.page();\n\n if (total <= 7) {\n return Array.from({ length: total }, (_, i) => i + 1);\n }\n\n const items: PageEntry[] = [1];\n if (current > 4) items.push(ELLIPSIS);\n\n let start: number;\n let end: number;\n if (current <= 4) {\n start = 2;\n end = 5;\n } else if (current >= total - 3) {\n start = total - 4;\n end = total - 1;\n } else {\n start = current - 2;\n end = current + 2;\n }\n\n for (let i = start; i <= end; i++) items.push(i);\n if (current < total - 3) items.push(ELLIPSIS);\n if (end !== total) items.push(total);\n\n return items;\n });\n\n protected readonly ellipsis = ELLIPSIS;\n\n protected readonly classes = computed(() => {\n const parts = [\n 'wr-pagination',\n `wr-pagination--${this.align()}`,\n `wr-pagination--${this.size()}`,\n `wr-pagination--${this.shape()}`,\n ];\n if (this.disabled()) parts.push('wr-pagination--disabled');\n if (this.responsive()) parts.push('wr-pagination--responsive');\n return parts.join(' ');\n });\n\n protected isCurrent(page: number): boolean {\n return this.page() === page;\n }\n\n protected goTo(page: number | typeof ELLIPSIS): void {\n if (typeof page !== 'number') return;\n if (this.disabled() || page < 1 || page > this.totalPages() || page === this.page()) return;\n this.page.set(page);\n }\n\n /**\n * `wr-select` carries an `unknown` value by design, so the size arrives\n * untyped and is narrowed here. It used to arrive as `any` through\n * `[ngModel]`, which hid the coercion rather than removing the need for it.\n */\n constructor() {\n // `page` is a plain `model` — a `model()` takes no transform — so a\n // shrinking `total` and a host `[(page)]` write both land in it\n // unchecked. The guard used to read and write the page inside `untracked()`\n // and pull DOWNWARDS only, so it fired when `total` / `pageSize` moved and\n // never when the host wrote: past the end no cell carries `aria-current`,\n // the arrow at that end stays enabled and inert (`goTo` refuses the step\n // rather than correcting the stored page), and below 1 `rangeLabel()` counts\n // backwards (\"-9-0 of 120\"). Two-sided and tracked, the same shape\n // `wr-stepper` and `wr-carousel` use for their own host-owned `model()`.\n //\n // Only the UPPER bound is conditional. A `total` at or below 0 is the one\n // value where pulling the page down is destructive rather than cosmetic:\n // there is no `loading` input, so 0 is genuinely ambiguous between \"the\n // list is empty\" and \"the page is still in flight\", and the second is the\n // ordinary state of a server-paged host — Angular's `resource` drops its\n // value whenever the params change, and a params function returning an\n // object literal is never reference-equal, so `total` reads 0 between the\n // click and the response. `totalPages()` is 1 there, so the guard pulled\n // every page above the first back to 1 and emitted a `pageChange`\n // the host could not tell from a click: the pager could not leave page 1,\n // and each click cost two events. A negative `total` is folded into the\n // same branch because it is as meaningless as a zero one.\n //\n // The LOWER bound is unconditional, and gating it on `total` was a mistake\n // in the first cut of this fix: no value of `total` makes page 0 correct,\n // and while it was skipped a host write of -3 survived — `rangeLabel()`\n // guards `start` for an empty total and not `end`, so it read \"0--30 of 0\",\n // and the compact pager read \"-3 / 1\". It is written as one expression\n // rather than an early return so the effect depends on `page()` and\n // `total()` on every run, whichever branch decides the result.\n //\n // What IS still given up while `total` is 0 and the held page is past the\n // first: no cell carries `aria-current`, because `pages` renders the single\n // page and that is not the page the host holds. `rangeLabel()` is not in\n // that list — it already special-cases an empty total and reads \"0-0 of 0\".\n // Nor are the arrows: they compare against the RANGE rather than its ends\n // (see the template), so past the end next renders disabled and prev stays\n // enabled and steps back INTO range via `prevPage`. That is a change from\n // the \"enabled and inert\" behaviour described above, which this component\n // used to accept at both ends.\n effect(() => {\n const page = this.page();\n const floored = Math.max(1, page);\n const clamped = this.total() <= 0 ? floored : Math.min(floored, this.totalPages());\n if (clamped !== page) this.page.set(clamped);\n });\n }\n\n protected onSizeChange(size: unknown): void {\n const next = Number(size);\n if (this.disabled() || !Number.isFinite(next) || next <= 0 || next === this.pageSize()) return;\n // The size and nothing else. This used to clamp the page against the new\n // size too, and it read a value that was already superseded: `pageSize.set`\n // emits `pageSizeChange` synchronously, so by the following statement the\n // host has run its own policy — most tables reset to the first page — and\n // that write has not reached the model yet, it arrives on the next binding\n // pass. So the clamp computed a cap from the PRE-change page and wrote the\n // result over the host's decision: from page 6 at size 10, choosing 25\n // emitted `pageChange(5)` on top of the host's reset to 1, which the\n // host cannot tell from a navigation, and a second request went out. The\n // guard effect in the constructor takes it over and runs once the bindings\n // have settled, so it clamps the page the host actually wants.\n //\n // For a SETTLED `total` that is the whole story. For the server-paged host\n // this fix exists for it is not: the size change invalidates the payload\n // too, so `total` reads 0 and the guard returns early. Measured, from page\n // 6 at size 10 choosing 25 with a host that does not reset — in flight:\n // `navigations []`, page 6, total 0; after settle: `navigations [5]`, page\n // 5. So a non-resetting host issues one doomed request for a page that no\n // longer exists and is corrected only when the answer lands. That is an\n // accepted cost of refusing to write back from transient state, not\n // something the guard covers.\n this.pageSize.set(next);\n }\n}\n","<div class=\"wr-pagination__inner\">\n @if (showTotal()) {\n <span class=\"wr-pagination__total\">{{ rangeLabel() }}</span>\n }\n\n <div class=\"wr-pagination__nav\">\n <!--\n `wr-pagination__nav-btn` exists so the stylesheet can mirror the arrow\n glyph under `dir=\"rtl\"`. A class rather than `:first-child` /\n `:last-child` on the row: those two positions do hold today — which is why\n the `--square` end caps use them — but only while the prev button, the\n compact label and the next button all stay unconditional and in this\n order. A mirror keyed to position would follow a reorder, or land on a\n page cell, without a word.\n\n The two `[disabled]` bindings compare against the RANGE (`<= 1`,\n `>= totalPages()`) rather than for equality with its ends. While `total`\n is 0 the guard effect stops enforcing the upper bound, so the page can\n legitimately sit past `totalPages()` — and `=== totalPages()` reported\n next as enabled there while `goTo` refused the step. Prev is still\n enabled in that state, correctly: `prevPage()` walks it back into range.\n -->\n <wr-btn\n class=\"wr-pagination__nav-btn\"\n [size]=\"size()\"\n outlined\n [attr.aria-label]=\"resolvedPrev()\"\n [disabled]=\"disabled() || page() <= 1\"\n (click)=\"goTo(prevPage())\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m12 19-7-7 7-7\" />\n <path d=\"M19 12H5\" />\n </svg>\n </wr-btn>\n\n @for (page of pages(); track $index) {\n @if (page === ellipsis) {\n <span class=\"wr-pagination__ellipsis\">{{ ellipsis }}</span>\n } @else {\n <wr-btn\n class=\"wr-pagination__page\"\n [size]=\"size()\"\n [outlined]=\"!isCurrent(page)\"\n [color]=\"isCurrent(page) ? 'primary' : null\"\n [attr.aria-current]=\"isCurrent(page) ? 'page' : null\"\n [attr.aria-label]=\"goToPage({ page })\"\n [disabled]=\"disabled()\"\n (click)=\"goTo(page)\"\n >\n {{ page }}\n </wr-btn>\n }\n }\n\n <!-- Compact pager — shown only in responsive mode on a narrow box (CSS). Not\n `aria-hidden`: `display: none` already keeps whichever of the two is\n hidden out of the accessibility tree, and on a narrow box this label is\n the only position on screen. -->\n <span class=\"wr-pagination__current\">{{ compactLabel() }}</span>\n\n <wr-btn\n class=\"wr-pagination__nav-btn\"\n [size]=\"size()\"\n outlined\n [attr.aria-label]=\"resolvedNext()\"\n [disabled]=\"disabled() || page() >= totalPages()\"\n (click)=\"goTo(page() + 1)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M5 12h14\" />\n <path d=\"m12 5 7 7-7 7\" />\n </svg>\n </wr-btn>\n </div>\n\n @if (showSizeChanger()) {\n <!--\n `ariaLabel`, not `[attr.aria-label]`: `<wr-select>`'s host carries no role,\n so an attribute there names nothing and never reaches the `role=\"combobox\"`\n button inside — the size changer announced itself as the catalog's generic\n \"Select\".\n -->\n <wr-select\n class=\"wr-pagination__size\"\n [ariaLabel]=\"resolvedItemsPerPage()\"\n [disabled]=\"disabled()\"\n [value]=\"pageSize()\"\n (valueChange)=\"onSizeChange($event)\"\n >\n @for (option of pageSizeOptions(); track option) {\n <wr-option [value]=\"option\">{{ perPage({ size: option }) }}</wr-option>\n }\n </wr-select>\n }\n</div>\n"],"mappings":"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"}
|
package/fesm2022/ngwr-pipes.mjs
CHANGED
|
@@ -162,14 +162,17 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
162
162
|
* Use of this source code is governed by an MIT-style license that can be
|
|
163
163
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
164
164
|
*/
|
|
165
|
+
const PLAIN_DECIMAL = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
165
166
|
var WrNumber = class WrNumber {
|
|
166
167
|
locale = inject(LOCALE_ID);
|
|
167
168
|
transform(value, styleOrOptions = "decimal", currency = "USD") {
|
|
168
169
|
if (value === null || value === void 0 || value === "") return "";
|
|
169
|
-
const
|
|
170
|
+
const text = typeof value === "string" ? value.trim() : null;
|
|
171
|
+
const n = Number(text ?? value);
|
|
170
172
|
if (!Number.isFinite(n)) return "";
|
|
171
173
|
const options = typeof styleOrOptions === "string" ? this.toOptions(styleOrOptions, currency) : styleOrOptions;
|
|
172
|
-
|
|
174
|
+
const exact = text !== null && PLAIN_DECIMAL.test(text) ? text : n;
|
|
175
|
+
return new Intl.NumberFormat(this.locale, options).format(exact);
|
|
173
176
|
}
|
|
174
177
|
toOptions(style, currency) {
|
|
175
178
|
switch (style) {
|
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\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 `&`, 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 & 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/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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\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 `&`, 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 & 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\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 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' ? this.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(this.locale, options).format(exact);\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 +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":";;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,iCAAiB,IAAI,IAAyB,CAAC,YAAY,QAAQ,CAAC;AAG1E,MAAM,gBAA8D;CAClE,OAAO;CACP,UAAU;CACV,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,QAAQ;AACT;AAeD,SAAS,mBAAmB,KAAa,OAAc;CAKrD,IAAI,OADe,MAA+C,OAAO,cAChD,YAAY;CAErC,IAAI,YAAY;CAChB,KAAK,MAAM,GAAG,UAAU,OACtB,IAAI,YAAY,KAAK,KAAM,MAAsB,CAAE,CAAC,SAAS,QAAQ,CAAC,GAAE,GAAI;CAE9E,IAAI,cAAc,GAAG;CAGrB,QAAQ,KACN,6BAA6B,IAAG,uBAAwB,UAAS,sJAED,IAAG,iBAC7D,IAAG,4DAA6D;AAE1E;AAYA,SAAS,mBAAmB,GAAkB;CAC5C,MAAM,WAAW,cAAc,EAAE,KAAK;CACtC,MAAM,QAAQ,EAAE,MAAK,CAAE,CAAC,MAAK;CAC7B,IAAI,CAAC,YAAY,UAAU,QAAQ,UAAU,UAAa,UAAU,IAAI;CACxE,IAAI,OAAO,UAAU,UAAU;CAG/B,QAAQ,KACN,mCAAmC,EAAE,IAAG,0BAA2B,EAAE,KAAK,KAAI,oBAC1D,SAAQ,0BAA2B,OAAO,MAAK,4DACN;AAEjE;AAoEA,IAAa,eAAb,MAAa,aAAY;CAUd,QAAQ,MAAM,SAAwB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAMtC,UAAU,MAAM,GAAC;EAAA,GAAA,YAAA,EAAA,WAAA,UAAA,+BAAA,CAAA;EAAI,WAAW,QAAQ,CAAC;CAAC,CAAA;CAQhC,WAAW,eAAe,UAAU,KAAK,IAAI,GAAG,KAAK,QAAO,CAAE,EAAC,oBAAoB,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAUnF,SAAS,eAA0C;EACpE,MAAM,UAAU,KAAK,QAAO;EAC5B,MAAM,MAAyB,CAAA;EAE/B,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,MAAK,GAA8C;GACjF,IAAI,CAAC,YAAY,KAAK,GAAG;GAEzB,MAAM,QAAQ;GACd,MAAM,OAAO,MAAK,CAAE,CAAC,SAAS,QAAQ,CAAC,GAAE;GACzC,IAAI,CAAC,MAAM;IACT,IAAI,UAAS,GAAI,mBAAmB,KAAK,KAAK;IAC9C;GACF;GAEA,MAAM,QAAQ,KAAK,SAAS,aAAa,GAAG;GAC5C,MAAM,OAAO,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO;GACtE,IAAI,KAAK;IACP;IACA;IACA;IACA;IACA,YAAY,eAAe,IAAI,KAAK,IAAI,IAAI,KAAK;IACjD,QAAQ,QAAQ;GACjB,CAAA;EACH;EAEA,IAAI,UAAS,GAAI,IAAI,SAAQ,MAAK,mBAAmB,CAAC,CAAC;EACvD,OAAO;CACT,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CA9DS,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAY,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAZ,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAY,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA;GAAA,YAAA,EAAA,+BAAA,aAAA;GAAA,gBAAA;EAAA;EAAA,UAAA;EAAA,UCpLzB;EA6DA,cAAA;GAAA;IAAA,MAAA;IAAA,MDoGI;IAAS,UAAA;IAAA,QAAA,CAAA,WAAA;IAAA,UAAA,CAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACT;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,iBAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACV;IAAY,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACZ;IAAW,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACX;IAAO,UAAA;IAAA,QAAA,CAAA,QAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACP;IAAa,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACR;IAAO,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACP;IAAY,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACZ;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACR;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACR;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,iBAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACR;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA,OAAA;GAAA;EAAA;EAAA,eAAA,GAAA,kBAAA;CAAA,CAAA;;AAOD,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAY,YAAA,CAAA;EAxBxB,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAgB,eAEX,kBAAkB;GAAI,SAC5B;IACP;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACD;GAAA,MACK;IACJ,OAAO;IACP,iCAAiC;GAClC;GAAA,UAAA;EAAA,CAAA"}
|
|
@@ -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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"}
|