@egose/shadcn-theme-ng 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -1
- package/autocomplete/fesm2022/autocomplete.mjs +14 -4
- package/autocomplete/types/autocomplete.d.ts +4 -1
- package/checkbox/README.md +6 -2
- package/checkbox/fesm2022/checkbox.mjs +11 -7
- package/checkbox/types/checkbox.d.ts +6 -4
- package/combobox/fesm2022/combobox.mjs +13 -7
- package/combobox/types/combobox.d.ts +8 -2
- package/data-table/README.md +10 -0
- package/data-table/fesm2022/data-table.mjs +48 -12
- package/data-table/types/data-table.d.ts +16 -4
- package/date-picker/README.md +41 -9
- package/date-picker/fesm2022/date-picker.mjs +265 -59
- package/date-picker/types/date-picker.d.ts +58 -27
- package/form-autocomplete/README.md +4 -2
- package/form-autocomplete/fesm2022/form-autocomplete.mjs +19 -6
- package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
- package/form-combobox/README.md +4 -2
- package/form-combobox/fesm2022/form-combobox.mjs +23 -6
- package/form-combobox/types/form-combobox.d.ts +10 -3
- package/form-input-otp/README.md +7 -5
- package/form-input-otp/fesm2022/form-input-otp.mjs +20 -8
- package/form-input-otp/types/form-input-otp.d.ts +9 -3
- package/form-searchable-multiselect/README.md +15 -1
- package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +15 -3
- package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
- package/form-slider/README.md +3 -1
- package/form-slider/fesm2022/form-slider.mjs +19 -6
- package/form-slider/types/form-slider.d.ts +9 -3
- package/form-toggle/README.md +22 -18
- package/form-toggle/fesm2022/form-toggle.mjs +22 -9
- package/form-toggle/types/form-toggle.d.ts +6 -2
- package/input-otp/README.md +33 -9
- package/input-otp/fesm2022/input-otp.mjs +127 -11
- package/input-otp/types/input-otp.d.ts +24 -4
- package/package.json +1 -1
- package/pagination/README.md +21 -12
- package/pagination/fesm2022/pagination.mjs +73 -65
- package/pagination/types/pagination.d.ts +21 -8
- package/phone-input/README.md +16 -0
- package/phone-input/fesm2022/phone-input.mjs +41 -3
- package/phone-input/types/phone-input.d.ts +5 -2
- package/searchable-multiselect/README.md +62 -40
- package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +59 -32
- package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
- package/slider/fesm2022/slider.mjs +9 -5
- package/slider/types/slider.d.ts +6 -2
- package/stepper/README.md +39 -19
- package/stepper/fesm2022/stepper.mjs +64 -12
- package/stepper/types/stepper.d.ts +12 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, Directive, ChangeDetectionStrategy, Component, booleanAttribute, computed, model, numberAttribute, untracked, NgModule } from '@angular/core';
|
|
2
|
+
import { input, Directive, ChangeDetectionStrategy, Component, booleanAttribute, computed, model, numberAttribute, effect, untracked, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1$1 from '@egose/shadcn-theme-ng/select';
|
|
4
4
|
import { HlmSelectImports } from '@egose/shadcn-theme-ng/select';
|
|
5
5
|
import { classes, hlm } from '@egose/shadcn-theme-ng/utils';
|
|
@@ -207,14 +207,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
207
207
|
}]
|
|
208
208
|
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], queryParamsHandling: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParamsHandling", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }] } });
|
|
209
209
|
|
|
210
|
+
/** Shared arithmetic policy; invalid totals/sizes represent a single page. */
|
|
211
|
+
function pageCount(totalItems, itemsPerPage) {
|
|
212
|
+
if (!Number.isFinite(totalItems) || totalItems <= 0 || !Number.isFinite(itemsPerPage) || itemsPerPage <= 0) {
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, Math.ceil(totalItems / itemsPerPage)));
|
|
216
|
+
}
|
|
217
|
+
|
|
210
218
|
class HlmNumberedPagination {
|
|
211
219
|
/**
|
|
212
|
-
* The current (active) page.
|
|
220
|
+
* The current (active) page, floored and clamped to the available range.
|
|
221
|
+
* Corrections emit currentPageChange once; empty/invalid paging uses page 1.
|
|
213
222
|
*/
|
|
214
223
|
currentPage = model.required(/* @ts-ignore */
|
|
215
224
|
...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
|
|
216
225
|
/**
|
|
217
|
-
* The number of items per paginated page.
|
|
226
|
+
* The number of items per paginated page. Non-finite/non-positive sizes
|
|
227
|
+
* disable paging (one page) without rewriting this model.
|
|
218
228
|
*/
|
|
219
229
|
itemsPerPage = model.required(/* @ts-ignore */
|
|
220
230
|
...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
|
|
@@ -225,7 +235,8 @@ class HlmNumberedPagination {
|
|
|
225
235
|
*/
|
|
226
236
|
totalItems = input.required({ ...(ngDevMode ? { debugName: "totalItems" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
227
237
|
/**
|
|
228
|
-
*
|
|
238
|
+
* Maximum window entries, including ellipses: floored and bounded to 1–100.
|
|
239
|
+
* Non-finite/non-positive values use the default of 7.
|
|
229
240
|
*/
|
|
230
241
|
maxSize = input(7, { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
231
242
|
/**
|
|
@@ -245,33 +256,29 @@ class HlmNumberedPagination {
|
|
|
245
256
|
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
|
|
246
257
|
}, /* @ts-ignore */
|
|
247
258
|
...(ngDevMode ? [{ debugName: "_pageSizesWithCurrent" }] : /* istanbul ignore next */ []));
|
|
248
|
-
|
|
259
|
+
_currentPage = computed(() => outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage()), /* @ts-ignore */
|
|
260
|
+
...(ngDevMode ? [{ debugName: "_currentPage" }] : /* istanbul ignore next */ []));
|
|
261
|
+
_isFirstPageActive = computed(() => this._currentPage() === 1, /* @ts-ignore */
|
|
249
262
|
...(ngDevMode ? [{ debugName: "_isFirstPageActive" }] : /* istanbul ignore next */ []));
|
|
250
|
-
_isLastPageActive = computed(() => this.
|
|
263
|
+
_isLastPageActive = computed(() => this._currentPage() === this._lastPageNumber(), /* @ts-ignore */
|
|
251
264
|
...(ngDevMode ? [{ debugName: "_isLastPageActive" }] : /* istanbul ignore next */ []));
|
|
252
|
-
_lastPageNumber = computed(() =>
|
|
253
|
-
if (this.totalItems() < 1) {
|
|
254
|
-
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
|
|
255
|
-
// but it makes sense to consider a single, empty page as the last page.
|
|
256
|
-
return 1;
|
|
257
|
-
}
|
|
258
|
-
return Math.ceil(this.totalItems() / this.itemsPerPage());
|
|
259
|
-
}, /* @ts-ignore */
|
|
265
|
+
_lastPageNumber = computed(() => pageCount(this.totalItems(), this.itemsPerPage()), /* @ts-ignore */
|
|
260
266
|
...(ngDevMode ? [{ debugName: "_lastPageNumber" }] : /* istanbul ignore next */ []));
|
|
261
|
-
_pages = computed(() =>
|
|
262
|
-
const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
|
|
263
|
-
if (correctedCurrentPage !== this.currentPage()) {
|
|
264
|
-
// update the current page
|
|
265
|
-
untracked(() => this.currentPage.set(correctedCurrentPage));
|
|
266
|
-
}
|
|
267
|
-
return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
|
|
268
|
-
}, /* @ts-ignore */
|
|
267
|
+
_pages = computed(() => createPageArray(this._currentPage(), this.itemsPerPage(), this.totalItems(), this.maxSize()), /* @ts-ignore */
|
|
269
268
|
...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
|
|
269
|
+
constructor() {
|
|
270
|
+
effect(() => {
|
|
271
|
+
const page = this._currentPage();
|
|
272
|
+
if (page !== this.currentPage()) {
|
|
273
|
+
untracked(() => this.currentPage.set(page));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
270
277
|
goToPrevious() {
|
|
271
|
-
this.currentPage.set(this.
|
|
278
|
+
this.currentPage.set(Math.max(1, this._currentPage() - 1));
|
|
272
279
|
}
|
|
273
280
|
goToNext() {
|
|
274
|
-
this.currentPage.set(this.
|
|
281
|
+
this.currentPage.set(Math.min(this._lastPageNumber(), this._currentPage() + 1));
|
|
275
282
|
}
|
|
276
283
|
goToFirst() {
|
|
277
284
|
this.currentPage.set(1);
|
|
@@ -280,7 +287,7 @@ class HlmNumberedPagination {
|
|
|
280
287
|
this.currentPage.set(this._lastPageNumber());
|
|
281
288
|
}
|
|
282
289
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNumberedPagination, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
283
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmNumberedPagination, isStandalone: true, selector: "hlm-numbered-pagination", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: true, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: true, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, showEdges: { classPropertyName: "showEdges", publicName: "showEdges", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", itemsPerPage: "itemsPerPageChange" }, ngImport: i0, template: "\n <div class=\"flex items-center justify-between gap-2 px-4 py-2\">\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem (click)=\"goToPrevious()\">\n <hlm-pagination-previous />\n </li>\n }\n\n @for (page of _pages(); track
|
|
290
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmNumberedPagination, isStandalone: true, selector: "hlm-numbered-pagination", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: true, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: true, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, showEdges: { classPropertyName: "showEdges", publicName: "showEdges", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", itemsPerPage: "itemsPerPageChange" }, ngImport: i0, template: "\n <div class=\"flex items-center justify-between gap-2 px-4 py-2\">\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem (click)=\"goToPrevious()\">\n <hlm-pagination-previous />\n </li>\n }\n\n @for (page of _pages(); track $index) {\n <li hlmPaginationItem>\n @if (page === '...') {\n <hlm-pagination-ellipsis />\n } @else {\n <a hlmPaginationLink [isActive]=\"_currentPage() === page\" (click)=\"currentPage.set(page)\">\n {{ page }}\n </a>\n }\n </li>\n }\n\n @if (showEdges() && !_isLastPageActive()) {\n <li hlmPaginationItem (click)=\"goToNext()\">\n <hlm-pagination-next />\n </li>\n }\n </ul>\n </nav>\n\n <!-- Show Page Size selector -->\n <hlm-select [(value)]=\"itemsPerPage\" class=\"ml-auto\">\n <hlm-select-trigger class=\"w-fit\">\n <hlm-select-value />\n </hlm-select-trigger>\n <hlm-select-content *hlmSelectPortal>\n <hlm-select-group>\n @for (pageSize of _pageSizesWithCurrent(); track pageSize) {\n <hlm-select-item [value]=\"pageSize\">{{ pageSize }}</hlm-select-item>\n }\n </hlm-select-group>\n </hlm-select-content>\n </hlm-select>\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmPagination, selector: "[hlmPagination],hlm-pagination", inputs: ["aria-label"] }, { kind: "directive", type: HlmPaginationContent, selector: "ul[hlmPaginationContent]" }, { kind: "directive", type: HlmPaginationItem, selector: "li[hlmPaginationItem]" }, { kind: "component", type: HlmPaginationPrevious, selector: "hlm-pagination-previous", inputs: ["class", "link", "queryParams", "queryParamsHandling", "aria-label", "text", "iconOnly"] }, { kind: "component", type: HlmPaginationNext, selector: "hlm-pagination-next", inputs: ["class", "link", "queryParams", "queryParamsHandling", "aria-label", "text", "iconOnly"] }, { kind: "directive", type: HlmPaginationLink, selector: "[hlmPaginationLink]", inputs: ["isActive", "size", "link"] }, { kind: "component", type: HlmPaginationEllipsis, selector: "hlm-pagination-ellipsis", inputs: ["srOnlyText"] }, { kind: "directive", type: i1$1.HlmSelect, selector: "[hlmSelect],hlm-select" }, { kind: "component", type: i1$1.HlmSelectContent, selector: "hlm-select-content", inputs: ["showScroll"] }, { kind: "directive", type: i1$1.HlmSelectGroup, selector: "[hlmSelectGroup],hlm-select-group" }, { kind: "component", type: i1$1.HlmSelectItem, selector: "hlm-select-item" }, { kind: "directive", type: i1$1.HlmSelectPortal, selector: "[hlmSelectPortal]" }, { kind: "component", type: i1$1.HlmSelectTrigger, selector: "hlm-select-trigger", inputs: ["class", "buttonId", "ariaDescribedby", "wrapperDisabled", "size", "forceInvalid"] }, { kind: "directive", type: i1$1.HlmSelectValue, selector: "[hlmSelectValue],hlm-select-value" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
284
291
|
}
|
|
285
292
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNumberedPagination, decorators: [{
|
|
286
293
|
type: Component,
|
|
@@ -297,37 +304,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
297
304
|
HlmSelectImports,
|
|
298
305
|
],
|
|
299
306
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
300
|
-
template: "\n <div class=\"flex items-center justify-between gap-2 px-4 py-2\">\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem (click)=\"goToPrevious()\">\n <hlm-pagination-previous />\n </li>\n }\n\n @for (page of _pages(); track
|
|
307
|
+
template: "\n <div class=\"flex items-center justify-between gap-2 px-4 py-2\">\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem (click)=\"goToPrevious()\">\n <hlm-pagination-previous />\n </li>\n }\n\n @for (page of _pages(); track $index) {\n <li hlmPaginationItem>\n @if (page === '...') {\n <hlm-pagination-ellipsis />\n } @else {\n <a hlmPaginationLink [isActive]=\"_currentPage() === page\" (click)=\"currentPage.set(page)\">\n {{ page }}\n </a>\n }\n </li>\n }\n\n @if (showEdges() && !_isLastPageActive()) {\n <li hlmPaginationItem (click)=\"goToNext()\">\n <hlm-pagination-next />\n </li>\n }\n </ul>\n </nav>\n\n <!-- Show Page Size selector -->\n <hlm-select [(value)]=\"itemsPerPage\" class=\"ml-auto\">\n <hlm-select-trigger class=\"w-fit\">\n <hlm-select-value />\n </hlm-select-trigger>\n <hlm-select-content *hlmSelectPortal>\n <hlm-select-group>\n @for (pageSize of _pageSizesWithCurrent(); track pageSize) {\n <hlm-select-item [value]=\"pageSize\">{{ pageSize }}</hlm-select-item>\n }\n </hlm-select-group>\n </hlm-select-content>\n </hlm-select>\n </div>\n ",
|
|
301
308
|
}]
|
|
302
|
-
}], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: true }] }, { type: i0.Output, args: ["currentPageChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: true }] }, { type: i0.Output, args: ["itemsPerPageChange"] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], showEdges: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdges", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }] } });
|
|
309
|
+
}], ctorParameters: () => [], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: true }] }, { type: i0.Output, args: ["currentPageChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: true }] }, { type: i0.Output, args: ["itemsPerPageChange"] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], showEdges: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdges", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }] } });
|
|
303
310
|
/**
|
|
304
311
|
* Checks that the instance.currentPage property is within bounds for the current page range.
|
|
305
312
|
* If not, return a correct value for currentPage, or the current value if OK.
|
|
306
313
|
*
|
|
307
|
-
*
|
|
314
|
+
* Empty/invalid totals or sizes use page 1. Finite pages are floored;
|
|
315
|
+
* non-finite pages use 1. Page counts saturate at Number.MAX_SAFE_INTEGER.
|
|
308
316
|
*/
|
|
309
317
|
function outOfBoundCorrection(totalItems, itemsPerPage, currentPage) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
return totalPages;
|
|
313
|
-
}
|
|
314
|
-
if (currentPage < 1) {
|
|
315
|
-
return 1;
|
|
316
|
-
}
|
|
317
|
-
return currentPage;
|
|
318
|
+
const page = Number.isFinite(currentPage) ? Math.floor(currentPage) : 1;
|
|
319
|
+
return Math.max(1, Math.min(pageCount(totalItems, itemsPerPage), page));
|
|
318
320
|
}
|
|
319
321
|
/**
|
|
320
322
|
* Returns an array of Page objects to use in the pagination controls.
|
|
321
323
|
*
|
|
322
|
-
*
|
|
324
|
+
* Window math adapted from 'ngx-pagination'. Range is floored and bounded to
|
|
325
|
+
* 1–100 entries (including gaps); non-finite/non-positive ranges use 7.
|
|
326
|
+
* Ranges below 5 show a contiguous window containing the active page.
|
|
327
|
+
* Work and allocation are proportional to this bounded window, not total pages.
|
|
323
328
|
*/
|
|
324
329
|
function createPageArray(currentPage, itemsPerPage, totalItems, paginationRange) {
|
|
325
|
-
|
|
326
|
-
|
|
330
|
+
paginationRange =
|
|
331
|
+
Number.isFinite(paginationRange) && paginationRange > 0
|
|
332
|
+
? Math.min(100, Math.max(1, Math.floor(paginationRange)))
|
|
333
|
+
: 7;
|
|
327
334
|
const pages = [];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
const totalPages = pageCount(totalItems, itemsPerPage);
|
|
336
|
+
currentPage = outOfBoundCorrection(totalItems, itemsPerPage, currentPage);
|
|
337
|
+
if (paginationRange < 5) {
|
|
338
|
+
const length = Math.min(totalPages, paginationRange);
|
|
339
|
+
const start = Math.max(1, Math.min(currentPage - Math.floor(length / 2), totalPages - length + 1));
|
|
340
|
+
return Array.from({ length }, (_, index) => start + index);
|
|
341
|
+
}
|
|
331
342
|
const halfWay = Math.ceil(paginationRange / 2);
|
|
332
343
|
const isStart = currentPage <= halfWay;
|
|
333
344
|
const isEnd = totalPages - halfWay < currentPage;
|
|
@@ -378,12 +389,14 @@ function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
|
|
|
378
389
|
|
|
379
390
|
class HlmNumberedPaginationQueryParams {
|
|
380
391
|
/**
|
|
381
|
-
* The current
|
|
392
|
+
* The current page, floored and clamped; corrections emit currentPageChange.
|
|
393
|
+
* The parent owns reading/synchronizing the URL; corrections do not navigate.
|
|
382
394
|
*/
|
|
383
395
|
currentPage = model.required(/* @ts-ignore */
|
|
384
396
|
...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
|
|
385
397
|
/**
|
|
386
|
-
* The number of items per paginated page.
|
|
398
|
+
* The number of items per paginated page. Non-finite/non-positive sizes
|
|
399
|
+
* disable paging (one page) without rewriting this model.
|
|
387
400
|
*/
|
|
388
401
|
itemsPerPage = model.required(/* @ts-ignore */
|
|
389
402
|
...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
|
|
@@ -400,7 +413,8 @@ class HlmNumberedPaginationQueryParams {
|
|
|
400
413
|
link = input('.', /* @ts-ignore */
|
|
401
414
|
...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
|
|
402
415
|
/**
|
|
403
|
-
*
|
|
416
|
+
* Maximum window entries, including ellipses: floored and bounded to 1–100.
|
|
417
|
+
* Non-finite/non-positive values use the default of 7.
|
|
404
418
|
*/
|
|
405
419
|
maxSize = input(7, { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
406
420
|
/**
|
|
@@ -420,33 +434,27 @@ class HlmNumberedPaginationQueryParams {
|
|
|
420
434
|
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
|
|
421
435
|
}, /* @ts-ignore */
|
|
422
436
|
...(ngDevMode ? [{ debugName: "_pageSizesWithCurrent" }] : /* istanbul ignore next */ []));
|
|
423
|
-
|
|
437
|
+
_currentPage = computed(() => outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage()), /* @ts-ignore */
|
|
438
|
+
...(ngDevMode ? [{ debugName: "_currentPage" }] : /* istanbul ignore next */ []));
|
|
439
|
+
_isFirstPageActive = computed(() => this._currentPage() === 1, /* @ts-ignore */
|
|
424
440
|
...(ngDevMode ? [{ debugName: "_isFirstPageActive" }] : /* istanbul ignore next */ []));
|
|
425
|
-
_isLastPageActive = computed(() => this.
|
|
441
|
+
_isLastPageActive = computed(() => this._currentPage() === this._lastPageNumber(), /* @ts-ignore */
|
|
426
442
|
...(ngDevMode ? [{ debugName: "_isLastPageActive" }] : /* istanbul ignore next */ []));
|
|
427
|
-
_lastPageNumber = computed(() =>
|
|
428
|
-
if (this.totalItems() < 1) {
|
|
429
|
-
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
|
|
430
|
-
// but it makes sense to consider a single, empty page as the last page.
|
|
431
|
-
return 1;
|
|
432
|
-
}
|
|
433
|
-
return Math.ceil(this.totalItems() / this.itemsPerPage());
|
|
434
|
-
}, /* @ts-ignore */
|
|
443
|
+
_lastPageNumber = computed(() => pageCount(this.totalItems(), this.itemsPerPage()), /* @ts-ignore */
|
|
435
444
|
...(ngDevMode ? [{ debugName: "_lastPageNumber" }] : /* istanbul ignore next */ []));
|
|
436
|
-
_pages = computed(() =>
|
|
437
|
-
const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
|
|
438
|
-
if (correctedCurrentPage !== this.currentPage()) {
|
|
439
|
-
// update the current page
|
|
440
|
-
untracked(() => this.currentPage.set(correctedCurrentPage));
|
|
441
|
-
}
|
|
442
|
-
return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
|
|
443
|
-
}, /* @ts-ignore */
|
|
445
|
+
_pages = computed(() => createPageArray(this._currentPage(), this.itemsPerPage(), this.totalItems(), this.maxSize()), /* @ts-ignore */
|
|
444
446
|
...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
|
|
445
447
|
constructor() {
|
|
446
448
|
classes(() => "flex items-center justify-between gap-2 px-4 py-2");
|
|
449
|
+
effect(() => {
|
|
450
|
+
const page = this._currentPage();
|
|
451
|
+
if (page !== this.currentPage()) {
|
|
452
|
+
untracked(() => this.currentPage.set(page));
|
|
453
|
+
}
|
|
454
|
+
});
|
|
447
455
|
}
|
|
448
456
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNumberedPaginationQueryParams, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
449
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmNumberedPaginationQueryParams, isStandalone: true, selector: "hlm-numbered-pagination-query-params", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: true, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: true, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, showEdges: { classPropertyName: "showEdges", publicName: "showEdges", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", itemsPerPage: "itemsPerPageChange" }, ngImport: i0, template: "\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-previous\n [link]=\"link()\"\n [queryParams]=\"{ page:
|
|
457
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmNumberedPaginationQueryParams, isStandalone: true, selector: "hlm-numbered-pagination-query-params", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: true, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: true, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, showEdges: { classPropertyName: "showEdges", publicName: "showEdges", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", itemsPerPage: "itemsPerPageChange" }, ngImport: i0, template: "\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-previous\n [link]=\"link()\"\n [queryParams]=\"{ page: _currentPage() - 1 }\"\n queryParamsHandling=\"merge\"\n />\n </li>\n }\n\n @for (page of _pages(); track $index) {\n <li hlmPaginationItem>\n @if (page === '...') {\n <hlm-pagination-ellipsis />\n } @else {\n <a\n hlmPaginationLink\n [link]=\"_currentPage() !== page ? link() : undefined\"\n [queryParams]=\"{ page }\"\n queryParamsHandling=\"merge\"\n [isActive]=\"_currentPage() === page\"\n >\n {{ page }}\n </a>\n }\n </li>\n }\n\n @if (showEdges() && !_isLastPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-next\n [link]=\"link()\"\n [queryParams]=\"{ page: _currentPage() + 1 }\"\n queryParamsHandling=\"merge\"\n />\n </li>\n }\n </ul>\n </nav>\n\n <!-- Show Page Size selector -->\n <hlm-select [(value)]=\"itemsPerPage\" class=\"ml-auto\">\n <hlm-select-trigger class=\"w-fit\">\n <hlm-select-value />\n </hlm-select-trigger>\n <hlm-select-content *hlmSelectPortal>\n <hlm-select-group>\n @for (pageSize of _pageSizesWithCurrent(); track pageSize) {\n <hlm-select-item [value]=\"pageSize\">{{ pageSize }}</hlm-select-item>\n }\n </hlm-select-group>\n </hlm-select-content>\n </hlm-select>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmPagination, selector: "[hlmPagination],hlm-pagination", inputs: ["aria-label"] }, { kind: "directive", type: HlmPaginationContent, selector: "ul[hlmPaginationContent]" }, { kind: "directive", type: HlmPaginationItem, selector: "li[hlmPaginationItem]" }, { kind: "component", type: HlmPaginationPrevious, selector: "hlm-pagination-previous", inputs: ["class", "link", "queryParams", "queryParamsHandling", "aria-label", "text", "iconOnly"] }, { kind: "component", type: HlmPaginationNext, selector: "hlm-pagination-next", inputs: ["class", "link", "queryParams", "queryParamsHandling", "aria-label", "text", "iconOnly"] }, { kind: "directive", type: HlmPaginationLink, selector: "[hlmPaginationLink]", inputs: ["isActive", "size", "link"] }, { kind: "component", type: HlmPaginationEllipsis, selector: "hlm-pagination-ellipsis", inputs: ["srOnlyText"] }, { kind: "directive", type: i1$1.HlmSelect, selector: "[hlmSelect],hlm-select" }, { kind: "component", type: i1$1.HlmSelectContent, selector: "hlm-select-content", inputs: ["showScroll"] }, { kind: "directive", type: i1$1.HlmSelectGroup, selector: "[hlmSelectGroup],hlm-select-group" }, { kind: "component", type: i1$1.HlmSelectItem, selector: "hlm-select-item" }, { kind: "directive", type: i1$1.HlmSelectPortal, selector: "[hlmSelectPortal]" }, { kind: "component", type: i1$1.HlmSelectTrigger, selector: "hlm-select-trigger", inputs: ["class", "buttonId", "ariaDescribedby", "wrapperDisabled", "size", "forceInvalid"] }, { kind: "directive", type: i1$1.HlmSelectValue, selector: "[hlmSelectValue],hlm-select-value" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
450
458
|
}
|
|
451
459
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNumberedPaginationQueryParams, decorators: [{
|
|
452
460
|
type: Component,
|
|
@@ -463,7 +471,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
463
471
|
HlmSelectImports,
|
|
464
472
|
],
|
|
465
473
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
466
|
-
template: "\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-previous\n [link]=\"link()\"\n [queryParams]=\"{ page:
|
|
474
|
+
template: "\n <div class=\"flex items-center gap-1 text-sm text-nowrap text-gray-600\">\n <b>{{ totalItems() }}</b>\n total items |\n <b>{{ _lastPageNumber() }}</b>\n pages\n </div>\n\n <nav hlmPagination>\n <ul hlmPaginationContent>\n @if (showEdges() && !_isFirstPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-previous\n [link]=\"link()\"\n [queryParams]=\"{ page: _currentPage() - 1 }\"\n queryParamsHandling=\"merge\"\n />\n </li>\n }\n\n @for (page of _pages(); track $index) {\n <li hlmPaginationItem>\n @if (page === '...') {\n <hlm-pagination-ellipsis />\n } @else {\n <a\n hlmPaginationLink\n [link]=\"_currentPage() !== page ? link() : undefined\"\n [queryParams]=\"{ page }\"\n queryParamsHandling=\"merge\"\n [isActive]=\"_currentPage() === page\"\n >\n {{ page }}\n </a>\n }\n </li>\n }\n\n @if (showEdges() && !_isLastPageActive()) {\n <li hlmPaginationItem>\n <hlm-pagination-next\n [link]=\"link()\"\n [queryParams]=\"{ page: _currentPage() + 1 }\"\n queryParamsHandling=\"merge\"\n />\n </li>\n }\n </ul>\n </nav>\n\n <!-- Show Page Size selector -->\n <hlm-select [(value)]=\"itemsPerPage\" class=\"ml-auto\">\n <hlm-select-trigger class=\"w-fit\">\n <hlm-select-value />\n </hlm-select-trigger>\n <hlm-select-content *hlmSelectPortal>\n <hlm-select-group>\n @for (pageSize of _pageSizesWithCurrent(); track pageSize) {\n <hlm-select-item [value]=\"pageSize\">{{ pageSize }}</hlm-select-item>\n }\n </hlm-select-group>\n </hlm-select-content>\n </hlm-select>\n ",
|
|
467
475
|
}]
|
|
468
476
|
}], ctorParameters: () => [], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: true }] }, { type: i0.Output, args: ["currentPageChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: true }] }, { type: i0.Output, args: ["itemsPerPageChange"] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], showEdges: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdges", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }] } });
|
|
469
477
|
|
|
@@ -5,11 +5,13 @@ import { ClassValue } from 'clsx';
|
|
|
5
5
|
|
|
6
6
|
declare class HlmNumberedPagination {
|
|
7
7
|
/**
|
|
8
|
-
* The current (active) page.
|
|
8
|
+
* The current (active) page, floored and clamped to the available range.
|
|
9
|
+
* Corrections emit currentPageChange once; empty/invalid paging uses page 1.
|
|
9
10
|
*/
|
|
10
11
|
readonly currentPage: _angular_core.ModelSignal<number>;
|
|
11
12
|
/**
|
|
12
|
-
* The number of items per paginated page.
|
|
13
|
+
* The number of items per paginated page. Non-finite/non-positive sizes
|
|
14
|
+
* disable paging (one page) without rewriting this model.
|
|
13
15
|
*/
|
|
14
16
|
readonly itemsPerPage: _angular_core.ModelSignal<number>;
|
|
15
17
|
/**
|
|
@@ -19,7 +21,8 @@ declare class HlmNumberedPagination {
|
|
|
19
21
|
*/
|
|
20
22
|
readonly totalItems: _angular_core.InputSignalWithTransform<number, NumberInput>;
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
24
|
+
* Maximum window entries, including ellipses: floored and bounded to 1–100.
|
|
25
|
+
* Non-finite/non-positive values use the default of 7.
|
|
23
26
|
*/
|
|
24
27
|
readonly maxSize: _angular_core.InputSignalWithTransform<number, NumberInput>;
|
|
25
28
|
/**
|
|
@@ -32,10 +35,12 @@ declare class HlmNumberedPagination {
|
|
|
32
35
|
*/
|
|
33
36
|
readonly pageSizes: _angular_core.InputSignal<number[]>;
|
|
34
37
|
protected readonly _pageSizesWithCurrent: _angular_core.Signal<number[]>;
|
|
38
|
+
protected readonly _currentPage: _angular_core.Signal<number>;
|
|
35
39
|
protected readonly _isFirstPageActive: _angular_core.Signal<boolean>;
|
|
36
40
|
protected readonly _isLastPageActive: _angular_core.Signal<boolean>;
|
|
37
41
|
protected readonly _lastPageNumber: _angular_core.Signal<number>;
|
|
38
42
|
protected readonly _pages: _angular_core.Signal<Page[]>;
|
|
43
|
+
constructor();
|
|
39
44
|
protected goToPrevious(): void;
|
|
40
45
|
protected goToNext(): void;
|
|
41
46
|
protected goToFirst(): void;
|
|
@@ -48,23 +53,29 @@ type Page = number | '...';
|
|
|
48
53
|
* Checks that the instance.currentPage property is within bounds for the current page range.
|
|
49
54
|
* If not, return a correct value for currentPage, or the current value if OK.
|
|
50
55
|
*
|
|
51
|
-
*
|
|
56
|
+
* Empty/invalid totals or sizes use page 1. Finite pages are floored;
|
|
57
|
+
* non-finite pages use 1. Page counts saturate at Number.MAX_SAFE_INTEGER.
|
|
52
58
|
*/
|
|
53
59
|
declare function outOfBoundCorrection(totalItems: number, itemsPerPage: number, currentPage: number): number;
|
|
54
60
|
/**
|
|
55
61
|
* Returns an array of Page objects to use in the pagination controls.
|
|
56
62
|
*
|
|
57
|
-
*
|
|
63
|
+
* Window math adapted from 'ngx-pagination'. Range is floored and bounded to
|
|
64
|
+
* 1–100 entries (including gaps); non-finite/non-positive ranges use 7.
|
|
65
|
+
* Ranges below 5 show a contiguous window containing the active page.
|
|
66
|
+
* Work and allocation are proportional to this bounded window, not total pages.
|
|
58
67
|
*/
|
|
59
68
|
declare function createPageArray(currentPage: number, itemsPerPage: number, totalItems: number, paginationRange: number): Page[];
|
|
60
69
|
|
|
61
70
|
declare class HlmNumberedPaginationQueryParams {
|
|
62
71
|
/**
|
|
63
|
-
* The current
|
|
72
|
+
* The current page, floored and clamped; corrections emit currentPageChange.
|
|
73
|
+
* The parent owns reading/synchronizing the URL; corrections do not navigate.
|
|
64
74
|
*/
|
|
65
75
|
readonly currentPage: _angular_core.ModelSignal<number>;
|
|
66
76
|
/**
|
|
67
|
-
* The number of items per paginated page.
|
|
77
|
+
* The number of items per paginated page. Non-finite/non-positive sizes
|
|
78
|
+
* disable paging (one page) without rewriting this model.
|
|
68
79
|
*/
|
|
69
80
|
readonly itemsPerPage: _angular_core.ModelSignal<number>;
|
|
70
81
|
/**
|
|
@@ -79,7 +90,8 @@ declare class HlmNumberedPaginationQueryParams {
|
|
|
79
90
|
*/
|
|
80
91
|
readonly link: _angular_core.InputSignal<string>;
|
|
81
92
|
/**
|
|
82
|
-
*
|
|
93
|
+
* Maximum window entries, including ellipses: floored and bounded to 1–100.
|
|
94
|
+
* Non-finite/non-positive values use the default of 7.
|
|
83
95
|
*/
|
|
84
96
|
readonly maxSize: _angular_core.InputSignalWithTransform<number, NumberInput>;
|
|
85
97
|
/**
|
|
@@ -92,6 +104,7 @@ declare class HlmNumberedPaginationQueryParams {
|
|
|
92
104
|
*/
|
|
93
105
|
readonly pageSizes: _angular_core.InputSignal<number[]>;
|
|
94
106
|
protected readonly _pageSizesWithCurrent: _angular_core.Signal<number[]>;
|
|
107
|
+
protected readonly _currentPage: _angular_core.Signal<number>;
|
|
95
108
|
protected readonly _isFirstPageActive: _angular_core.Signal<boolean>;
|
|
96
109
|
protected readonly _isLastPageActive: _angular_core.Signal<boolean>;
|
|
97
110
|
protected readonly _lastPageNumber: _angular_core.Signal<number>;
|
package/phone-input/README.md
CHANGED
|
@@ -49,6 +49,22 @@ import { HlmPhoneInput, formatNanpPhoneNumber, type PhoneNumberFormatter } from
|
|
|
49
49
|
|
|
50
50
|
Model values may be `string | null | undefined`; non-digit characters are stripped defensively on write. An empty model renders as empty text (no dangling mask characters) and emits `null`. With `modelFormat: 'formatted'` the model holds display text instead of digits — useful when a backend expects the masked value; validators then apply to the formatted string.
|
|
51
51
|
|
|
52
|
+
### Keyboard editing
|
|
53
|
+
|
|
54
|
+
- With a collapsed caret, Backspace removes the preceding digit and Delete removes the following digit, skipping `)`, spaces, hyphens and other non-digit separators. Repeated deletion continues through the mask.
|
|
55
|
+
- With a selection, only selected digits are removed. A punctuation-only selection collapses to its logical digit boundary without deleting an adjacent digit or emitting a model change.
|
|
56
|
+
- Typing and paste replace the native selection, strip non-digits and respect `maxDigits`. The caret is restored after the preceding logical digit; clearing emits `null`. Only effective digit changes emit CVA changes, and blur marks the control touched. Programmatic writes format without emitting user changes.
|
|
57
|
+
- Deletion uses the native cancelable `beforeinput` event's `inputType` and original selection. Other edits retain native editing followed by `input` normalization. Read-only and disabled controls reject edits. Custom formatters should preserve digit content/order and add only non-digit separators so logical positions map correctly.
|
|
58
|
+
|
|
59
|
+
The source regression suites and real Chrome keyboard/clipboard probe can be run from the repository root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm --dir packages/angular test:library phone-input form-phone-input
|
|
63
|
+
node packages/angular/test/phone-keyboard-probe.mjs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The probe reuses installed Puppeteer and the shared Chrome resolver (`CHROME_BIN` or the package browser cache), compiles only an opt-in Angular test fixture, and uses Home/Arrow/Shift, Backspace/Delete, typing and Ctrl+C/Ctrl+V. Its Karma port defaults to `9877` (override with `PHONE_KEYBOARD_PORT`). It checks both primitive and wrapper controls; no published artifacts are needed.
|
|
67
|
+
|
|
52
68
|
### `formatNanpPhoneNumber(digits)`
|
|
53
69
|
|
|
54
70
|
North-American numbering plan formatter: `4` → `(4`, `4155` → `(415) 5`, `4155552671` → `(415) 555-2671`, truncated to 10 digits. Pass a custom `PhoneNumberFormatter` for other plans (e.g. chunk-and-join national formats) together with a matching `maxDigits`.
|
|
@@ -24,7 +24,8 @@ function formatNanpPhoneNumber(digits) {
|
|
|
24
24
|
/**
|
|
25
25
|
* A masked phone input. The form model holds raw digits (`"4155552671"`);
|
|
26
26
|
* the visible text is formatted (`"(415) 555-2671"`). Caret position is
|
|
27
|
-
* preserved across mask characters while typing or deleting.
|
|
27
|
+
* preserved across mask characters while typing or deleting. Backspace/Delete
|
|
28
|
+
* skip separators in their direction; selected ranges delete only selected digits.
|
|
28
29
|
*/
|
|
29
30
|
class HlmPhoneInput {
|
|
30
31
|
static _id = 0;
|
|
@@ -53,7 +54,7 @@ class HlmPhoneInput {
|
|
|
53
54
|
*/
|
|
54
55
|
modelFormat = input('digits', /* @ts-ignore */
|
|
55
56
|
...(ngDevMode ? [{ debugName: "modelFormat" }] : /* istanbul ignore next */ []));
|
|
56
|
-
/** Display formatter.
|
|
57
|
+
/** Display formatter. Preserve digit order/content and add only non-digit separators for caret mapping. */
|
|
57
58
|
formatPhoneNumber = input(formatNanpPhoneNumber, /* @ts-ignore */
|
|
58
59
|
...(ngDevMode ? [{ debugName: "formatPhoneNumber" }] : /* istanbul ignore next */ []));
|
|
59
60
|
/** Extra classes merged onto the inner `<input>` (over the `HlmInput` base). */
|
|
@@ -88,11 +89,46 @@ class HlmPhoneInput {
|
|
|
88
89
|
this._onChange?.(digits);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
_handleBeforeInput(event) {
|
|
93
|
+
if (event.defaultPrevented || !event.cancelable)
|
|
94
|
+
return;
|
|
95
|
+
if (this.disabledState() || this.readonly()) {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (event.inputType !== 'deleteContentBackward' && event.inputType !== 'deleteContentForward')
|
|
100
|
+
return;
|
|
101
|
+
// Input events contain the already-edited value. Only beforeinput retains
|
|
102
|
+
// both the browser's deletion intent and the original selected range.
|
|
103
|
+
const element = event.target;
|
|
104
|
+
const start = element.selectionStart;
|
|
105
|
+
const end = element.selectionEnd;
|
|
106
|
+
if (start === null || end === null)
|
|
107
|
+
return;
|
|
108
|
+
const digits = element.value.replace(/\D/g, '');
|
|
109
|
+
let from = element.value.slice(0, start).replace(/\D/g, '').length;
|
|
110
|
+
let to = element.value.slice(0, end).replace(/\D/g, '').length;
|
|
111
|
+
if (start === end) {
|
|
112
|
+
if (event.inputType === 'deleteContentBackward')
|
|
113
|
+
from = Math.max(0, from - 1);
|
|
114
|
+
else
|
|
115
|
+
to = Math.min(digits.length, to + 1);
|
|
116
|
+
}
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
this._renderEdit(element, digits.slice(0, from) + digits.slice(to), from);
|
|
119
|
+
}
|
|
91
120
|
_handleInput(event) {
|
|
92
121
|
const element = event.target;
|
|
122
|
+
if (this.disabledState() || this.readonly()) {
|
|
123
|
+
element.value = this._display();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
93
126
|
const caret = element.selectionStart ?? element.value.length;
|
|
94
127
|
const digitsBeforeCaret = element.value.slice(0, caret).replace(/\D/g, '').length;
|
|
95
|
-
|
|
128
|
+
this._renderEdit(element, element.value.replace(/\D/g, ''), digitsBeforeCaret);
|
|
129
|
+
}
|
|
130
|
+
_renderEdit(element, rawDigits, digitsBeforeCaret) {
|
|
131
|
+
const digits = rawDigits.slice(0, Math.max(0, this.maxDigits()));
|
|
96
132
|
const formatted = this.formatPhoneNumber()(digits);
|
|
97
133
|
const target = Math.min(digitsBeforeCaret, digits.length);
|
|
98
134
|
let position = 0;
|
|
@@ -140,6 +176,7 @@ class HlmPhoneInput {
|
|
|
140
176
|
[forceInvalid]="forceInvalid()"
|
|
141
177
|
[class]="$inputClass()"
|
|
142
178
|
[value]="_display()"
|
|
179
|
+
(beforeinput)="_handleBeforeInput($event)"
|
|
143
180
|
(input)="_handleInput($event)"
|
|
144
181
|
(blur)="_handleBlur()"
|
|
145
182
|
/>
|
|
@@ -172,6 +209,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
172
209
|
[forceInvalid]="forceInvalid()"
|
|
173
210
|
[class]="$inputClass()"
|
|
174
211
|
[value]="_display()"
|
|
212
|
+
(beforeinput)="_handleBeforeInput($event)"
|
|
175
213
|
(input)="_handleInput($event)"
|
|
176
214
|
(blur)="_handleBlur()"
|
|
177
215
|
/>
|
|
@@ -16,7 +16,8 @@ declare function formatNanpPhoneNumber(digits: string): string;
|
|
|
16
16
|
/**
|
|
17
17
|
* A masked phone input. The form model holds raw digits (`"4155552671"`);
|
|
18
18
|
* the visible text is formatted (`"(415) 555-2671"`). Caret position is
|
|
19
|
-
* preserved across mask characters while typing or deleting.
|
|
19
|
+
* preserved across mask characters while typing or deleting. Backspace/Delete
|
|
20
|
+
* skip separators in their direction; selected ranges delete only selected digits.
|
|
20
21
|
*/
|
|
21
22
|
declare class HlmPhoneInput implements ControlValueAccessor {
|
|
22
23
|
private static _id;
|
|
@@ -37,7 +38,7 @@ declare class HlmPhoneInput implements ControlValueAccessor {
|
|
|
37
38
|
* (`"(415) 555-2671"`).
|
|
38
39
|
*/
|
|
39
40
|
readonly modelFormat: _angular_core.InputSignal<"digits" | "formatted">;
|
|
40
|
-
/** Display formatter.
|
|
41
|
+
/** Display formatter. Preserve digit order/content and add only non-digit separators for caret mapping. */
|
|
41
42
|
readonly formatPhoneNumber: _angular_core.InputSignal<PhoneNumberFormatter>;
|
|
42
43
|
/** Extra classes merged onto the inner `<input>` (over the `HlmInput` base). */
|
|
43
44
|
readonly userClass: _angular_core.InputSignal<ClassValue>;
|
|
@@ -52,7 +53,9 @@ declare class HlmPhoneInput implements ControlValueAccessor {
|
|
|
52
53
|
protected _onChange?: ChangeFn<string | null>;
|
|
53
54
|
protected _onTouched?: TouchFn;
|
|
54
55
|
private _emitModel;
|
|
56
|
+
protected _handleBeforeInput(event: InputEvent): void;
|
|
55
57
|
protected _handleInput(event: Event): void;
|
|
58
|
+
private _renderEdit;
|
|
56
59
|
protected _handleBlur(): void;
|
|
57
60
|
/** CONTROL VALUE ACCESSOR */
|
|
58
61
|
writeValue(value: string | null | undefined): void;
|