ngwr 14.5.0 → 14.6.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/README.md +9 -3
- package/avatar/styles/_index.scss +46 -0
- package/fesm2022/ngwr-avatar.mjs +58 -7
- package/fesm2022/ngwr-avatar.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +16 -5
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +13 -0
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-image-cropper-testing.mjs +34 -3
- package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -1
- package/fesm2022/ngwr-image-cropper.mjs +98 -5
- package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +1 -1
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-schema-form.mjs +1 -1
- package/fesm2022/ngwr-schema-form.mjs.map +1 -1
- package/fesm2022/ngwr-select-testing.mjs +21 -2
- package/fesm2022/ngwr-select-testing.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +233 -35
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +10 -2
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/image-cropper/styles/_index.scss +7 -0
- package/llms-full.txt +5 -5
- package/llms.txt +12 -3
- package/mcp/providers.js +8 -2
- package/package.json +1 -1
- package/schematics/migrations/v14/index.js +90 -34
- package/schematics/migrations/v7/index.js +15 -4
- package/schematics/migrations/v8/index.js +13 -2
- package/schematics/migrations/v9/index.js +19 -10
- package/schematics/use/symbol-map.json +1 -0
- package/select/styles/_index.scss +34 -0
- package/skills/ngwr/SKILL.md +9 -4
- package/skills/ngwr/references/catalog.md +1 -1
- package/skills/ngwr/references/setup.md +2 -2
- package/table/styles/_index.scss +39 -12
- package/types/ngwr-avatar.d.ts +36 -3
- package/types/ngwr-i18n-en.d.ts +16 -5
- package/types/ngwr-i18n.d.ts +13 -0
- package/types/ngwr-image-cropper-testing.d.ts +25 -3
- package/types/ngwr-image-cropper.d.ts +88 -2
- package/types/ngwr-select-testing.d.ts +18 -1
- package/types/ngwr-select.d.ts +181 -10
- package/types/ngwr-table.d.ts +8 -0
- package/types/ngwr-version.d.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-i18n.mjs","sources":["../../../projects/lib/i18n/i18n-base-catalogs.ts","../../../projects/lib/i18n/i18n-config.ts","../../../projects/lib/i18n/i18n-loader.ts","../../../projects/lib/i18n/i18n.ts","../../../projects/lib/i18n/t.pipe.ts","../../../projects/lib/i18n/t.directive.ts","../../../projects/lib/i18n/loaders/static-loader.ts","../../../projects/lib/i18n/loaders/http-loader.ts","../../../projects/lib/i18n/provide-wr-i18n.ts","../../../projects/lib/i18n/util.ts","../../../projects/lib/i18n/ngwr-i18n.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 { InjectionToken } from '@angular/core';\n\nimport { type WrI18nCatalog } from './i18n-config';\n\n/** Base catalogs keyed by locale, contributed by {@link provideWrI18nBaseCatalogs}. */\nexport type WrI18nBaseCatalogs = Readonly<Record<string, WrI18nCatalog>>;\n\n/**\n * Catalogs consulted *underneath* whatever the loader returned — the place for\n * ngwr's own built-in strings.\n *\n * A loader **replaces** the catalog for a locale rather than extending it, so an\n * app whose JSON holds only its own keys used to lose every built-in label to\n * its hardcoded English fallback, silently. Registering `wrRu` / `wrEn` here\n * puts them back without the app having to merge anything itself.\n *\n * `multi`, so several features can each contribute; later contributions win on\n * a key collision, and anything the loader returned still wins over all of them.\n *\n * It has to be handed in by the consumer rather than wired up inside\n * `provideWrI18n()`: the catalogs are their own entry points (`ngwr/i18n/ru`)\n * and they already import from `ngwr/i18n`, so importing them back here is a\n * circular entry-point dependency that ng-packagr rejects outright. Passing\n * them in also keeps the locales you don't ship out of your bundle.\n */\nexport const WR_I18N_BASE_CATALOGS = new InjectionToken<readonly WrI18nBaseCatalogs[]>('WR_I18N_BASE_CATALOGS');\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 { InjectionToken, LOCALE_ID, inject } from '@angular/core';\n\n/** Translation catalog — flat or nested record of strings. */\nexport interface WrI18nCatalog {\n readonly [key: string]: string | WrI18nCatalog;\n}\n\n/** Parameters passed to interpolation — coerced to strings via `String(v)`. */\nexport type WrI18nParams = Readonly<Record<string, unknown>>;\n\n/** Hook called once on every key that resolves to nothing. */\nexport type WrI18nMissingHandler = (key: string, locale: string) => string;\n\n/** User-facing config for {@link provideWrI18n}. */\nexport interface WrI18nConfig {\n /**\n * BCP-47 tag used until the user calls `use()`.\n *\n * Omitted, it is Angular's own `LOCALE_ID` — the app already states its\n * locale there, and ngwr having a second, unrelated default is how a fully\n * Russian app came to render an English calendar inside itself. See\n * {@link WrI18n} for the whole precedence.\n *\n * @default LOCALE_ID\n */\n readonly defaultLocale?: string;\n\n /**\n * Whitelisted locales — `use()` ignores anything outside this list. Omitted,\n * it is `[defaultLocale]`: it used to be a hardcoded `['en']`, so\n * `provideWrI18n({ defaultLocale: 'ru' })` produced a service whose own\n * default locale was not switchable back to.\n *\n * @default [defaultLocale]\n */\n readonly availableLocales?: readonly string[];\n\n /**\n * Storage key for persisting the active locale (via `WrStorage`).\n * Set to `null` to disable persistence. @default 'wr:i18n:locale'\n */\n readonly storageKey?: string | null;\n\n /** Called when a key is missing. Default returns the key itself. */\n readonly missingHandler?: WrI18nMissingHandler;\n}\n\n/** Resolved config — every field non-optional. @internal */\nexport interface WrI18nConfigResolved {\n readonly defaultLocale: string;\n readonly availableLocales: readonly string[];\n readonly storageKey: string | null;\n readonly missingHandler: WrI18nMissingHandler;\n}\n\n/**\n * Everything but the locale, which cannot be a constant: it is resolved from\n * `LOCALE_ID` in an injection context. @internal\n */\nexport const DEFAULT_WR_I18N_CONFIG: Omit<WrI18nConfigResolved, 'defaultLocale' | 'availableLocales'> = {\n storageKey: 'wr:i18n:locale',\n missingHandler: key => key,\n};\n\n/**\n * Resolve the locale half of the config. Callable only from an injection\n * context, because the seed is `LOCALE_ID`. @internal\n */\nexport function resolveWrI18nLocales(\n config: WrI18nConfig\n): Pick<WrI18nConfigResolved, 'defaultLocale' | 'availableLocales'> {\n const defaultLocale = config.defaultLocale ?? inject(LOCALE_ID);\n return { defaultLocale, availableLocales: config.availableLocales ?? [defaultLocale] };\n}\n\nexport const WR_I18N_CONFIG = new InjectionToken<WrI18nConfigResolved>('WR_I18N_CONFIG', {\n factory: () => ({ ...DEFAULT_WR_I18N_CONFIG, ...resolveWrI18nLocales({}) }),\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 { InjectionToken } from '@angular/core';\n\nimport { of, type Observable } from 'rxjs';\n\nimport { type WrI18nCatalog } from './i18n-config';\n\n/**\n * Loads a catalog for a `(locale, scope)` pair. `scope` is `null` for the\n * root catalog. Implementations may return the catalog synchronously\n * (via `of(...)`) for static catalogs or asynchronously (via HTTP) for\n * lazy fetches.\n */\nexport interface WrI18nLoader {\n load(locale: string, scope: string | null): Observable<WrI18nCatalog>;\n}\n\n/**\n * The active loader.\n *\n * Defaults to one that serves an empty catalog, which is what makes\n * `provideWrI18n()` genuinely optional: every ngwr component routes its\n * built-in strings through `useI18nText` / `readI18nText`, those inject\n * `WrI18n` optionally — and because `WrI18n` is `providedIn: 'root'`, an\n * optional inject still CONSTRUCTS it. Without a default here that construction\n * threw `NG0201: No provider found for WR_I18N_LOADER`, so an app that had\n * simply not configured i18n could not render `<wr-alert>` at all.\n *\n * With the empty catalog, every lookup misses, `t()` hands back the key, and\n * the helpers fall through to the component's own English default — the\n * documented no-i18n behaviour.\n */\nexport const WR_I18N_LOADER = new InjectionToken<WrI18nLoader>('WR_I18N_LOADER', {\n providedIn: 'root',\n factory: () => ({ load: () => of({}) }),\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, Service, type Signal, computed, effect, inject, isDevMode, signal } from '@angular/core';\n\nimport { type Observable, firstValueFrom } from 'rxjs';\n\nimport { WrStorage } from 'ngwr/storage';\n\nimport { WR_I18N_BASE_CATALOGS } from './i18n-base-catalogs';\nimport { WR_I18N_CONFIG, type WrI18nCatalog, type WrI18nParams } from './i18n-config';\nimport { WR_I18N_LOADER } from './i18n-loader';\n\n/** Cache key shape: `<locale>::<scope|''>`. @internal */\nfunction cacheKey(locale: string, scope: string | null): string {\n return `${locale}::${scope ?? ''}`;\n}\n\n/**\n * A locale and the language under it: `'ru-RU'` gives `['ru-RU', 'ru']`.\n *\n * Every lookup and every load walks this, so a catalog registered as `ru`\n * answers an app running `ru-RU`. Without it the two spellings are unrelated\n * strings, and the miss is silent — `t()` returns the key, `useI18nText` reads\n * that as \"missing\" and serves the English fallback, so a fully translated app\n * renders English with nothing logged. It only ever truncates: an `en` lookup\n * never reaches an `en-US` catalog, because a language is not a region.\n *\n * @internal\n */\nfunction localeChain(locale: string): readonly string[] {\n const dash = locale.indexOf('-');\n return dash > 0 ? [locale, locale.slice(0, dash)] : [locale];\n}\n\n/** Walk a dotted path through a catalog tree. Returns string | null. @internal */\nfunction walk(cat: WrI18nCatalog | null, key: string): string | null {\n if (!cat) return null;\n let node: string | WrI18nCatalog | undefined = cat;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return null;\n node = node[part];\n }\n return typeof node === 'string' ? node : null;\n}\n\n/**\n * `{{name}}` interpolation. Missing params resolve to `''`. Exported so\n * helpers in `util.ts` can interpolate the fallback template when no\n * `WrI18n` provider is wired up.\n *\n * @internal\n */\nexport function wrInterpolate(template: string, params: WrI18nParams | undefined): string {\n if (!params) return template;\n return template.replace(/\\{\\{\\s*([\\w.-]+)\\s*\\}\\}/g, (_match, name: string) => {\n const v = params[name];\n if (v === undefined || v === null) return '';\n if (typeof v === 'string') return v;\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n try {\n return JSON.stringify(v);\n } catch {\n return '';\n }\n });\n}\n\n/**\n * Reactive i18n service. Holds:\n *\n * - the active locale as a `Signal<string>`\n * - per-`(locale, scope)` catalogs in a `Map` (loaded once, then cached)\n * - a per-locale signal counter so every `translate()` reading the signal\n * re-evaluates the moment a new catalog finishes loading\n *\n * Translations are looked up scope-first then root, per locale, and the locale\n * itself is tried as the full tag before the bare language underneath it — so\n * an app running `ru-RU` resolves against a catalog registered as `ru`, and a\n * regional catalog is never shadowed by a scope in the base language. For an\n * app that spells its locale one way, which is every app that existed before\n * the chain, the order is exactly what it always was.\n *\n * ## Which locale wins\n *\n * ngwr has three locale inputs and they used to be three unrelated answers — an\n * app that set `LOCALE_ID: 'ru-RU'` correctly still rendered an English\n * calendar, because the date token defaulted to `navigator.language`. The\n * precedence is now stated, highest first:\n *\n * 1. **A subsystem's own explicit option.** `provideWrDateAdapter({ locale })`\n * pins dates; `provideWrI18n({ defaultLocale })` pins the catalog. The most\n * specific statement wins, because it can only have been deliberate.\n * 2. **`WrI18n.locale()` — the runtime value.** The only one of the three that\n * can change without a reload, so it is what a language switcher writes.\n * 3. **`LOCALE_ID`.** Angular's app-wide constant, and the seed for both of the\n * above when neither was given. One statement, one answer.\n * 4. `'en-US'`, which is what `LOCALE_ID` itself falls back to.\n *\n * `navigator.language` is deliberately NOT in that list. It is the browser's\n * preference, not the application's, and reading it made prerendered output and\n * the hydrated page disagree.\n *\n * **What a runtime switch reaches, and what it does not.** `use()` repaints\n * every catalog string. It does not move `LOCALE_ID`, which is a constant, so\n * anything keyed on it — `wrDate` / `wrNumber` / `wrPlural`, `wr-input-number`,\n * `wr-statistic`, `wr-counter` — and anything keyed on `WR_DATE_LOCALE` — the\n * date adapter, and so `wr-calendar` and `wr-date-picker` — stays where it was.\n * `use()` says so once in dev mode rather than leaving it to be discovered.\n *\n * @example\n * ```ts\n * const i18n = inject(WrI18n);\n * i18n.use('ru'); // switch locale\n * i18n.t('cart.checkout'); // → 'Оформить заказ'\n * i18n.t('greeting', { name: 'Ada' }); // → 'Hello, Ada!'\n *\n * effect(() => console.log(i18n.translate('hi')())); // re-runs on locale change\n * ```\n *\n * @see https://ngwr.dev/reference/services/i18n\n */\n@Service()\nexport class WrI18n {\n private readonly config = inject(WR_I18N_CONFIG);\n private readonly loader = inject(WR_I18N_LOADER);\n private readonly storage = inject(WrStorage);\n\n /**\n * Angular's own locale. Read for the dev-mode drift warning only — the\n * resolved default already folded it in through `WR_I18N_CONFIG`. @internal\n */\n private readonly localeId = inject(LOCALE_ID);\n\n /**\n * Catalogs registered via `provideWrI18nBaseCatalogs()` — the floor under the\n * loader's own catalogs, so ngwr's built-in strings survive an app that only\n * ships its own keys. @internal\n */\n private readonly baseCatalogs = inject(WR_I18N_BASE_CATALOGS, { optional: true }) ?? [];\n\n /** Per-(locale,scope) catalogs, populated by `loadCatalog`. @internal */\n private readonly catalogs = new Map<string, WrI18nCatalog>();\n\n /** Bumped after each catalog write so reactive translates recompute. @internal */\n private readonly revision = signal(0);\n\n /** In-flight load promises — dedupes parallel `t()` callers. @internal */\n private readonly inflight = new Map<string, Promise<WrI18nCatalog>>();\n\n /** Registered scopes — auto-loaded whenever the locale changes. @internal */\n private readonly scopes = new Set<string>();\n\n /** Active locale. Writes go through `use()` so storage stays in sync. */\n private readonly _locale = signal<string>(this.initialLocale());\n readonly locale: Signal<string> = this._locale.asReadonly();\n\n constructor() {\n // Eagerly load the root catalog + every registered scope for the\n // current locale. Subsequent locale changes refire the same effect.\n effect(() => {\n const lc = this._locale();\n void this.loadChain(lc, null);\n for (const sc of this.scopes) void this.loadChain(lc, sc);\n });\n }\n\n // Locale\n\n /**\n * Switch active locale. Ignored when not whitelisted.\n *\n * Text only — see the class docs for what a runtime switch cannot reach. The\n * dev-mode warning below is the whole reporting of that limit; without it the\n * failure is a screen showing German labels beside `3/15/2026`, with nothing\n * anywhere saying why.\n */\n use(locale: string): void {\n if (!this.config.availableLocales.includes(locale)) return;\n const previous = this._locale();\n this._locale.set(locale);\n if (this.config.storageKey) this.storage.set(this.config.storageKey, locale);\n if (isDevMode() && previous !== locale) this.warnOnLocaleIdDrift(locale);\n }\n\n /** Available locales — pass-through from config. */\n available(): readonly string[] {\n return this.config.availableLocales;\n }\n\n // Scopes\n\n /**\n * Register a scope so its catalog loads alongside the root one. Idempotent.\n * Returns a promise resolving once the current locale's scope catalog is in\n * the cache — handy for feature `canActivate` guards.\n */\n registerScope(scope: string): Promise<WrI18nCatalog> {\n this.scopes.add(scope);\n const lc = this._locale();\n // Resolve on the exact tag: the promise is what a `canActivate` waits on,\n // and the base-language catalog is only ever a fallback under it.\n const exact = this.loadCatalog(lc, scope);\n void this.loadChain(lc, scope);\n return exact;\n }\n\n // Translate\n\n /**\n * Eager translate — returns the current value or the missing-handler\n * fallback. Does NOT auto-load missing catalogs; combine with the\n * reactive `translate(...)` if you want load-on-demand.\n */\n t(key: string, params?: WrI18nParams, scope?: string): string {\n // Read revision so callers inside computed/effect re-run on updates.\n this.revision();\n const lc = this._locale();\n\n const chain = localeChain(lc);\n let hit: string | null = null;\n for (const candidate of chain) {\n if (scope) hit = walk(this.catalogs.get(cacheKey(candidate, scope)) ?? null, key);\n hit ??= walk(this.catalogs.get(cacheKey(candidate, null)) ?? null, key);\n if (hit !== null) break;\n }\n // Base catalogs are the floor: consulted only once the loader's own catalogs\n // have missed, so an app's keys always win, but ngwr's built-in strings are\n // still there when the app's catalog doesn't carry them.\n hit ??= this.walkBase(chain, key);\n\n if (hit === null) {\n // Fire a background load so a future read can resolve it.\n void this.loadChain(lc, scope ?? null);\n return this.config.missingHandler(key, lc);\n }\n return wrInterpolate(hit, params);\n }\n\n /** Reactive translate — re-evaluates on locale or catalog updates. */\n translate(key: string, params?: WrI18nParams, scope?: string): Signal<string> {\n return computed(() => this.t(key, params, scope));\n }\n\n // Internals\n\n /**\n * Look a key up in the registered base catalogs, later contributions first so\n * a feature can shadow an earlier one.\n */\n private walkBase(chain: readonly string[], key: string): string | null {\n for (const locale of chain) {\n for (let i = this.baseCatalogs.length - 1; i >= 0; i--) {\n const hit = walk(this.baseCatalogs[i][locale] ?? null, key);\n if (hit !== null) return hit;\n }\n }\n return null;\n }\n\n private initialLocale(): string {\n if (this.config.storageKey) {\n const saved = this.storage.get<string>(this.config.storageKey);\n if (saved && this.config.availableLocales.includes(saved)) return saved;\n }\n return this.config.defaultLocale;\n }\n\n /**\n * Load a `(locale, scope)` catalog and, when the locale carries a region, the\n * bare-language one under it. Both go in the cache; `t()` reads them in that\n * order.\n */\n private loadChain(locale: string, scope: string | null): Promise<unknown> {\n return Promise.all(localeChain(locale).map(candidate => this.loadCatalog(candidate, scope)));\n }\n\n /** One warning per service, not one per switch. @internal */\n private warnedLocaleIdDrift = false;\n\n /**\n * Say once, in dev, that a runtime switch moved the text and nothing else.\n *\n * The mismatch is otherwise invisible in the way that costs the most time:\n * the catalog strings all change, which reads as \"the switch worked\", and the\n * dates and numbers beside them are still correct-looking output in the old\n * locale. Silent when the new locale IS `LOCALE_ID`, because then nothing is\n * out of step and a warning that fires when everything agrees is one people\n * learn to ignore. Production builds drop the call entirely via `isDevMode()`\n * tree-shaking.\n */\n private warnOnLocaleIdDrift(locale: string): void {\n if (this.warnedLocaleIdDrift || locale === this.localeId) return;\n this.warnedLocaleIdDrift = true;\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] WrI18n.use('${locale}') switched the catalog, but LOCALE_ID is still '${this.localeId}'. ` +\n `LOCALE_ID is an Angular constant and WR_DATE_LOCALE resolves from it once at bootstrap, so ` +\n `formatted dates and numbers do not follow the switch and stay on '${this.localeId}': the date ` +\n `adapter (and so wr-calendar / wr-date-picker), the wrDate / wrNumber / wrPlural pipes, ` +\n `wr-input-number, wr-statistic, wr-counter, wr-pagination's total and wr-file-upload's sizes. ` +\n `Reload the app with the new LOCALE_ID to move those, or keep runtime switching to text.`\n );\n }\n\n private loadCatalog(locale: string, scope: string | null): Promise<WrI18nCatalog> {\n const ck = cacheKey(locale, scope);\n const cached = this.catalogs.get(ck);\n if (cached) return Promise.resolve(cached);\n\n const pending = this.inflight.get(ck);\n if (pending) return pending;\n\n const stream: Observable<WrI18nCatalog> = this.loader.load(locale, scope);\n const promise = firstValueFrom(stream)\n .then(catalog => {\n this.catalogs.set(ck, catalog);\n this.revision.update(v => v + 1);\n return catalog;\n })\n .catch((err: unknown) => {\n // eslint-disable-next-line no-console -- intentional diagnostic for missing catalog\n console.warn(`[ngwr/i18n] failed to load (${locale}, ${scope ?? 'root'})`, err);\n const empty: WrI18nCatalog = {};\n this.catalogs.set(ck, empty);\n return empty;\n })\n .finally(() => {\n this.inflight.delete(ck);\n });\n\n this.inflight.set(ck, promise);\n return promise;\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';\n\nimport { WrI18n } from './i18n';\nimport { type WrI18nParams } from './i18n-config';\n\n/**\n * Translate a key. Impure — re-evaluates on every CD so locale switches\n * and catalog loads propagate without a manual refresh.\n *\n * @example\n * ```html\n * <h1>{{ 'home.title' | wrT }}</h1>\n * <p>{{ 'greeting' | wrT: { name: user().name } }}</p>\n * <button>{{ 'forms.save' | wrT: null : 'checkout' }}</button>\n * ```\n */\n// eslint-disable-next-line @angular-eslint/no-pipe-impure -- intentional: locale + catalog updates must propagate even when args are stable\n@Pipe({ name: 'wrT', pure: false })\nexport class WrTPipe implements PipeTransform {\n private readonly i18n = inject(WrI18n);\n\n transform(key: string, params?: WrI18nParams | null, scope?: string): string {\n return this.i18n.t(key, params ?? undefined, scope);\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 { Directive, ElementRef, computed, effect, inject, input } from '@angular/core';\n\nimport { WrI18n } from './i18n';\nimport { type WrI18nParams } from './i18n-config';\n\n/**\n * Attribute directive that sets `textContent` of its host to the translation\n * of the bound key. Reactive — re-renders on locale + catalog changes.\n *\n * @example\n * ```html\n * <span [wrT]=\"'home.title'\"></span>\n * <span [wrT]=\"'greeting'\" [wrTParams]=\"{ name: user().name }\"></span>\n * <span [wrT]=\"'forms.save'\" wrTScope=\"checkout\"></span>\n * ```\n */\n@Directive({\n selector: '[wrT]',\n})\nexport class WrTDirective {\n /** Translation key. Required. */\n readonly wrT = input.required<string>();\n\n /** Interpolation params for `{{name}}` tokens. */\n readonly wrTParams = input<WrI18nParams | null>(null);\n\n /** Optional scope — scoped lookup first, then root fallback. */\n readonly wrTScope = input<string | null>(null);\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly i18n = inject(WrI18n);\n\n private readonly text = computed(() =>\n this.i18n.t(this.wrT(), this.wrTParams() ?? undefined, this.wrTScope() ?? undefined)\n );\n\n constructor() {\n effect(() => {\n this.host.nativeElement.textContent = this.text();\n });\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 { type Observable, of } from 'rxjs';\n\nimport { type WrI18nCatalog } from '../i18n-config';\nimport { type WrI18nLoader } from '../i18n-loader';\n\n/** Root catalogs keyed by locale: `{ en: catalog, ru: catalog, … }`. */\nexport type WrI18nStaticCatalogs = Readonly<Record<string, WrI18nCatalog>>;\n\n/** Per-scope catalogs: `{ checkout: { en: catalog, ru: catalog }, … }`. */\nexport type WrI18nStaticScopedCatalogs = Readonly<Record<string, WrI18nStaticCatalogs>>;\n\n/**\n * Loader for catalogs already in memory (imported from JSON or generated).\n *\n * - Pass `catalogs` for the root catalog of every locale.\n * - Pass `scopes` if you ship per-feature catalogs the loader should serve\n * to `WrI18n.registerScope(name)` calls.\n *\n * Missing locale or scope resolves to `{}` — the missing-key handler then\n * decides what to render.\n */\nexport class WrI18nStaticLoader implements WrI18nLoader {\n constructor(\n private readonly catalogs: WrI18nStaticCatalogs,\n private readonly scopes: WrI18nStaticScopedCatalogs = {}\n ) {}\n\n load(locale: string, scope: string | null): Observable<WrI18nCatalog> {\n if (scope === null) return of(this.catalogs[locale] ?? {});\n return of(this.scopes[scope]?.[locale] ?? {});\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 { HttpClient } from '@angular/common/http';\nimport { inject } from '@angular/core';\n\nimport { type Observable, catchError, of } from 'rxjs';\n\nimport { type WrI18nCatalog } from '../i18n-config';\nimport { type WrI18nLoader } from '../i18n-loader';\n\n/** Configuration for {@link WrI18nHttpLoader}. */\nexport interface WrI18nHttpLoaderConfig {\n /**\n * URL template. Tokens:\n * - `{locale}` — required, BCP-47 tag (e.g. `en`).\n * - `{scope}` — optional, scope name. Use the `rootPath` if you need a\n * different path entirely for the root catalog.\n *\n * @example\n * ```ts\n * { path: '/assets/i18n/{locale}.json' } // root only\n * { path: '/assets/i18n/{scope}/{locale}.json',\n * rootPath: '/assets/i18n/{locale}.json' } // root + scopes\n * ```\n */\n readonly path: string;\n /** Path template for the root catalog. Defaults to `path`. */\n readonly rootPath?: string;\n}\n\n/**\n * Loader that fetches catalogs via `HttpClient`. Failures resolve to `{}`\n * (logged once at the service layer) so a missing file never throws into\n * user code.\n */\nexport class WrI18nHttpLoader implements WrI18nLoader {\n private readonly http = inject(HttpClient);\n\n constructor(private readonly config: WrI18nHttpLoaderConfig) {}\n\n load(locale: string, scope: string | null): Observable<WrI18nCatalog> {\n const template = scope === null ? (this.config.rootPath ?? this.config.path) : this.config.path;\n const url = template.replace('{locale}', locale).replace('{scope}', scope ?? '');\n return this.http.get<WrI18nCatalog>(url).pipe(catchError(() => of<WrI18nCatalog>({})));\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 { type EnvironmentProviders, type Provider, makeEnvironmentProviders } from '@angular/core';\n\nimport { WR_I18N_BASE_CATALOGS, type WrI18nBaseCatalogs } from './i18n-base-catalogs';\nimport {\n DEFAULT_WR_I18N_CONFIG,\n WR_I18N_CONFIG,\n type WrI18nConfig,\n type WrI18nConfigResolved,\n resolveWrI18nLocales,\n} from './i18n-config';\nimport { WR_I18N_LOADER, type WrI18nLoader } from './i18n-loader';\nimport { WrI18nHttpLoader, type WrI18nHttpLoaderConfig } from './loaders/http-loader';\nimport {\n WrI18nStaticLoader,\n type WrI18nStaticCatalogs,\n type WrI18nStaticScopedCatalogs,\n} from './loaders/static-loader';\n\n/**\n * Configure {@link WrI18n}. Either pass a `loader` directly, or use the\n * `provideWrI18nHttpLoader` / `provideWrI18nStaticLoader` helpers in the\n * same providers array.\n */\nexport interface ProvideWrI18nOptions extends WrI18nConfig {\n readonly loader?: WrI18nLoader;\n}\n\nexport function provideWrI18n(options: ProvideWrI18nOptions = {}): EnvironmentProviders {\n const { loader, ...config } = options;\n\n const providers: Provider[] = [\n {\n provide: WR_I18N_CONFIG,\n // A factory rather than a value: `defaultLocale` falls back to `LOCALE_ID`,\n // which only an injection context can read.\n useFactory: (): WrI18nConfigResolved => ({\n ...DEFAULT_WR_I18N_CONFIG,\n ...config,\n ...resolveWrI18nLocales(config),\n }),\n },\n ];\n if (loader) providers.push({ provide: WR_I18N_LOADER, useValue: loader });\n\n return makeEnvironmentProviders(providers);\n}\n\n/**\n * Provide a static loader — catalogs already in memory.\n *\n * @param catalogs Root catalogs keyed by locale.\n * @param scopes Optional per-scope catalogs served to `registerScope(name)`.\n */\nexport function provideWrI18nStaticLoader(\n catalogs: WrI18nStaticCatalogs,\n scopes?: WrI18nStaticScopedCatalogs\n): Provider {\n return { provide: WR_I18N_LOADER, useValue: new WrI18nStaticLoader(catalogs, scopes) };\n}\n\n/**\n * Register catalogs consulted *underneath* the loader — this is how ngwr's own\n * built-in strings survive when you supply your own catalogs.\n *\n * A loader replaces the catalog for a locale instead of extending it, so a JSON\n * file holding only your keys leaves every built-in label on its hardcoded\n * English fallback, with nothing logged. One line fixes it, on either loader:\n *\n * @example\n * ```ts\n * import { wrRu } from 'ngwr/i18n/ru';\n *\n * providers: [\n * provideWrI18n({ defaultLocale: 'ru', availableLocales: ['ru'] }),\n * provideWrI18nBaseCatalogs({ ru: wrRu }),\n * provideWrI18nHttpLoader({ path: '/assets/i18n/{locale}.json' }),\n * ]\n * ```\n *\n * Pass only the locales you ship — the ones you leave out stay out of the\n * bundle. Your own keys always win; this is a floor, not an override.\n */\nexport function provideWrI18nBaseCatalogs(catalogs: WrI18nBaseCatalogs): Provider {\n return { provide: WR_I18N_BASE_CATALOGS, useValue: catalogs, multi: true };\n}\n\n/** Provide an HTTP loader — catalogs fetched from URLs. Requires `provideHttpClient`. */\nexport function provideWrI18nHttpLoader(config: WrI18nHttpLoaderConfig): Provider {\n return { provide: WR_I18N_LOADER, useFactory: () => new WrI18nHttpLoader(config) };\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 { type Signal, computed, inject, signal } from '@angular/core';\n\nimport { WrI18n, wrInterpolate } from './i18n';\nimport type { WrI18nParams } from './i18n-config';\n\n/**\n * Helper for component default text. Returns a reactive string that:\n *\n * 1. Prefers the consumer's override (an `input<string | null>()` value).\n * 2. Falls back to the i18n catalog under `key` — re-renders on locale\n * change so a switcher updates the DOM live.\n * 3. Falls back to `fallback` when no `WrI18n` is provided OR the catalog\n * is missing the key.\n *\n * Components don't need a hard dependency on `provideWrI18n` — without it,\n * the fallback is returned and everything still works.\n *\n * @example\n * ```ts\n * readonly emptyText = input<string | null>(null);\n * protected readonly empty = useI18nText(this.emptyText, 'table.empty', 'No data');\n *\n * // In template: <div>{{ empty() }}</div>\n * ```\n */\nexport function useI18nText(binding: Signal<string | null | undefined>, key: string, fallback: string): Signal<string> {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) {\n return computed(() => {\n const v = binding();\n return v ?? fallback;\n });\n }\n\n return computed(() => {\n const v = binding();\n if (v !== null && v !== undefined && v !== '') return v;\n const translated = i18n.t(key);\n // Default missing handler returns the key itself — treat that as \"missing\"\n // and use our explicit fallback.\n return translated === key ? fallback : translated;\n });\n}\n\n/**\n * {@link useI18nText} without a binding to forward — for a built-in string the\n * component exposes no override input for. Same reactivity, same fallback\n * rules; the only difference is the missing first argument.\n *\n * It used to return a plain `string`, resolved once at injection time, and\n * that was wrong in the ordinary case rather than an edge one: `WrI18n` writes\n * every loader-backed catalog from `firstValueFrom(...).then(...)`, so even a\n * static loader serving an object literal lands a microtask AFTER the first\n * change-detection pass. Every component built in that pass read an empty\n * catalog and froze its English fallback for the life of the app — in one\n * `<wr-date-picker>` the field said \"Открыть календарь\" and the calendar\n * button beside it \"Open calendar\", off the same key.\n */\nexport function readI18nText(key: string, fallback: string): Signal<string> {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) return computed(() => fallback);\n\n return computed(() => {\n const v = i18n.t(key);\n return v === key ? fallback : v;\n });\n}\n\n/** Tiny shim — useful when the caller doesn't have an `input` to forward. */\nexport function nullSignal(): Signal<null> {\n return signal(null).asReadonly();\n}\n\n/**\n * Build a per-call formatter for parametrized labels (e.g. ARIA labels\n * that interpolate a per-row value: `\"Remove {{label}}\"`). Returns a\n * function the template calls with the params for the current item.\n *\n * The returned function:\n * - Re-resolves the i18n catalog on every call (live locale switches just\n * work — re-rendered templates pick up new strings).\n * - Falls back to the literal `fallback` template with the same\n * `{{name}}` syntax when no `WrI18n` provider is present, or when the\n * key is missing.\n *\n * Use this for labels that vary per row. For static labels, prefer\n * {@link useI18nText} or {@link readI18nText}.\n *\n * @example\n * ```ts\n * protected readonly removeChipLabel = useI18nFormatter('select.removeItem', 'Remove {{label}}');\n *\n * // In template:\n * // [attr.aria-label]=\"removeChipLabel({ label: chip.label })\"\n * ```\n */\nexport function useI18nFormatter(key: string, fallback: string): (params?: WrI18nParams) => string {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) {\n return (params?: WrI18nParams) => wrInterpolate(fallback, params);\n }\n return (params?: WrI18nParams) => {\n const v = i18n.t(key, params);\n return v === key ? wrInterpolate(fallback, params) : v;\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-i18n.mjs","sources":["../../../projects/lib/i18n/i18n-base-catalogs.ts","../../../projects/lib/i18n/i18n-config.ts","../../../projects/lib/i18n/i18n-loader.ts","../../../projects/lib/i18n/i18n.ts","../../../projects/lib/i18n/t.pipe.ts","../../../projects/lib/i18n/t.directive.ts","../../../projects/lib/i18n/loaders/static-loader.ts","../../../projects/lib/i18n/loaders/http-loader.ts","../../../projects/lib/i18n/provide-wr-i18n.ts","../../../projects/lib/i18n/util.ts","../../../projects/lib/i18n/ngwr-i18n.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 { InjectionToken } from '@angular/core';\n\nimport { type WrI18nCatalog } from './i18n-config';\n\n/** Base catalogs keyed by locale, contributed by {@link provideWrI18nBaseCatalogs}. */\nexport type WrI18nBaseCatalogs = Readonly<Record<string, WrI18nCatalog>>;\n\n/**\n * Catalogs consulted *underneath* whatever the loader returned — the place for\n * ngwr's own built-in strings.\n *\n * A loader **replaces** the catalog for a locale rather than extending it, so an\n * app whose JSON holds only its own keys used to lose every built-in label to\n * its hardcoded English fallback, silently. Registering `wrRu` / `wrEn` here\n * puts them back without the app having to merge anything itself.\n *\n * `multi`, so several features can each contribute; later contributions win on\n * a key collision, and anything the loader returned still wins over all of them.\n *\n * It has to be handed in by the consumer rather than wired up inside\n * `provideWrI18n()`: the catalogs are their own entry points (`ngwr/i18n/ru`)\n * and they already import from `ngwr/i18n`, so importing them back here is a\n * circular entry-point dependency that ng-packagr rejects outright. Passing\n * them in also keeps the locales you don't ship out of your bundle.\n */\nexport const WR_I18N_BASE_CATALOGS = new InjectionToken<readonly WrI18nBaseCatalogs[]>('WR_I18N_BASE_CATALOGS');\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 { InjectionToken, LOCALE_ID, inject } from '@angular/core';\n\n/** Translation catalog — flat or nested record of strings. */\nexport interface WrI18nCatalog {\n readonly [key: string]: string | WrI18nCatalog;\n}\n\n/** Parameters passed to interpolation — coerced to strings via `String(v)`. */\nexport type WrI18nParams = Readonly<Record<string, unknown>>;\n\n/** Hook called once on every key that resolves to nothing. */\nexport type WrI18nMissingHandler = (key: string, locale: string) => string;\n\n/** User-facing config for {@link provideWrI18n}. */\nexport interface WrI18nConfig {\n /**\n * BCP-47 tag used until the user calls `use()`.\n *\n * Omitted, it is Angular's own `LOCALE_ID` — the app already states its\n * locale there, and ngwr having a second, unrelated default is how a fully\n * Russian app came to render an English calendar inside itself. See\n * {@link WrI18n} for the whole precedence.\n *\n * @default LOCALE_ID\n */\n readonly defaultLocale?: string;\n\n /**\n * Whitelisted locales — `use()` ignores anything outside this list. Omitted,\n * it is `[defaultLocale]`: it used to be a hardcoded `['en']`, so\n * `provideWrI18n({ defaultLocale: 'ru' })` produced a service whose own\n * default locale was not switchable back to.\n *\n * @default [defaultLocale]\n */\n readonly availableLocales?: readonly string[];\n\n /**\n * Storage key for persisting the active locale (via `WrStorage`).\n * Set to `null` to disable persistence. @default 'wr:i18n:locale'\n */\n readonly storageKey?: string | null;\n\n /** Called when a key is missing. Default returns the key itself. */\n readonly missingHandler?: WrI18nMissingHandler;\n}\n\n/** Resolved config — every field non-optional. @internal */\nexport interface WrI18nConfigResolved {\n readonly defaultLocale: string;\n readonly availableLocales: readonly string[];\n readonly storageKey: string | null;\n readonly missingHandler: WrI18nMissingHandler;\n}\n\n/**\n * Everything but the locale, which cannot be a constant: it is resolved from\n * `LOCALE_ID` in an injection context. @internal\n */\nexport const DEFAULT_WR_I18N_CONFIG: Omit<WrI18nConfigResolved, 'defaultLocale' | 'availableLocales'> = {\n storageKey: 'wr:i18n:locale',\n missingHandler: key => key,\n};\n\n/**\n * Resolve the locale half of the config. Callable only from an injection\n * context, because the seed is `LOCALE_ID`. @internal\n */\nexport function resolveWrI18nLocales(\n config: WrI18nConfig\n): Pick<WrI18nConfigResolved, 'defaultLocale' | 'availableLocales'> {\n const defaultLocale = config.defaultLocale ?? inject(LOCALE_ID);\n return { defaultLocale, availableLocales: config.availableLocales ?? [defaultLocale] };\n}\n\nexport const WR_I18N_CONFIG = new InjectionToken<WrI18nConfigResolved>('WR_I18N_CONFIG', {\n factory: () => ({ ...DEFAULT_WR_I18N_CONFIG, ...resolveWrI18nLocales({}) }),\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 { InjectionToken } from '@angular/core';\n\nimport { of, type Observable } from 'rxjs';\n\nimport { type WrI18nCatalog } from './i18n-config';\n\n/**\n * Loads a catalog for a `(locale, scope)` pair. `scope` is `null` for the\n * root catalog. Implementations may return the catalog synchronously\n * (via `of(...)`) for static catalogs or asynchronously (via HTTP) for\n * lazy fetches.\n */\nexport interface WrI18nLoader {\n load(locale: string, scope: string | null): Observable<WrI18nCatalog>;\n}\n\n/**\n * The active loader.\n *\n * Defaults to one that serves an empty catalog, which is what makes\n * `provideWrI18n()` genuinely optional: every ngwr component routes its\n * built-in strings through `useI18nText` / `readI18nText`, those inject\n * `WrI18n` optionally — and because `WrI18n` is `providedIn: 'root'`, an\n * optional inject still CONSTRUCTS it. Without a default here that construction\n * threw `NG0201: No provider found for WR_I18N_LOADER`, so an app that had\n * simply not configured i18n could not render `<wr-alert>` at all.\n *\n * With the empty catalog, every lookup misses, `t()` hands back the key, and\n * the helpers fall through to the component's own English default — the\n * documented no-i18n behaviour.\n */\nexport const WR_I18N_LOADER = new InjectionToken<WrI18nLoader>('WR_I18N_LOADER', {\n providedIn: 'root',\n factory: () => ({ load: () => of({}) }),\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, Service, type Signal, computed, effect, inject, isDevMode, signal } from '@angular/core';\n\nimport { type Observable, firstValueFrom } from 'rxjs';\n\nimport { WrStorage } from 'ngwr/storage';\n\nimport { WR_I18N_BASE_CATALOGS } from './i18n-base-catalogs';\nimport { WR_I18N_CONFIG, type WrI18nCatalog, type WrI18nParams } from './i18n-config';\nimport { WR_I18N_LOADER } from './i18n-loader';\n\n/** Cache key shape: `<locale>::<scope|''>`. @internal */\nfunction cacheKey(locale: string, scope: string | null): string {\n return `${locale}::${scope ?? ''}`;\n}\n\n/**\n * A locale and the language under it: `'ru-RU'` gives `['ru-RU', 'ru']`.\n *\n * Every lookup and every load walks this, so a catalog registered as `ru`\n * answers an app running `ru-RU`. Without it the two spellings are unrelated\n * strings, and the miss is silent — `t()` returns the key, `useI18nText` reads\n * that as \"missing\" and serves the English fallback, so a fully translated app\n * renders English with nothing logged. It only ever truncates: an `en` lookup\n * never reaches an `en-US` catalog, because a language is not a region.\n *\n * @internal\n */\nfunction localeChain(locale: string): readonly string[] {\n const dash = locale.indexOf('-');\n return dash > 0 ? [locale, locale.slice(0, dash)] : [locale];\n}\n\n/** Walk a dotted path through a catalog tree. Returns string | null. @internal */\nfunction walk(cat: WrI18nCatalog | null, key: string): string | null {\n if (!cat) return null;\n let node: string | WrI18nCatalog | undefined = cat;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return null;\n node = node[part];\n }\n return typeof node === 'string' ? node : null;\n}\n\n/**\n * `{{name}}` interpolation. Missing params resolve to `''`. Exported so\n * helpers in `util.ts` can interpolate the fallback template when no\n * `WrI18n` provider is wired up.\n *\n * @internal\n */\nexport function wrInterpolate(template: string, params: WrI18nParams | undefined): string {\n if (!params) return template;\n return template.replace(/\\{\\{\\s*([\\w.-]+)\\s*\\}\\}/g, (_match, name: string) => {\n const v = params[name];\n if (v === undefined || v === null) return '';\n if (typeof v === 'string') return v;\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n try {\n return JSON.stringify(v);\n } catch {\n return '';\n }\n });\n}\n\n/**\n * Reactive i18n service. Holds:\n *\n * - the active locale as a `Signal<string>`\n * - per-`(locale, scope)` catalogs in a `Map` (loaded once, then cached)\n * - a per-locale signal counter so every `translate()` reading the signal\n * re-evaluates the moment a new catalog finishes loading\n *\n * Translations are looked up scope-first then root, per locale, and the locale\n * itself is tried as the full tag before the bare language underneath it — so\n * an app running `ru-RU` resolves against a catalog registered as `ru`, and a\n * regional catalog is never shadowed by a scope in the base language. For an\n * app that spells its locale one way, which is every app that existed before\n * the chain, the order is exactly what it always was.\n *\n * ## Which locale wins\n *\n * ngwr has three locale inputs and they used to be three unrelated answers — an\n * app that set `LOCALE_ID: 'ru-RU'` correctly still rendered an English\n * calendar, because the date token defaulted to `navigator.language`. The\n * precedence is now stated, highest first:\n *\n * 1. **A subsystem's own explicit option.** `provideWrDateAdapter({ locale })`\n * pins dates; `provideWrI18n({ defaultLocale })` pins the catalog. The most\n * specific statement wins, because it can only have been deliberate.\n * 2. **`WrI18n.locale()` — the runtime value.** The only one of the three that\n * can change without a reload, so it is what a language switcher writes.\n * 3. **`LOCALE_ID`.** Angular's app-wide constant, and the seed for both of the\n * above when neither was given. One statement, one answer.\n * 4. `'en-US'`, which is what `LOCALE_ID` itself falls back to.\n *\n * `navigator.language` is deliberately NOT in that list. It is the browser's\n * preference, not the application's, and reading it made prerendered output and\n * the hydrated page disagree.\n *\n * **What a runtime switch reaches, and what it does not.** `use()` repaints\n * every catalog string. It does not move `LOCALE_ID`, which is a constant, so\n * anything keyed on it — `wrDate` / `wrNumber` / `wrPlural`, `wr-input-number`,\n * `wr-statistic`, `wr-counter` — and anything keyed on `WR_DATE_LOCALE` — the\n * date adapter, and so `wr-calendar` and `wr-date-picker` — stays where it was.\n * `use()` says so once in dev mode rather than leaving it to be discovered.\n *\n * @example\n * ```ts\n * const i18n = inject(WrI18n);\n * i18n.use('ru'); // switch locale\n * i18n.t('cart.checkout'); // → 'Оформить заказ'\n * i18n.t('greeting', { name: 'Ada' }); // → 'Hello, Ada!'\n *\n * effect(() => console.log(i18n.translate('hi')())); // re-runs on locale change\n * ```\n *\n * @see https://ngwr.dev/reference/services/i18n\n */\n@Service()\nexport class WrI18n {\n private readonly config = inject(WR_I18N_CONFIG);\n private readonly loader = inject(WR_I18N_LOADER);\n private readonly storage = inject(WrStorage);\n\n /**\n * Angular's own locale. Read for the dev-mode drift warning only — the\n * resolved default already folded it in through `WR_I18N_CONFIG`. @internal\n */\n private readonly localeId = inject(LOCALE_ID);\n\n /**\n * Catalogs registered via `provideWrI18nBaseCatalogs()` — the floor under the\n * loader's own catalogs, so ngwr's built-in strings survive an app that only\n * ships its own keys. @internal\n */\n private readonly baseCatalogs = inject(WR_I18N_BASE_CATALOGS, { optional: true }) ?? [];\n\n /** Per-(locale,scope) catalogs, populated by `loadCatalog`. @internal */\n private readonly catalogs = new Map<string, WrI18nCatalog>();\n\n /** Bumped after each catalog write so reactive translates recompute. @internal */\n private readonly revision = signal(0);\n\n /** In-flight load promises — dedupes parallel `t()` callers. @internal */\n private readonly inflight = new Map<string, Promise<WrI18nCatalog>>();\n\n /** Registered scopes — auto-loaded whenever the locale changes. @internal */\n private readonly scopes = new Set<string>();\n\n /** Active locale. Writes go through `use()` so storage stays in sync. */\n private readonly _locale = signal<string>(this.initialLocale());\n readonly locale: Signal<string> = this._locale.asReadonly();\n\n constructor() {\n // Eagerly load the root catalog + every registered scope for the\n // current locale. Subsequent locale changes refire the same effect.\n effect(() => {\n const lc = this._locale();\n void this.loadChain(lc, null);\n for (const sc of this.scopes) void this.loadChain(lc, sc);\n });\n }\n\n // Locale\n\n /**\n * Switch active locale. Ignored when not whitelisted.\n *\n * Text only — see the class docs for what a runtime switch cannot reach. The\n * dev-mode warning below is the whole reporting of that limit; without it the\n * failure is a screen showing German labels beside `3/15/2026`, with nothing\n * anywhere saying why.\n */\n use(locale: string): void {\n if (!this.config.availableLocales.includes(locale)) return;\n const previous = this._locale();\n this._locale.set(locale);\n if (this.config.storageKey) this.storage.set(this.config.storageKey, locale);\n if (isDevMode() && previous !== locale) this.warnOnLocaleIdDrift(locale);\n }\n\n /** Available locales — pass-through from config. */\n available(): readonly string[] {\n return this.config.availableLocales;\n }\n\n // Scopes\n\n /**\n * Register a scope so its catalog loads alongside the root one. Idempotent.\n * Returns a promise resolving once the current locale's scope catalog is in\n * the cache — handy for feature `canActivate` guards.\n */\n registerScope(scope: string): Promise<WrI18nCatalog> {\n this.scopes.add(scope);\n const lc = this._locale();\n // Resolve on the exact tag: the promise is what a `canActivate` waits on,\n // and the base-language catalog is only ever a fallback under it.\n const exact = this.loadCatalog(lc, scope);\n void this.loadChain(lc, scope);\n return exact;\n }\n\n // Translate\n\n /**\n * Eager translate — returns the current value or the missing-handler\n * fallback. Does NOT auto-load missing catalogs; combine with the\n * reactive `translate(...)` if you want load-on-demand.\n */\n t(key: string, params?: WrI18nParams, scope?: string): string {\n // Read revision so callers inside computed/effect re-run on updates.\n this.revision();\n const lc = this._locale();\n\n const chain = localeChain(lc);\n let hit: string | null = null;\n for (const candidate of chain) {\n if (scope) hit = walk(this.catalogs.get(cacheKey(candidate, scope)) ?? null, key);\n hit ??= walk(this.catalogs.get(cacheKey(candidate, null)) ?? null, key);\n if (hit !== null) break;\n }\n // Base catalogs are the floor: consulted only once the loader's own catalogs\n // have missed, so an app's keys always win, but ngwr's built-in strings are\n // still there when the app's catalog doesn't carry them.\n hit ??= this.walkBase(chain, key);\n\n if (hit === null) {\n // Fire a background load so a future read can resolve it.\n void this.loadChain(lc, scope ?? null);\n return this.config.missingHandler(key, lc);\n }\n return wrInterpolate(hit, params);\n }\n\n /** Reactive translate — re-evaluates on locale or catalog updates. */\n translate(key: string, params?: WrI18nParams, scope?: string): Signal<string> {\n return computed(() => this.t(key, params, scope));\n }\n\n // Internals\n\n /**\n * Look a key up in the registered base catalogs, later contributions first so\n * a feature can shadow an earlier one.\n */\n private walkBase(chain: readonly string[], key: string): string | null {\n for (const locale of chain) {\n for (let i = this.baseCatalogs.length - 1; i >= 0; i--) {\n const hit = walk(this.baseCatalogs[i][locale] ?? null, key);\n if (hit !== null) return hit;\n }\n }\n return null;\n }\n\n private initialLocale(): string {\n if (this.config.storageKey) {\n const saved = this.storage.get<string>(this.config.storageKey);\n if (saved && this.config.availableLocales.includes(saved)) return saved;\n }\n return this.config.defaultLocale;\n }\n\n /**\n * Load a `(locale, scope)` catalog and, when the locale carries a region, the\n * bare-language one under it. Both go in the cache; `t()` reads them in that\n * order.\n */\n private loadChain(locale: string, scope: string | null): Promise<unknown> {\n return Promise.all(localeChain(locale).map(candidate => this.loadCatalog(candidate, scope)));\n }\n\n /** One warning per service, not one per switch. @internal */\n private warnedLocaleIdDrift = false;\n\n /**\n * Say once, in dev, that a runtime switch moved the text and nothing else.\n *\n * The mismatch is otherwise invisible in the way that costs the most time:\n * the catalog strings all change, which reads as \"the switch worked\", and the\n * dates and numbers beside them are still correct-looking output in the old\n * locale. Silent when the new locale IS `LOCALE_ID`, because then nothing is\n * out of step and a warning that fires when everything agrees is one people\n * learn to ignore. Production builds drop the call entirely via `isDevMode()`\n * tree-shaking.\n */\n private warnOnLocaleIdDrift(locale: string): void {\n if (this.warnedLocaleIdDrift || locale === this.localeId) return;\n this.warnedLocaleIdDrift = true;\n // eslint-disable-next-line no-console -- dev-mode validation\n console.warn(\n `[NGWR] WrI18n.use('${locale}') switched the catalog, but LOCALE_ID is still '${this.localeId}'. ` +\n `LOCALE_ID is an Angular constant and WR_DATE_LOCALE resolves from it once at bootstrap, so ` +\n `formatted dates and numbers do not follow the switch and stay on '${this.localeId}': the date ` +\n `adapter (and so wr-calendar / wr-date-picker), the wrDate / wrNumber / wrPlural pipes, ` +\n `wr-input-number, wr-statistic, wr-counter, wr-pagination's total and wr-file-upload's sizes. ` +\n `Reload the app with the new LOCALE_ID to move those, or keep runtime switching to text.`\n );\n }\n\n private loadCatalog(locale: string, scope: string | null): Promise<WrI18nCatalog> {\n const ck = cacheKey(locale, scope);\n const cached = this.catalogs.get(ck);\n if (cached) return Promise.resolve(cached);\n\n const pending = this.inflight.get(ck);\n if (pending) return pending;\n\n const stream: Observable<WrI18nCatalog> = this.loader.load(locale, scope);\n const promise = firstValueFrom(stream)\n .then(catalog => {\n this.catalogs.set(ck, catalog);\n this.revision.update(v => v + 1);\n return catalog;\n })\n .catch((err: unknown) => {\n // eslint-disable-next-line no-console -- intentional diagnostic for missing catalog\n console.warn(`[ngwr/i18n] failed to load (${locale}, ${scope ?? 'root'})`, err);\n const empty: WrI18nCatalog = {};\n this.catalogs.set(ck, empty);\n return empty;\n })\n .finally(() => {\n this.inflight.delete(ck);\n });\n\n this.inflight.set(ck, promise);\n return promise;\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';\n\nimport { WrI18n } from './i18n';\nimport { type WrI18nParams } from './i18n-config';\n\n/**\n * Translate a key. Impure — re-evaluates on every CD so locale switches\n * and catalog loads propagate without a manual refresh.\n *\n * @example\n * ```html\n * <h1>{{ 'home.title' | wrT }}</h1>\n * <p>{{ 'greeting' | wrT: { name: user().name } }}</p>\n * <button>{{ 'forms.save' | wrT: null : 'checkout' }}</button>\n * ```\n */\n// eslint-disable-next-line @angular-eslint/no-pipe-impure -- intentional: locale + catalog updates must propagate even when args are stable\n@Pipe({ name: 'wrT', pure: false })\nexport class WrTPipe implements PipeTransform {\n private readonly i18n = inject(WrI18n);\n\n transform(key: string, params?: WrI18nParams | null, scope?: string): string {\n return this.i18n.t(key, params ?? undefined, scope);\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 { Directive, ElementRef, computed, effect, inject, input } from '@angular/core';\n\nimport { WrI18n } from './i18n';\nimport { type WrI18nParams } from './i18n-config';\n\n/**\n * Attribute directive that sets `textContent` of its host to the translation\n * of the bound key. Reactive — re-renders on locale + catalog changes.\n *\n * @example\n * ```html\n * <span [wrT]=\"'home.title'\"></span>\n * <span [wrT]=\"'greeting'\" [wrTParams]=\"{ name: user().name }\"></span>\n * <span [wrT]=\"'forms.save'\" wrTScope=\"checkout\"></span>\n * ```\n */\n@Directive({\n selector: '[wrT]',\n})\nexport class WrTDirective {\n /** Translation key. Required. */\n readonly wrT = input.required<string>();\n\n /** Interpolation params for `{{name}}` tokens. */\n readonly wrTParams = input<WrI18nParams | null>(null);\n\n /** Optional scope — scoped lookup first, then root fallback. */\n readonly wrTScope = input<string | null>(null);\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly i18n = inject(WrI18n);\n\n private readonly text = computed(() =>\n this.i18n.t(this.wrT(), this.wrTParams() ?? undefined, this.wrTScope() ?? undefined)\n );\n\n constructor() {\n effect(() => {\n this.host.nativeElement.textContent = this.text();\n });\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 { type Observable, of } from 'rxjs';\n\nimport { type WrI18nCatalog } from '../i18n-config';\nimport { type WrI18nLoader } from '../i18n-loader';\n\n/** Root catalogs keyed by locale: `{ en: catalog, ru: catalog, … }`. */\nexport type WrI18nStaticCatalogs = Readonly<Record<string, WrI18nCatalog>>;\n\n/** Per-scope catalogs: `{ checkout: { en: catalog, ru: catalog }, … }`. */\nexport type WrI18nStaticScopedCatalogs = Readonly<Record<string, WrI18nStaticCatalogs>>;\n\n/**\n * Loader for catalogs already in memory (imported from JSON or generated).\n *\n * - Pass `catalogs` for the root catalog of every locale.\n * - Pass `scopes` if you ship per-feature catalogs the loader should serve\n * to `WrI18n.registerScope(name)` calls.\n *\n * Missing locale or scope resolves to `{}` — the missing-key handler then\n * decides what to render.\n */\nexport class WrI18nStaticLoader implements WrI18nLoader {\n constructor(\n private readonly catalogs: WrI18nStaticCatalogs,\n private readonly scopes: WrI18nStaticScopedCatalogs = {}\n ) {}\n\n load(locale: string, scope: string | null): Observable<WrI18nCatalog> {\n if (scope === null) return of(this.catalogs[locale] ?? {});\n return of(this.scopes[scope]?.[locale] ?? {});\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 { HttpClient } from '@angular/common/http';\nimport { inject } from '@angular/core';\n\nimport { type Observable, catchError, of } from 'rxjs';\n\nimport { type WrI18nCatalog } from '../i18n-config';\nimport { type WrI18nLoader } from '../i18n-loader';\n\n/** Configuration for {@link WrI18nHttpLoader}. */\nexport interface WrI18nHttpLoaderConfig {\n /**\n * URL template. Tokens:\n * - `{locale}` — required, BCP-47 tag (e.g. `en`).\n * - `{scope}` — optional, scope name. Use the `rootPath` if you need a\n * different path entirely for the root catalog.\n *\n * @example\n * ```ts\n * { path: '/assets/i18n/{locale}.json' } // root only\n * { path: '/assets/i18n/{scope}/{locale}.json',\n * rootPath: '/assets/i18n/{locale}.json' } // root + scopes\n * ```\n */\n readonly path: string;\n /** Path template for the root catalog. Defaults to `path`. */\n readonly rootPath?: string;\n}\n\n/**\n * Loader that fetches catalogs via `HttpClient`. Failures resolve to `{}`\n * (logged once at the service layer) so a missing file never throws into\n * user code.\n */\nexport class WrI18nHttpLoader implements WrI18nLoader {\n private readonly http = inject(HttpClient);\n\n constructor(private readonly config: WrI18nHttpLoaderConfig) {}\n\n load(locale: string, scope: string | null): Observable<WrI18nCatalog> {\n const template = scope === null ? (this.config.rootPath ?? this.config.path) : this.config.path;\n const url = template.replace('{locale}', locale).replace('{scope}', scope ?? '');\n return this.http.get<WrI18nCatalog>(url).pipe(catchError(() => of<WrI18nCatalog>({})));\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 { type EnvironmentProviders, type Provider, makeEnvironmentProviders } from '@angular/core';\n\nimport { WR_I18N_BASE_CATALOGS, type WrI18nBaseCatalogs } from './i18n-base-catalogs';\nimport {\n DEFAULT_WR_I18N_CONFIG,\n WR_I18N_CONFIG,\n type WrI18nConfig,\n type WrI18nConfigResolved,\n resolveWrI18nLocales,\n} from './i18n-config';\nimport { WR_I18N_LOADER, type WrI18nLoader } from './i18n-loader';\nimport { WrI18nHttpLoader, type WrI18nHttpLoaderConfig } from './loaders/http-loader';\nimport {\n WrI18nStaticLoader,\n type WrI18nStaticCatalogs,\n type WrI18nStaticScopedCatalogs,\n} from './loaders/static-loader';\n\n/**\n * Configure {@link WrI18n}. Either pass a `loader` directly, or use the\n * `provideWrI18nHttpLoader` / `provideWrI18nStaticLoader` helpers in the\n * same providers array.\n */\nexport interface ProvideWrI18nOptions extends WrI18nConfig {\n readonly loader?: WrI18nLoader;\n}\n\nexport function provideWrI18n(options: ProvideWrI18nOptions = {}): EnvironmentProviders {\n const { loader, ...config } = options;\n\n const providers: Provider[] = [\n {\n provide: WR_I18N_CONFIG,\n // A factory rather than a value: `defaultLocale` falls back to `LOCALE_ID`,\n // which only an injection context can read.\n useFactory: (): WrI18nConfigResolved => ({\n ...DEFAULT_WR_I18N_CONFIG,\n ...config,\n ...resolveWrI18nLocales(config),\n }),\n },\n ];\n if (loader) providers.push({ provide: WR_I18N_LOADER, useValue: loader });\n\n return makeEnvironmentProviders(providers);\n}\n\n/**\n * Provide a static loader — catalogs already in memory.\n *\n * A loader serves only the catalog you pass it, and ngwr registers none of its\n * own. Pass only your own keys here and register ngwr's catalogs with\n * {@link provideWrI18nBaseCatalogs} — not `{ ...wrRu, ...yours }`. A spread is\n * shallow, so any namespace you share with ngwr, e.g. `common`, `validation`,\n * `table`, keeps only the keys of the side spread last, and the other side's\n * keys stop resolving with nothing logged: ngwr's components fall back to their\n * English defaults, and a `wrT` read of a lost key renders the raw key under the\n * default missing handler.\n *\n * @param catalogs Root catalogs keyed by locale.\n * @param scopes Optional per-scope catalogs served to `registerScope(name)`.\n */\nexport function provideWrI18nStaticLoader(\n catalogs: WrI18nStaticCatalogs,\n scopes?: WrI18nStaticScopedCatalogs\n): Provider {\n return { provide: WR_I18N_LOADER, useValue: new WrI18nStaticLoader(catalogs, scopes) };\n}\n\n/**\n * Register catalogs consulted *underneath* the loader — this is how ngwr's own\n * built-in strings survive when you supply your own catalogs.\n *\n * A loader replaces the catalog for a locale instead of extending it, so a JSON\n * file holding only your keys leaves every built-in label on its hardcoded\n * English fallback, with nothing logged. One line fixes it, on either loader:\n *\n * @example\n * ```ts\n * import { wrRu } from 'ngwr/i18n/ru';\n *\n * providers: [\n * provideWrI18n({ defaultLocale: 'ru', availableLocales: ['ru'] }),\n * provideWrI18nBaseCatalogs({ ru: wrRu }),\n * provideWrI18nHttpLoader({ path: '/assets/i18n/{locale}.json' }),\n * ]\n * ```\n *\n * Pass only the locales you ship — the ones you leave out stay out of the\n * bundle. Your own keys always win; this is a floor, not an override.\n *\n * Use it instead of spreading `wrRu` into your own catalog, too: the lookup\n * walks these key by key after the loader's catalog misses, so a namespace both\n * define keeps both halves, where a shallow spread keeps only one.\n */\nexport function provideWrI18nBaseCatalogs(catalogs: WrI18nBaseCatalogs): Provider {\n return { provide: WR_I18N_BASE_CATALOGS, useValue: catalogs, multi: true };\n}\n\n/** Provide an HTTP loader — catalogs fetched from URLs. Requires `provideHttpClient`. */\nexport function provideWrI18nHttpLoader(config: WrI18nHttpLoaderConfig): Provider {\n return { provide: WR_I18N_LOADER, useFactory: () => new WrI18nHttpLoader(config) };\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 { type Signal, computed, inject, signal } from '@angular/core';\n\nimport { WrI18n, wrInterpolate } from './i18n';\nimport type { WrI18nParams } from './i18n-config';\n\n/**\n * Helper for component default text. Returns a reactive string that:\n *\n * 1. Prefers the consumer's override (an `input<string | null>()` value).\n * 2. Falls back to the i18n catalog under `key` — re-renders on locale\n * change so a switcher updates the DOM live.\n * 3. Falls back to `fallback` when no `WrI18n` is provided OR the catalog\n * is missing the key.\n *\n * Components don't need a hard dependency on `provideWrI18n` — without it,\n * the fallback is returned and everything still works.\n *\n * @example\n * ```ts\n * readonly emptyText = input<string | null>(null);\n * protected readonly empty = useI18nText(this.emptyText, 'table.empty', 'No data');\n *\n * // In template: <div>{{ empty() }}</div>\n * ```\n */\nexport function useI18nText(binding: Signal<string | null | undefined>, key: string, fallback: string): Signal<string> {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) {\n return computed(() => {\n const v = binding();\n return v ?? fallback;\n });\n }\n\n return computed(() => {\n const v = binding();\n if (v !== null && v !== undefined && v !== '') return v;\n const translated = i18n.t(key);\n // Default missing handler returns the key itself — treat that as \"missing\"\n // and use our explicit fallback.\n return translated === key ? fallback : translated;\n });\n}\n\n/**\n * {@link useI18nText} without a binding to forward — for a built-in string the\n * component exposes no override input for. Same reactivity, same fallback\n * rules; the only difference is the missing first argument.\n *\n * It used to return a plain `string`, resolved once at injection time, and\n * that was wrong in the ordinary case rather than an edge one: `WrI18n` writes\n * every loader-backed catalog from `firstValueFrom(...).then(...)`, so even a\n * static loader serving an object literal lands a microtask AFTER the first\n * change-detection pass. Every component built in that pass read an empty\n * catalog and froze its English fallback for the life of the app — in one\n * `<wr-date-picker>` the field said \"Открыть календарь\" and the calendar\n * button beside it \"Open calendar\", off the same key.\n */\nexport function readI18nText(key: string, fallback: string): Signal<string> {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) return computed(() => fallback);\n\n return computed(() => {\n const v = i18n.t(key);\n return v === key ? fallback : v;\n });\n}\n\n/** Tiny shim — useful when the caller doesn't have an `input` to forward. */\nexport function nullSignal(): Signal<null> {\n return signal(null).asReadonly();\n}\n\n/**\n * Build a per-call formatter for parametrized labels (e.g. ARIA labels\n * that interpolate a per-row value: `\"Remove {{label}}\"`). Returns a\n * function the template calls with the params for the current item.\n *\n * The returned function:\n * - Re-resolves the i18n catalog on every call (live locale switches just\n * work — re-rendered templates pick up new strings).\n * - Falls back to the literal `fallback` template with the same\n * `{{name}}` syntax when no `WrI18n` provider is present, or when the\n * key is missing.\n *\n * Use this for labels that vary per row. For static labels, prefer\n * {@link useI18nText} or {@link readI18nText}.\n *\n * @example\n * ```ts\n * protected readonly removeChipLabel = useI18nFormatter('select.removeItem', 'Remove {{label}}');\n *\n * // In template:\n * // [attr.aria-label]=\"removeChipLabel({ label: chip.label })\"\n * ```\n */\nexport function useI18nFormatter(key: string, fallback: string): (params?: WrI18nParams) => string {\n const i18n = inject(WrI18n, { optional: true });\n if (!i18n) {\n return (params?: WrI18nParams) => wrInterpolate(fallback, params);\n }\n return (params?: WrI18nParams) => {\n const v = i18n.t(key, params);\n return v === key ? wrInterpolate(fallback, params) : v;\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -44,9 +44,10 @@ const ARROWS = {
|
|
|
44
44
|
* arrives on the `(cropped)` output, which emits a `Blob` after each drag end or
|
|
45
45
|
* each run of arrow keys, and a consumer who wants a one-off read calls `toBlob()` /
|
|
46
46
|
* `toDataUrl()` — or `cropRect()` for the geometry — on the component through a
|
|
47
|
-
* `viewChild`.
|
|
48
|
-
*
|
|
49
|
-
* rather than
|
|
47
|
+
* `viewChild`. The same goes for `(loadError)` and `status()`, which is why
|
|
48
|
+
* {@link isFailed} reads the failure off the host class instead. Outputs and instance
|
|
49
|
+
* members are component API rather than DOM, so all of it is outside what a harness
|
|
50
|
+
* can reach at all, which is worth knowing rather than discovering.
|
|
50
51
|
*
|
|
51
52
|
* @example
|
|
52
53
|
* ```ts
|
|
@@ -118,6 +119,36 @@ class WrImageCropperHarness extends ComponentHarness {
|
|
|
118
119
|
}
|
|
119
120
|
await image.dispatchEvent('load');
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Fire the image's `error` event — what a browser does with a source it cannot
|
|
124
|
+
* decode, and the only way to get one in a unit test, where nothing is decoded.
|
|
125
|
+
*
|
|
126
|
+
* Afterwards {@link isFailed} is `true` and the component has emitted
|
|
127
|
+
* `(loadError)`; a spec that wants the payload binds that output on its host.
|
|
128
|
+
*/
|
|
129
|
+
async dispatchImageError() {
|
|
130
|
+
const image = await this.image();
|
|
131
|
+
if (!image) {
|
|
132
|
+
throw new Error('WrImageCropperHarness.dispatchImageError(): there is no image — the cropper has no `src`, so it is ' +
|
|
133
|
+
'showing its empty state and there is nothing to fail.');
|
|
134
|
+
}
|
|
135
|
+
await image.dispatchEvent('error');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether the browser failed to decode the current source.
|
|
139
|
+
*
|
|
140
|
+
* Read off the `wr-image-cropper--failed` host modifier — the part of the failure
|
|
141
|
+
* the DOM shows. It is a third answer beside {@link isEmpty} and {@link isReady},
|
|
142
|
+
* not a combination of them: a failed cropper has a `src` (so it is not empty) and
|
|
143
|
+
* no crop window (so it is not ready), which is also exactly what a cropper still
|
|
144
|
+
* LOADING looks like. Asking those two would call a broken image a slow one.
|
|
145
|
+
*
|
|
146
|
+
* The component renders no message for it, so there is no text to read here; the
|
|
147
|
+
* host's own message is the host's to assert.
|
|
148
|
+
*/
|
|
149
|
+
async isFailed() {
|
|
150
|
+
return (await this.host()).hasClass('wr-image-cropper--failed');
|
|
151
|
+
}
|
|
121
152
|
/**
|
|
122
153
|
* Where the crop window sits over the displayed image, in CSS pixels.
|
|
123
154
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-image-cropper-testing.mjs","sources":["../../../projects/lib/image-cropper/testing/wr-image-cropper-harness.ts","../../../projects/lib/image-cropper/testing/ngwr-image-cropper-testing.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, TestKey, type TestElement } from '@angular/cdk/testing';\n\nimport type { WrCropHandle } from 'ngwr/image-cropper';\n\nimport type { WrImageCropperHarnessFilters } from './interfaces';\n\nconst ARROWS = {\n left: TestKey.LEFT_ARROW,\n right: TestKey.RIGHT_ARROW,\n up: TestKey.UP_ARROW,\n down: TestKey.DOWN_ARROW,\n} as const;\n\n/** The four directions the crop window's keyboard model understands. */\nexport type WrCropArrow = keyof typeof ARROWS;\n\n/** Where the crop window sits over the DISPLAYED image, in CSS pixels. */\nexport interface WrCropWindowBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Test harness for `<wr-image-cropper>`.\n *\n * **This is the smallest honest surface of any harness here, and the reason is worth\n * reading before you reach for a method that is not on it.** Almost everything the\n * cropper does is measured: it reads the `<img>`'s rendered box and its\n * `naturalWidth`, converts between display and source pixels, and moves the crop\n * window with pointer deltas. jsdom measures nothing — every rect is 0×0 and\n * `naturalWidth` is 0 — so an image never \"loads\", the crop UI never renders, and a\n * synthetic drag divides by zero. There is deliberately no drag method: it would\n * write `NaN` and report success.\n *\n * What a spec CAN do is give the image a box and a natural size itself, then call\n * {@link dispatchImageLoad} — the two numbers `onImageLoad` reads are the whole\n * stub, and from there {@link getCropBox} and {@link getHandles} are real answers,\n * because the component writes the crop window's geometry as inline pixels. The\n * KEYBOARD path needs the same stub to get the crop UI up, but nothing beyond it:\n * its step is a constant rather than a pointer delta, so {@link pressArrow} lands\n * on a number a spec can name.\n *\n * ```ts\n * // In your spec, before dispatching the load:\n * const img = fixture.nativeElement.querySelector('.wr-image-cropper__image');\n * img.getBoundingClientRect = () => ({ width: 400, height: 400, x: 0, y: 0 }) as DOMRect;\n * Object.defineProperty(img, 'naturalWidth', { value: 800 });\n * Object.defineProperty(img, 'naturalHeight', { value: 800 });\n * ```\n *\n * **There is nothing to click.** The cropper renders no buttons: the result\n * arrives on the `(cropped)` output, which emits a `Blob` after each drag end or\n * each run of arrow keys, and a consumer who wants a one-off read calls `toBlob()` /\n * `toDataUrl()` — or `cropRect()` for the geometry — on the component through a\n * `viewChild`. An output and three instance members: component API rather than DOM,\n * so all of it is outside what a harness can reach at all, which is worth knowing\n * rather than discovering.\n *\n * @example\n * ```ts\n * const cropper = await loader.getHarness(WrImageCropperHarness);\n *\n * expect(await cropper.isEmpty()).toBe(false);\n * expect(await cropper.isReady()).toBe(false); // nothing measured yet\n *\n * await cropper.dispatchImageLoad();\n * expect(await cropper.getCropBox()).toEqual({ x: 80, y: 80, width: 240, height: 240 });\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrImageCropperHarness extends ComponentHarness {\n static hostSelector = 'wr-image-cropper';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrImageCropperHarnessFilters = {}): HarnessPredicate<WrImageCropperHarness> {\n return new HarnessPredicate(WrImageCropperHarness, options).addOption(\n 'empty',\n options.empty,\n async (harness, empty) => (await harness.isEmpty()) === empty\n );\n }\n\n private readonly image = this.locatorForOptional('.wr-image-cropper__image');\n private readonly cropWindow = this.locatorForOptional('.wr-image-cropper__window');\n\n /** Whether the cropper has no source at all, and is showing its placeholder. */\n async isEmpty(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-image-cropper__empty')()) !== null;\n }\n\n /** The placeholder's text, or `null` when there is an image. Comes from the catalog. */\n async getEmptyText(): Promise<string | null> {\n const empty = await this.locatorForOptional('.wr-image-cropper__empty')();\n return empty ? empty.text() : null;\n }\n\n /**\n * Whether the crop UI is up.\n *\n * Not the same question as {@link isEmpty}: a cropper with a perfectly good `src`\n * shows nothing but the image until it has been MEASURED, because the window's size\n * is a fraction of the rendered box. In a browser that happens on load; in a unit\n * test it happens when the spec stubs the box and calls\n * {@link dispatchImageLoad}.\n */\n async isReady(): Promise<boolean> {\n return (await this.cropWindow()) !== null;\n }\n\n /**\n * What the `<img>` is pointing at.\n *\n * A `File` or `Blob` source becomes an object URL, so this is `blob:…` for those\n * and the plain string for a string `src`. Comparing the two is not useful; asking\n * whether it CHANGED is.\n */\n async getImageSrc(): Promise<string | null> {\n const image = await this.image();\n return image ? image.getProperty<string>('src') : null;\n }\n\n /**\n * Fire the image's `load` event.\n *\n * The event, not a real load: jsdom fetches nothing, and the handler reads\n * `getBoundingClientRect()` and `naturalWidth` off the element. Stub those first —\n * see this class's docs — or the crop window will still not render and\n * {@link isReady} will stay `false`, which is exactly what a real cropper does with\n * a zero-sized image.\n */\n async dispatchImageLoad(): Promise<void> {\n const image = await this.image();\n if (!image) {\n throw new Error(\n 'WrImageCropperHarness.dispatchImageLoad(): there is no image — the cropper has no `src`, so it is ' +\n 'showing its empty state.'\n );\n }\n await image.dispatchEvent('load');\n }\n\n /**\n * Where the crop window sits over the displayed image, in CSS pixels.\n *\n * From the inline styles the component writes — a measured box would be 0×0 — and\n * in DISPLAY pixels, which is not what a consumer receives: `cropRect()` converts\n * to the image's natural pixels, and that conversion is the component's own contract\n * rather than something the DOM shows.\n */\n async getCropBox(): Promise<WrCropWindowBox> {\n const window_ = await this.requireReady('getCropBox');\n return {\n x: Number.parseFloat(await window_.getCssValue('left')),\n y: Number.parseFloat(await window_.getCssValue('top')),\n width: Number.parseFloat(await window_.getCssValue('width')),\n height: Number.parseFloat(await window_.getCssValue('height')),\n };\n }\n\n /** The eight resize handles, by their compass suffix. Throws before the crop UI is up. */\n async getHandles(): Promise<WrCropHandle[]> {\n await this.requireReady('getHandles');\n const handles = await this.locatorForAll('.wr-image-cropper__handle')();\n\n return Promise.all(\n handles.map(async handle => {\n const classes = (await handle.getAttribute('class')) ?? '';\n const found = classes.split(/\\s+/).find(name => name.startsWith('wr-image-cropper__handle--'));\n return (found?.slice('wr-image-cropper__handle--'.length) ?? '') as WrCropHandle;\n })\n );\n }\n\n /** Whether the dimming backdrop with the cut-out is drawn. */\n async hasBackdrop(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-image-cropper__backdrop')()) !== null;\n }\n\n /**\n * Press an arrow key on the crop window — one display pixel, or ten with\n * `shift`; `alt` resizes from the east / south edge instead of moving.\n *\n * Sent to the crop window, where the component listens, so it does not depend on\n * jsdom having managed to focus anything. `sendKeys` fires keyup as well as\n * keydown, which is what makes `(cropped)` emit — the component treats a run of\n * arrows as one gesture, the way a drag is one.\n */\n async pressArrow(arrow: WrCropArrow, modifiers: { alt?: boolean; shift?: boolean } = {}): Promise<void> {\n const window_ = await this.requireReady('pressArrow');\n const keys = ARROWS[arrow];\n await (modifiers.alt || modifiers.shift ? window_.sendKeys(modifiers, keys) : window_.sendKeys(keys));\n }\n\n /** The crop window's accessible name. Comes from the catalog. */\n async getCropWindowLabel(): Promise<string | null> {\n return (await this.requireReady('getCropWindowLabel')).getAttribute('aria-label');\n }\n\n /**\n * The last thing the live region announced — the crop rect in SOURCE pixels,\n * as `x, y, width × height`.\n *\n * Empty until a key has moved the crop: a pointer drag deliberately says nothing\n * (it would flood the region and the sighted user can see it), and the initial\n * crop is not news. Two questions, not one — {@link getCropBox} reads the window's\n * DISPLAY geometry, and a component that moved the box without announcing it\n * would still pass that one.\n */\n async getAnnouncement(): Promise<string> {\n await this.requireReady('getAnnouncement');\n const status = await this.locatorForOptional('[role=\"status\"]')();\n return status ? status.text() : '';\n }\n\n private async requireReady(method: string): Promise<TestElement> {\n const window_ = await this.cropWindow();\n if (window_) return window_;\n\n throw new Error(\n `WrImageCropperHarness.${method}(): the crop window is not rendered. The cropper sizes it from the ` +\n \"image's MEASURED box, and a unit test has none — give the <img> a `getBoundingClientRect` and a \" +\n '`naturalWidth`, then call dispatchImageLoad().'\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-image-cropper-testing.mjs","sources":["../../../projects/lib/image-cropper/testing/wr-image-cropper-harness.ts","../../../projects/lib/image-cropper/testing/ngwr-image-cropper-testing.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, TestKey, type TestElement } from '@angular/cdk/testing';\n\nimport type { WrCropHandle } from 'ngwr/image-cropper';\n\nimport type { WrImageCropperHarnessFilters } from './interfaces';\n\nconst ARROWS = {\n left: TestKey.LEFT_ARROW,\n right: TestKey.RIGHT_ARROW,\n up: TestKey.UP_ARROW,\n down: TestKey.DOWN_ARROW,\n} as const;\n\n/** The four directions the crop window's keyboard model understands. */\nexport type WrCropArrow = keyof typeof ARROWS;\n\n/** Where the crop window sits over the DISPLAYED image, in CSS pixels. */\nexport interface WrCropWindowBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Test harness for `<wr-image-cropper>`.\n *\n * **This is the smallest honest surface of any harness here, and the reason is worth\n * reading before you reach for a method that is not on it.** Almost everything the\n * cropper does is measured: it reads the `<img>`'s rendered box and its\n * `naturalWidth`, converts between display and source pixels, and moves the crop\n * window with pointer deltas. jsdom measures nothing — every rect is 0×0 and\n * `naturalWidth` is 0 — so an image never \"loads\", the crop UI never renders, and a\n * synthetic drag divides by zero. There is deliberately no drag method: it would\n * write `NaN` and report success.\n *\n * What a spec CAN do is give the image a box and a natural size itself, then call\n * {@link dispatchImageLoad} — the two numbers `onImageLoad` reads are the whole\n * stub, and from there {@link getCropBox} and {@link getHandles} are real answers,\n * because the component writes the crop window's geometry as inline pixels. The\n * KEYBOARD path needs the same stub to get the crop UI up, but nothing beyond it:\n * its step is a constant rather than a pointer delta, so {@link pressArrow} lands\n * on a number a spec can name.\n *\n * ```ts\n * // In your spec, before dispatching the load:\n * const img = fixture.nativeElement.querySelector('.wr-image-cropper__image');\n * img.getBoundingClientRect = () => ({ width: 400, height: 400, x: 0, y: 0 }) as DOMRect;\n * Object.defineProperty(img, 'naturalWidth', { value: 800 });\n * Object.defineProperty(img, 'naturalHeight', { value: 800 });\n * ```\n *\n * **There is nothing to click.** The cropper renders no buttons: the result\n * arrives on the `(cropped)` output, which emits a `Blob` after each drag end or\n * each run of arrow keys, and a consumer who wants a one-off read calls `toBlob()` /\n * `toDataUrl()` — or `cropRect()` for the geometry — on the component through a\n * `viewChild`. The same goes for `(loadError)` and `status()`, which is why\n * {@link isFailed} reads the failure off the host class instead. Outputs and instance\n * members are component API rather than DOM, so all of it is outside what a harness\n * can reach at all, which is worth knowing rather than discovering.\n *\n * @example\n * ```ts\n * const cropper = await loader.getHarness(WrImageCropperHarness);\n *\n * expect(await cropper.isEmpty()).toBe(false);\n * expect(await cropper.isReady()).toBe(false); // nothing measured yet\n *\n * await cropper.dispatchImageLoad();\n * expect(await cropper.getCropBox()).toEqual({ x: 80, y: 80, width: 240, height: 240 });\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrImageCropperHarness extends ComponentHarness {\n static hostSelector = 'wr-image-cropper';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrImageCropperHarnessFilters = {}): HarnessPredicate<WrImageCropperHarness> {\n return new HarnessPredicate(WrImageCropperHarness, options).addOption(\n 'empty',\n options.empty,\n async (harness, empty) => (await harness.isEmpty()) === empty\n );\n }\n\n private readonly image = this.locatorForOptional('.wr-image-cropper__image');\n private readonly cropWindow = this.locatorForOptional('.wr-image-cropper__window');\n\n /** Whether the cropper has no source at all, and is showing its placeholder. */\n async isEmpty(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-image-cropper__empty')()) !== null;\n }\n\n /** The placeholder's text, or `null` when there is an image. Comes from the catalog. */\n async getEmptyText(): Promise<string | null> {\n const empty = await this.locatorForOptional('.wr-image-cropper__empty')();\n return empty ? empty.text() : null;\n }\n\n /**\n * Whether the crop UI is up.\n *\n * Not the same question as {@link isEmpty}: a cropper with a perfectly good `src`\n * shows nothing but the image until it has been MEASURED, because the window's size\n * is a fraction of the rendered box. In a browser that happens on load; in a unit\n * test it happens when the spec stubs the box and calls\n * {@link dispatchImageLoad}.\n */\n async isReady(): Promise<boolean> {\n return (await this.cropWindow()) !== null;\n }\n\n /**\n * What the `<img>` is pointing at.\n *\n * A `File` or `Blob` source becomes an object URL, so this is `blob:…` for those\n * and the plain string for a string `src`. Comparing the two is not useful; asking\n * whether it CHANGED is.\n */\n async getImageSrc(): Promise<string | null> {\n const image = await this.image();\n return image ? image.getProperty<string>('src') : null;\n }\n\n /**\n * Fire the image's `load` event.\n *\n * The event, not a real load: jsdom fetches nothing, and the handler reads\n * `getBoundingClientRect()` and `naturalWidth` off the element. Stub those first —\n * see this class's docs — or the crop window will still not render and\n * {@link isReady} will stay `false`, which is exactly what a real cropper does with\n * a zero-sized image.\n */\n async dispatchImageLoad(): Promise<void> {\n const image = await this.image();\n if (!image) {\n throw new Error(\n 'WrImageCropperHarness.dispatchImageLoad(): there is no image — the cropper has no `src`, so it is ' +\n 'showing its empty state.'\n );\n }\n await image.dispatchEvent('load');\n }\n\n /**\n * Fire the image's `error` event — what a browser does with a source it cannot\n * decode, and the only way to get one in a unit test, where nothing is decoded.\n *\n * Afterwards {@link isFailed} is `true` and the component has emitted\n * `(loadError)`; a spec that wants the payload binds that output on its host.\n */\n async dispatchImageError(): Promise<void> {\n const image = await this.image();\n if (!image) {\n throw new Error(\n 'WrImageCropperHarness.dispatchImageError(): there is no image — the cropper has no `src`, so it is ' +\n 'showing its empty state and there is nothing to fail.'\n );\n }\n await image.dispatchEvent('error');\n }\n\n /**\n * Whether the browser failed to decode the current source.\n *\n * Read off the `wr-image-cropper--failed` host modifier — the part of the failure\n * the DOM shows. It is a third answer beside {@link isEmpty} and {@link isReady},\n * not a combination of them: a failed cropper has a `src` (so it is not empty) and\n * no crop window (so it is not ready), which is also exactly what a cropper still\n * LOADING looks like. Asking those two would call a broken image a slow one.\n *\n * The component renders no message for it, so there is no text to read here; the\n * host's own message is the host's to assert.\n */\n async isFailed(): Promise<boolean> {\n return (await this.host()).hasClass('wr-image-cropper--failed');\n }\n\n /**\n * Where the crop window sits over the displayed image, in CSS pixels.\n *\n * From the inline styles the component writes — a measured box would be 0×0 — and\n * in DISPLAY pixels, which is not what a consumer receives: `cropRect()` converts\n * to the image's natural pixels, and that conversion is the component's own contract\n * rather than something the DOM shows.\n */\n async getCropBox(): Promise<WrCropWindowBox> {\n const window_ = await this.requireReady('getCropBox');\n return {\n x: Number.parseFloat(await window_.getCssValue('left')),\n y: Number.parseFloat(await window_.getCssValue('top')),\n width: Number.parseFloat(await window_.getCssValue('width')),\n height: Number.parseFloat(await window_.getCssValue('height')),\n };\n }\n\n /** The eight resize handles, by their compass suffix. Throws before the crop UI is up. */\n async getHandles(): Promise<WrCropHandle[]> {\n await this.requireReady('getHandles');\n const handles = await this.locatorForAll('.wr-image-cropper__handle')();\n\n return Promise.all(\n handles.map(async handle => {\n const classes = (await handle.getAttribute('class')) ?? '';\n const found = classes.split(/\\s+/).find(name => name.startsWith('wr-image-cropper__handle--'));\n return (found?.slice('wr-image-cropper__handle--'.length) ?? '') as WrCropHandle;\n })\n );\n }\n\n /** Whether the dimming backdrop with the cut-out is drawn. */\n async hasBackdrop(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-image-cropper__backdrop')()) !== null;\n }\n\n /**\n * Press an arrow key on the crop window — one display pixel, or ten with\n * `shift`; `alt` resizes from the east / south edge instead of moving.\n *\n * Sent to the crop window, where the component listens, so it does not depend on\n * jsdom having managed to focus anything. `sendKeys` fires keyup as well as\n * keydown, which is what makes `(cropped)` emit — the component treats a run of\n * arrows as one gesture, the way a drag is one.\n */\n async pressArrow(arrow: WrCropArrow, modifiers: { alt?: boolean; shift?: boolean } = {}): Promise<void> {\n const window_ = await this.requireReady('pressArrow');\n const keys = ARROWS[arrow];\n await (modifiers.alt || modifiers.shift ? window_.sendKeys(modifiers, keys) : window_.sendKeys(keys));\n }\n\n /** The crop window's accessible name. Comes from the catalog. */\n async getCropWindowLabel(): Promise<string | null> {\n return (await this.requireReady('getCropWindowLabel')).getAttribute('aria-label');\n }\n\n /**\n * The last thing the live region announced — the crop rect in SOURCE pixels,\n * as `x, y, width × height`.\n *\n * Empty until a key has moved the crop: a pointer drag deliberately says nothing\n * (it would flood the region and the sighted user can see it), and the initial\n * crop is not news. Two questions, not one — {@link getCropBox} reads the window's\n * DISPLAY geometry, and a component that moved the box without announcing it\n * would still pass that one.\n */\n async getAnnouncement(): Promise<string> {\n await this.requireReady('getAnnouncement');\n const status = await this.locatorForOptional('[role=\"status\"]')();\n return status ? status.text() : '';\n }\n\n private async requireReady(method: string): Promise<TestElement> {\n const window_ = await this.cropWindow();\n if (window_) return window_;\n\n throw new Error(\n `WrImageCropperHarness.${method}(): the crop window is not rendered. The cropper sizes it from the ` +\n \"image's MEASURED box, and a unit test has none — give the <img> a `getBoundingClientRect` and a \" +\n '`naturalWidth`, then call dispatchImageLoad().'\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,17 @@ import { randomId, clamp } from 'ngwr/utils';
|
|
|
10
10
|
* Use of this source code is governed by an MIT-style license that can be
|
|
11
11
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* `[maxOutputSize]`'s transform. Anything that is not a positive, finite number —
|
|
15
|
+
* `null`, an empty attribute, `0`, a typo — means "no cap" rather than a 1px export:
|
|
16
|
+
* a bad config should cost resolution nobody asked to lose, not the whole image.
|
|
17
|
+
*/
|
|
18
|
+
function coerceMaxOutputSize(value) {
|
|
19
|
+
if (value === null || value === undefined || value === '')
|
|
20
|
+
return null;
|
|
21
|
+
const n = coerceNumberProperty(value, Number.NaN);
|
|
22
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
23
|
+
}
|
|
13
24
|
/**
|
|
14
25
|
* Stands in for the consumer input `useI18nText` expects. None of this
|
|
15
26
|
* component's three strings has a `*Label` input behind it — the catalog is the
|
|
@@ -31,12 +42,18 @@ const KEY_STEP_COARSE = 10;
|
|
|
31
42
|
* freshly-rendered `Blob` of the cropped region. For one-off reads use
|
|
32
43
|
* `toBlob()` / `toDataUrl()`.
|
|
33
44
|
*
|
|
45
|
+
* A source the browser cannot decode fires `(loadError)` once, sets `status()` to
|
|
46
|
+
* `'failed'` and puts `wr-image-cropper--failed` on the host. The component renders
|
|
47
|
+
* no message of its own for it — what went wrong and what to do next is the host's
|
|
48
|
+
* to say.
|
|
49
|
+
*
|
|
34
50
|
* @example
|
|
35
51
|
* ```html
|
|
36
52
|
* <wr-image-cropper
|
|
37
53
|
* [src]="file"
|
|
38
54
|
* [aspectRatio]="1"
|
|
39
55
|
* (cropped)="onBlob($event)"
|
|
56
|
+
* (loadError)="onUnreadable($event)"
|
|
40
57
|
* />
|
|
41
58
|
* ```
|
|
42
59
|
*
|
|
@@ -59,8 +76,23 @@ class WrImageCropper {
|
|
|
59
76
|
/** JPEG / WebP quality for `(cropped)` in [0, 1]. @default 0.92 */
|
|
60
77
|
outputQuality = input(0.92, /* @ts-ignore */
|
|
61
78
|
...(ngDevMode ? [{ debugName: "outputQuality" }] : /* istanbul ignore next */ []));
|
|
79
|
+
/**
|
|
80
|
+
* Longest side of the exported image, in pixels — for `(cropped)`, `toBlob()` and
|
|
81
|
+
* `toDataUrl()` alike. A crop larger than this is scaled down to fit, keeping its
|
|
82
|
+
* aspect ratio; a smaller one is exported as it is, never upscaled. The crop itself
|
|
83
|
+
* — `cropRect()`, in source pixels — is not affected. `null` (or anything that is
|
|
84
|
+
* not a positive number) exports at the source image's own resolution.
|
|
85
|
+
* @default null
|
|
86
|
+
*/
|
|
87
|
+
maxOutputSize = input(null, { ...(ngDevMode ? { debugName: "maxOutputSize" } : /* istanbul ignore next */ {}), transform: coerceMaxOutputSize });
|
|
62
88
|
/** Emits a Blob after each drag end. */
|
|
63
89
|
cropped = output();
|
|
90
|
+
/**
|
|
91
|
+
* Fires once when the browser cannot decode the current `src` — a format it does
|
|
92
|
+
* not support, a broken file, a URL that fails. Nothing is rendered for it; show
|
|
93
|
+
* your own message. A new `src` clears the failure.
|
|
94
|
+
*/
|
|
95
|
+
loadError = output();
|
|
64
96
|
imgEl = viewChild.required('img', /* @ts-ignore */
|
|
65
97
|
...(ngDevMode ? [{ debugName: "imgEl" }] : /* istanbul ignore next */ []));
|
|
66
98
|
/** Resolved object URL for `src` (so File / Blob render in `<img>`). */
|
|
@@ -97,6 +129,14 @@ class WrImageCropper {
|
|
|
97
129
|
startPointer = { x: 0, y: 0 };
|
|
98
130
|
startRect = { x: 0, y: 0, w: 0, h: 0 };
|
|
99
131
|
destroyRef = inject(DestroyRef);
|
|
132
|
+
statusState = signal('empty', /* @ts-ignore */
|
|
133
|
+
...(ngDevMode ? [{ debugName: "statusState" }] : /* istanbul ignore next */ []));
|
|
134
|
+
/**
|
|
135
|
+
* Where the source stands: `'empty'`, `'loading'`, `'loaded'` or `'failed'`. Reset
|
|
136
|
+
* to `'loading'` (or `'empty'`) whenever `src` changes, so a failure never outlives
|
|
137
|
+
* the source that caused it.
|
|
138
|
+
*/
|
|
139
|
+
status = this.statusState.asReadonly();
|
|
100
140
|
/** Resolved crop rect in natural (source) pixel coordinates. */
|
|
101
141
|
cropRect = computed(() => {
|
|
102
142
|
const display = this.display();
|
|
@@ -132,8 +172,13 @@ class WrImageCropper {
|
|
|
132
172
|
this.resetGeometry();
|
|
133
173
|
if (!src) {
|
|
134
174
|
this.objectUrl.set(null);
|
|
175
|
+
this.statusState.set('empty');
|
|
135
176
|
return;
|
|
136
177
|
}
|
|
178
|
+
// A new source starts over: a failure belongs to the source that caused it, and
|
|
179
|
+
// left in place it would mark a perfectly good next image as broken until the
|
|
180
|
+
// browser answered for that one too.
|
|
181
|
+
this.statusState.set('loading');
|
|
137
182
|
if (typeof src === 'string') {
|
|
138
183
|
this.objectUrl.set(src);
|
|
139
184
|
}
|
|
@@ -175,11 +220,38 @@ class WrImageCropper {
|
|
|
175
220
|
w: img.naturalWidth || display.w,
|
|
176
221
|
h: img.naturalHeight || display.h,
|
|
177
222
|
};
|
|
223
|
+
this.statusState.set('loaded');
|
|
178
224
|
this.natural.set(natural);
|
|
179
225
|
this.display.set(display);
|
|
180
226
|
this.cropDisplay.set(this.initialCrop(display));
|
|
181
227
|
this.watchResize(img);
|
|
182
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The `<img>` could not decode its source.
|
|
231
|
+
*
|
|
232
|
+
* Before this handler existed a failed decode left the component exactly as it
|
|
233
|
+
* looks while loading — an image with no size, no crop window, nothing emitted —
|
|
234
|
+
* so a host had no way to tell "still decoding" from "never going to", and a user
|
|
235
|
+
* who picked a `.heic` the browser cannot read saw an empty box and no reason.
|
|
236
|
+
*
|
|
237
|
+
* Once per source: `status` is reset by a new `src` and by nothing else, so a
|
|
238
|
+
* second `error` for the same source says nothing new and is not re-emitted.
|
|
239
|
+
*/
|
|
240
|
+
onImageError() {
|
|
241
|
+
const url = this.objectUrl();
|
|
242
|
+
if (!url || this.statusState() === 'failed')
|
|
243
|
+
return;
|
|
244
|
+
this.statusState.set('failed');
|
|
245
|
+
this.resetGeometry();
|
|
246
|
+
const src = this.src();
|
|
247
|
+
const isBlob = typeof src !== 'string' && src !== null;
|
|
248
|
+
this.loadError.emit({
|
|
249
|
+
url,
|
|
250
|
+
name: typeof File !== 'undefined' && src instanceof File ? src.name : null,
|
|
251
|
+
type: isBlob ? src.type : null,
|
|
252
|
+
size: isBlob ? src.size : null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
183
255
|
/**
|
|
184
256
|
* Keep `display` on the box the image is actually rendered at.
|
|
185
257
|
*
|
|
@@ -432,12 +504,30 @@ class WrImageCropper {
|
|
|
432
504
|
await this.emitCropped();
|
|
433
505
|
}
|
|
434
506
|
// Internals
|
|
507
|
+
/**
|
|
508
|
+
* The exported image's pixel size: the crop in source pixels, scaled down so its
|
|
509
|
+
* longer side fits `maxOutputSize`. `Math.min(1, …)` is the never-upscale rule — a
|
|
510
|
+
* crop already inside the cap keeps its own size, which is also the whole of the
|
|
511
|
+
* uncapped path.
|
|
512
|
+
*/
|
|
513
|
+
outputSize(c) {
|
|
514
|
+
const cap = this.maxOutputSize();
|
|
515
|
+
const longest = Math.max(c.width, c.height);
|
|
516
|
+
const scale = cap === null || longest === 0 ? 1 : Math.min(1, cap / longest);
|
|
517
|
+
return {
|
|
518
|
+
width: Math.max(1, Math.round(c.width * scale)),
|
|
519
|
+
height: Math.max(1, Math.round(c.height * scale)),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
435
522
|
toCanvas() {
|
|
436
523
|
const img = this.imgEl().nativeElement;
|
|
437
524
|
const c = this.cropRect();
|
|
438
525
|
const canvas = document.createElement('canvas');
|
|
439
|
-
|
|
440
|
-
canvas.
|
|
526
|
+
const size = this.outputSize(c);
|
|
527
|
+
canvas.width = size.width;
|
|
528
|
+
canvas.height = size.height;
|
|
529
|
+
// The SOURCE rect stays the whole crop and the destination is the canvas, so a
|
|
530
|
+
// capped export is the same region drawn smaller rather than a smaller region.
|
|
441
531
|
const ctx = canvas.getContext('2d');
|
|
442
532
|
if (ctx)
|
|
443
533
|
ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, canvas.width, canvas.height);
|
|
@@ -453,12 +543,15 @@ class WrImageCropper {
|
|
|
453
543
|
}
|
|
454
544
|
}
|
|
455
545
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
456
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrImageCropper, isStandalone: true, selector: "wr-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, outputType: { classPropertyName: "outputType", publicName: "outputType", isSignal: true, isRequired: false, transformFunction: null }, outputQuality: { classPropertyName: "outputQuality", publicName: "outputQuality", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropped: "cropped" }, host: { classAttribute: "wr-image-cropper" }, viewQueries: [{ propertyName: "imgEl", first: true, predicate: ["img"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img
|
|
546
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrImageCropper, isStandalone: true, selector: "wr-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, outputType: { classPropertyName: "outputType", publicName: "outputType", isSignal: true, isRequired: false, transformFunction: null }, outputQuality: { classPropertyName: "outputQuality", publicName: "outputQuality", isSignal: true, isRequired: false, transformFunction: null }, maxOutputSize: { classPropertyName: "maxOutputSize", publicName: "maxOutputSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropped: "cropped", loadError: "loadError" }, host: { properties: { "class.wr-image-cropper--failed": "status() === 'failed'" }, classAttribute: "wr-image-cropper" }, viewQueries: [{ propertyName: "imgEl", first: true, predicate: ["img"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img\n #img\n class=\"wr-image-cropper__image\"\n [src]=\"url\"\n alt=\"\"\n draggable=\"false\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n", encapsulation: i0.ViewEncapsulation.None });
|
|
457
547
|
}
|
|
458
548
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrImageCropper, decorators: [{
|
|
459
549
|
type: Component,
|
|
460
|
-
args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: {
|
|
461
|
-
|
|
550
|
+
args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: {
|
|
551
|
+
class: 'wr-image-cropper',
|
|
552
|
+
'[class.wr-image-cropper--failed]': "status() === 'failed'",
|
|
553
|
+
}, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img\n #img\n class=\"wr-image-cropper__image\"\n [src]=\"url\"\n alt=\"\"\n draggable=\"false\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n" }]
|
|
554
|
+
}], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], outputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputType", required: false }] }], outputQuality: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputQuality", required: false }] }], maxOutputSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxOutputSize", required: false }] }], cropped: [{ type: i0.Output, args: ["cropped"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], imgEl: [{ type: i0.ViewChild, args: ['img', { isSignal: true }] }] } });
|
|
462
555
|
|
|
463
556
|
/**
|
|
464
557
|
* Generated bundle index. Do not edit.
|