ngwr 11.2.0 → 12.1.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 +85 -26
- package/burger/styles/_index.scss +1 -1
- package/button/styles/_index.scss +2 -2
- package/calendar/styles/_index.scss +32 -12
- package/calendar-heatmap/styles/_index.scss +20 -0
- package/carousel/styles/_index.scss +23 -1
- package/cascader/styles/_index.scss +15 -3
- package/checkbox/styles/_index.scss +2 -2
- package/collapse/styles/_index.scss +2 -2
- package/color-picker/styles/_index.scss +3 -3
- package/compare/styles/_index.scss +18 -2
- package/context-menu/styles/_index.scss +21 -2
- package/date-picker/styles/_index.scss +5 -5
- package/descriptions/styles/_index.scss +1 -1
- package/drag-drop/styles/_index.scss +28 -1
- package/drawer/styles/_index.scss +37 -0
- package/dropdown/styles/_index.scss +2 -2
- package/event-calendar/styles/_index.scss +67 -10
- package/fesm2022/ngwr-action-sheet.mjs +3 -3
- package/fesm2022/ngwr-affix.mjs +3 -3
- package/fesm2022/ngwr-alert.mjs +3 -3
- package/fesm2022/ngwr-anchor.mjs +3 -3
- package/fesm2022/ngwr-aurora.mjs +3 -3
- package/fesm2022/ngwr-avatar.mjs +3 -3
- package/fesm2022/ngwr-back-top.mjs +3 -3
- package/fesm2022/ngwr-badge.mjs +6 -6
- package/fesm2022/ngwr-bar-chart-testing.mjs +20 -2
- package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -1
- package/fesm2022/ngwr-bar-chart.mjs +3 -3
- package/fesm2022/ngwr-blur-text.mjs +3 -3
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-border-glow.mjs +4 -4
- package/fesm2022/ngwr-border-glow.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +29 -17
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-burger.mjs +3 -3
- package/fesm2022/ngwr-button.mjs +6 -6
- package/fesm2022/ngwr-calendar-heatmap.mjs +17 -5
- package/fesm2022/ngwr-calendar-heatmap.mjs.map +1 -1
- package/fesm2022/ngwr-calendar-testing.mjs +1 -1
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-calendar.mjs +187 -9
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-card.mjs +10 -10
- package/fesm2022/ngwr-card.mjs.map +1 -1
- package/fesm2022/ngwr-carousel.mjs +124 -24
- package/fesm2022/ngwr-carousel.mjs.map +1 -1
- package/fesm2022/ngwr-cascader.mjs +48 -5
- package/fesm2022/ngwr-cascader.mjs.map +1 -1
- package/fesm2022/ngwr-checkbox.mjs +6 -6
- package/fesm2022/ngwr-circular-text.mjs +4 -4
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-click-spark.mjs +27 -7
- package/fesm2022/ngwr-click-spark.mjs.map +1 -1
- package/fesm2022/ngwr-clipboard.mjs +3 -3
- package/fesm2022/ngwr-collapse.mjs +9 -9
- package/fesm2022/ngwr-color-picker-testing.mjs +26 -6
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker.mjs +98 -8
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-command-palette.mjs +14 -5
- package/fesm2022/ngwr-command-palette.mjs.map +1 -1
- package/fesm2022/ngwr-compare.mjs +3 -3
- package/fesm2022/ngwr-confetti.mjs +31 -6
- package/fesm2022/ngwr-confetti.mjs.map +1 -1
- package/fesm2022/ngwr-config.mjs +9 -0
- package/fesm2022/ngwr-config.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu-testing.mjs +30 -11
- package/fesm2022/ngwr-context-menu-testing.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +333 -43
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-cookie.mjs +3 -3
- package/fesm2022/ngwr-counter.mjs +25 -9
- package/fesm2022/ngwr-counter.mjs.map +1 -1
- package/fesm2022/{ngwr-date-adapter-fns.mjs → ngwr-date-adapters-fns.mjs} +37 -10
- package/fesm2022/ngwr-date-adapters-fns.mjs.map +1 -0
- package/fesm2022/{ngwr-date-adapter-luxon.mjs → ngwr-date-adapters-luxon.mjs} +5 -5
- package/fesm2022/ngwr-date-adapters-luxon.mjs.map +1 -0
- package/fesm2022/ngwr-date-picker-testing.mjs +11 -6
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +119 -37
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/{ngwr-date-adapter.mjs → ngwr-date.mjs} +36 -10
- package/fesm2022/ngwr-date.mjs.map +1 -0
- package/fesm2022/ngwr-decrypt-text.mjs +3 -3
- package/fesm2022/ngwr-density.mjs +6 -6
- package/fesm2022/ngwr-descriptions.mjs +6 -6
- package/fesm2022/ngwr-dialog.mjs +73 -18
- package/fesm2022/ngwr-dialog.mjs.map +1 -1
- package/fesm2022/ngwr-directives.mjs +12 -12
- package/fesm2022/ngwr-divider.mjs +52 -5
- package/fesm2022/ngwr-divider.mjs.map +1 -1
- package/fesm2022/ngwr-donut-chart-testing.mjs +8 -4
- package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -1
- package/fesm2022/ngwr-donut-chart.mjs +7 -7
- package/fesm2022/ngwr-donut-chart.mjs.map +1 -1
- package/fesm2022/ngwr-drag-drop.mjs +160 -11
- package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
- package/fesm2022/ngwr-drawer.mjs +121 -58
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +26 -10
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-empty.mjs +3 -3
- package/fesm2022/ngwr-event-calendar.mjs +28 -15
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-falling-text-testing.mjs +5 -3
- package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -1
- package/fesm2022/ngwr-falling-text.mjs +75 -46
- package/fesm2022/ngwr-falling-text.mjs.map +1 -1
- package/fesm2022/ngwr-file-upload.mjs +3 -3
- package/fesm2022/ngwr-form.mjs +32 -15
- package/fesm2022/ngwr-form.mjs.map +1 -1
- package/fesm2022/ngwr-fuzzy-text.mjs +21 -11
- package/fesm2022/ngwr-fuzzy-text.mjs.map +1 -1
- package/fesm2022/ngwr-gauge.mjs +3 -3
- package/fesm2022/ngwr-glitch-text-testing.mjs +51 -17
- package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -1
- package/fesm2022/ngwr-glitch-text.mjs +7 -7
- package/fesm2022/ngwr-glitch-text.mjs.map +1 -1
- package/fesm2022/ngwr-gradient-text-testing.mjs +46 -11
- package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -1
- package/fesm2022/ngwr-gradient-text.mjs +4 -4
- package/fesm2022/ngwr-gradient-text.mjs.map +1 -1
- package/fesm2022/ngwr-hotkey.mjs +24 -9
- package/fesm2022/ngwr-hotkey.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +73 -1
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +55 -1
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +26 -15
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-icon.mjs +6 -6
- package/fesm2022/ngwr-image-cropper-testing.mjs +53 -9
- package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -1
- package/fesm2022/ngwr-image-cropper.mjs +150 -12
- package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
- package/fesm2022/ngwr-input-number-testing.mjs +15 -10
- package/fesm2022/ngwr-input-number-testing.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +17 -10
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-input-otp-testing.mjs +12 -6
- package/fesm2022/ngwr-input-otp-testing.mjs.map +1 -1
- package/fesm2022/ngwr-input-otp.mjs +24 -5
- package/fesm2022/ngwr-input-otp.mjs.map +1 -1
- package/fesm2022/ngwr-input.mjs +15 -15
- package/fesm2022/ngwr-keyboard.mjs +3 -3
- package/fesm2022/ngwr-knob.mjs +3 -3
- package/fesm2022/ngwr-layout.mjs +15 -15
- package/fesm2022/ngwr-lightbox.mjs +4 -4
- package/fesm2022/ngwr-lightbox.mjs.map +1 -1
- package/fesm2022/ngwr-line-chart.mjs +73 -26
- package/fesm2022/ngwr-line-chart.mjs.map +1 -1
- package/fesm2022/ngwr-list.mjs +7 -7
- package/fesm2022/ngwr-list.mjs.map +1 -1
- package/fesm2022/ngwr-loading-bar.mjs +7 -7
- package/fesm2022/ngwr-loading-bar.mjs.map +1 -1
- package/fesm2022/ngwr-markdown.mjs +66 -8
- package/fesm2022/ngwr-markdown.mjs.map +1 -1
- package/fesm2022/ngwr-marquee.mjs +4 -4
- package/fesm2022/ngwr-marquee.mjs.map +1 -1
- package/fesm2022/ngwr-media.mjs +3 -3
- package/fesm2022/ngwr-mention.mjs +108 -13
- package/fesm2022/ngwr-mention.mjs.map +1 -1
- package/fesm2022/ngwr-meta.mjs +6 -6
- package/fesm2022/ngwr-meter-group-testing.mjs +26 -6
- package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -1
- package/fesm2022/ngwr-meter-group.mjs +3 -3
- package/fesm2022/ngwr-overlay.mjs +6 -6
- package/fesm2022/ngwr-page-header.mjs +3 -3
- package/fesm2022/ngwr-pagination-testing.mjs +6 -6
- package/fesm2022/ngwr-pagination-testing.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +83 -16
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-pipes.mjs +39 -26
- package/fesm2022/ngwr-pipes.mjs.map +1 -1
- package/fesm2022/ngwr-platform.mjs +9 -9
- package/fesm2022/ngwr-popconfirm.mjs +6 -6
- package/fesm2022/ngwr-popover-testing.mjs +7 -2
- package/fesm2022/ngwr-popover-testing.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +155 -33
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-progress.mjs +3 -3
- package/fesm2022/ngwr-pull-to-refresh.mjs +3 -3
- package/fesm2022/ngwr-qr.mjs +42 -5
- package/fesm2022/ngwr-qr.mjs.map +1 -1
- package/fesm2022/ngwr-radio.mjs +6 -6
- package/fesm2022/ngwr-rating.mjs +3 -3
- package/fesm2022/ngwr-result.mjs +21 -30
- package/fesm2022/ngwr-result.mjs.map +1 -1
- package/fesm2022/ngwr-rotating-text.mjs +3 -3
- package/fesm2022/ngwr-scroll.mjs +3 -3
- package/fesm2022/ngwr-segmented-testing.mjs +85 -1
- package/fesm2022/ngwr-segmented-testing.mjs.map +1 -1
- package/fesm2022/ngwr-segmented.mjs +123 -10
- package/fesm2022/ngwr-segmented.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +159 -34
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-shiny-text.mjs +11 -9
- package/fesm2022/ngwr-shiny-text.mjs.map +1 -1
- package/fesm2022/ngwr-sidebar.mjs +31 -16
- package/fesm2022/ngwr-sidebar.mjs.map +1 -1
- package/fesm2022/ngwr-skeleton.mjs +3 -3
- package/fesm2022/ngwr-slider.mjs +100 -9
- package/fesm2022/ngwr-slider.mjs.map +1 -1
- package/fesm2022/ngwr-sparkline.mjs +3 -3
- package/fesm2022/ngwr-speed-dial-testing.mjs +15 -13
- package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -1
- package/fesm2022/ngwr-speed-dial.mjs +27 -12
- package/fesm2022/ngwr-speed-dial.mjs.map +1 -1
- package/fesm2022/ngwr-spinner.mjs +3 -3
- package/fesm2022/ngwr-splash-cursor.mjs +3 -3
- package/fesm2022/ngwr-split-text.mjs +17 -5
- package/fesm2022/ngwr-split-text.mjs.map +1 -1
- package/fesm2022/ngwr-splitter.mjs +3 -3
- package/fesm2022/ngwr-spotlight-card.mjs +7 -7
- package/fesm2022/ngwr-spotlight-card.mjs.map +1 -1
- package/fesm2022/ngwr-squircle.mjs +6 -6
- package/fesm2022/ngwr-star-border.mjs +4 -4
- package/fesm2022/ngwr-star-border.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +9 -9
- package/fesm2022/ngwr-stepper-testing.mjs +8 -8
- package/fesm2022/ngwr-stepper-testing.mjs.map +1 -1
- package/fesm2022/ngwr-stepper.mjs +33 -8
- package/fesm2022/ngwr-stepper.mjs.map +1 -1
- package/fesm2022/ngwr-storage.mjs +17 -5
- package/fesm2022/ngwr-storage.mjs.map +1 -1
- package/fesm2022/ngwr-switch.mjs +3 -3
- package/fesm2022/ngwr-table.mjs +99 -39
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs-testing.mjs +11 -10
- package/fesm2022/ngwr-tabs-testing.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +36 -10
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-textarea.mjs +3 -3
- package/fesm2022/ngwr-theme.mjs +181 -4
- package/fesm2022/ngwr-theme.mjs.map +1 -1
- package/fesm2022/ngwr-tilt-card.mjs +7 -7
- package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
- package/fesm2022/ngwr-timeline.mjs +6 -6
- package/fesm2022/ngwr-toast.mjs +9 -9
- package/fesm2022/ngwr-toolbar.mjs +3 -3
- package/fesm2022/ngwr-tour.mjs +34 -10
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-transfer.mjs +3 -3
- package/fesm2022/ngwr-tree.mjs +28 -10
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-typewriter-testing.mjs +15 -1
- package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -1
- package/fesm2022/ngwr-typewriter.mjs +13 -5
- package/fesm2022/ngwr-typewriter.mjs.map +1 -1
- package/fesm2022/ngwr-typography.mjs +3 -3
- package/fesm2022/ngwr-utils.mjs +5 -2
- package/fesm2022/ngwr-utils.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-virtual-scroll.mjs +4 -4
- package/fesm2022/ngwr-virtual-scroll.mjs.map +1 -1
- package/fesm2022/ngwr-waves.mjs +4 -4
- package/fesm2022/ngwr-waves.mjs.map +1 -1
- package/fesm2022/ngwr-window.mjs +94 -24
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/file-upload/styles/_index.scss +2 -2
- package/glitch-text/styles/_index.scss +27 -12
- package/i18n/en.json +56 -6
- package/i18n/ru.json +56 -6
- package/image-cropper/styles/_index.scss +19 -0
- package/input/styles/_index.scss +2 -2
- package/input-number/styles/_index.scss +2 -2
- package/input-otp/styles/_index.scss +9 -2
- package/knob/styles/_index.scss +1 -1
- package/lightbox/styles/_index.scss +16 -2
- package/line-chart/styles/_index.scss +28 -14
- package/list/styles/_index.scss +1 -1
- package/llms-full.txt +19 -24
- package/llms.txt +46 -8
- package/markdown/styles/_index.scss +3 -3
- package/mcp/api.js +6 -4
- package/mcp/catalog.js +40 -3
- package/mcp/providers.js +16 -3
- package/mcp/tools.js +21 -1
- package/meter-group/styles/_index.scss +1 -1
- package/overlay/styles/_index.scss +33 -1
- package/package.json +17 -12
- package/pagination/styles/_index.scss +23 -1
- package/popover/styles/_index.scss +16 -7
- package/progress/styles/_index.scss +1 -1
- package/radio/styles/_index.scss +2 -2
- package/rating/styles/_index.scss +1 -1
- package/schematics/collection.json +1 -1
- package/schematics/migrations/v12/index.js +65 -0
- package/schematics/migrations/v7/index.js +12 -1
- package/schematics/migrations/v8/index.js +10 -1
- package/schematics/migrations.json +5 -0
- package/schematics/ng-add/index.js +8 -3
- package/schematics/provider/index.js +80 -13
- package/schematics/use/index.js +9 -129
- package/schematics/use/rule.js +206 -0
- package/schematics/use/symbol-map.json +4 -4
- package/segmented/styles/_index.scss +14 -3
- package/select/styles/_index.scss +14 -2
- package/sidebar/styles/_index.scss +12 -10
- package/skills/ngwr/SKILL.md +7 -5
- package/skills/ngwr/references/catalog.md +9 -9
- package/skills/ngwr/references/setup.md +10 -5
- package/slider/styles/_index.scss +1 -1
- package/speed-dial/styles/_index.scss +1 -1
- package/stepper/styles/_index.scss +12 -3
- package/switch/styles/_index.scss +2 -2
- package/table/styles/_index.scss +61 -12
- package/tabs/styles/_index.scss +1 -1
- package/textarea/styles/_index.scss +1 -1
- package/theme/styles/_colors.scss +142 -56
- package/theme/styles/_dark.scss +46 -3
- package/theme/styles/_focus.scss +28 -5
- package/theme/styles/_mixins.scss +1 -1
- package/theme/styles/_variables.scss +67 -0
- package/transfer/styles/_index.scss +26 -3
- package/tree/styles/_index.scss +29 -6
- package/types/ngwr-bar-chart-testing.d.ts +9 -1
- package/types/ngwr-blur-text.d.ts +8 -4
- package/types/ngwr-breadcrumbs.d.ts +17 -10
- package/types/ngwr-calendar-testing.d.ts +1 -1
- package/types/ngwr-calendar.d.ts +66 -0
- package/types/ngwr-carousel.d.ts +85 -6
- package/types/ngwr-cascader.d.ts +17 -0
- package/types/ngwr-click-spark.d.ts +4 -0
- package/types/ngwr-color-picker-testing.d.ts +8 -0
- package/types/ngwr-color-picker.d.ts +21 -1
- package/types/ngwr-confetti.d.ts +8 -0
- package/types/ngwr-config.d.ts +10 -1
- package/types/ngwr-context-menu-testing.d.ts +24 -11
- package/types/ngwr-context-menu.d.ts +56 -0
- package/types/ngwr-counter.d.ts +1 -0
- package/types/{ngwr-date-adapter-fns.d.ts → ngwr-date-adapters-fns.d.ts} +33 -6
- package/types/{ngwr-date-adapter-luxon.d.ts → ngwr-date-adapters-luxon.d.ts} +1 -1
- package/types/ngwr-date-picker-testing.d.ts +11 -6
- package/types/ngwr-date-picker.d.ts +8 -4
- package/types/{ngwr-date-adapter.d.ts → ngwr-date.d.ts} +24 -5
- package/types/ngwr-dialog.d.ts +38 -1
- package/types/ngwr-divider.d.ts +25 -0
- package/types/ngwr-donut-chart-testing.d.ts +7 -3
- package/types/ngwr-donut-chart.d.ts +3 -3
- package/types/ngwr-drag-drop.d.ts +56 -2
- package/types/ngwr-drawer.d.ts +56 -5
- package/types/ngwr-event-calendar.d.ts +8 -2
- package/types/ngwr-falling-text-testing.d.ts +5 -3
- package/types/ngwr-falling-text.d.ts +1 -1
- package/types/ngwr-form.d.ts +12 -2
- package/types/ngwr-glitch-text-testing.d.ts +37 -8
- package/types/ngwr-gradient-text-testing.d.ts +16 -3
- package/types/ngwr-i18n.d.ts +13 -4
- package/types/ngwr-image-cropper-testing.d.ts +49 -10
- package/types/ngwr-image-cropper.d.ts +70 -5
- package/types/ngwr-input-number-testing.d.ts +15 -10
- package/types/ngwr-input-number.d.ts +11 -4
- package/types/ngwr-input-otp-testing.d.ts +12 -6
- package/types/ngwr-input-otp.d.ts +15 -1
- package/types/ngwr-line-chart.d.ts +20 -3
- package/types/ngwr-markdown.d.ts +11 -0
- package/types/ngwr-mention.d.ts +32 -0
- package/types/ngwr-meter-group-testing.d.ts +12 -2
- package/types/ngwr-overlay.d.ts +1 -0
- package/types/ngwr-pagination-testing.d.ts +4 -4
- package/types/ngwr-pagination.d.ts +11 -0
- package/types/ngwr-pipes.d.ts +1 -1
- package/types/ngwr-popover-testing.d.ts +7 -2
- package/types/ngwr-popover.d.ts +1 -0
- package/types/ngwr-result.d.ts +6 -0
- package/types/ngwr-segmented-testing.d.ts +53 -0
- package/types/ngwr-segmented.d.ts +107 -5
- package/types/ngwr-select.d.ts +68 -6
- package/types/ngwr-shiny-text.d.ts +4 -2
- package/types/ngwr-sidebar.d.ts +14 -5
- package/types/ngwr-slider.d.ts +69 -1
- package/types/ngwr-speed-dial-testing.d.ts +15 -11
- package/types/ngwr-speed-dial.d.ts +22 -3
- package/types/ngwr-split-text.d.ts +9 -0
- package/types/ngwr-stepper-testing.d.ts +5 -5
- package/types/ngwr-stepper.d.ts +8 -0
- package/types/ngwr-storage.d.ts +1 -0
- package/types/ngwr-table.d.ts +104 -11
- package/types/ngwr-tabs-testing.d.ts +11 -10
- package/types/ngwr-tabs.d.ts +2 -0
- package/types/ngwr-theme.d.ts +82 -1
- package/types/ngwr-tour.d.ts +22 -4
- package/types/ngwr-tree.d.ts +5 -4
- package/types/ngwr-typewriter-testing.d.ts +10 -0
- package/types/ngwr-utils.d.ts +5 -2
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-window.d.ts +39 -11
- package/typography/styles/_index.scss +1 -1
- package/window/styles/_index.scss +72 -26
- package/fesm2022/ngwr-date-adapter-fns.mjs.map +0 -1
- package/fesm2022/ngwr-date-adapter-luxon.mjs.map +0 -1
- package/fesm2022/ngwr-date-adapter.mjs.map +0 -1
|
@@ -2,7 +2,7 @@ import { coerceNumberProperty } from '@angular/cdk/coercion';
|
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { signal, input, output, viewChild, inject, DestroyRef, computed, effect, ViewEncapsulation, Component } from '@angular/core';
|
|
4
4
|
import { useI18nText } from 'ngwr/i18n';
|
|
5
|
-
import { clamp } from 'ngwr/utils';
|
|
5
|
+
import { randomId, clamp } from 'ngwr/utils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @license
|
|
@@ -11,17 +11,25 @@ import { clamp } from 'ngwr/utils';
|
|
|
11
11
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
|
-
* Stands in for the consumer input `useI18nText` expects
|
|
15
|
-
*
|
|
14
|
+
* Stands in for the consumer input `useI18nText` expects. None of this
|
|
15
|
+
* component's three strings has a `*Label` input behind it — the catalog is the
|
|
16
|
+
* override channel — so they share one empty override.
|
|
16
17
|
*/
|
|
17
18
|
const NO_OVERRIDE = signal(null).asReadonly();
|
|
19
|
+
/** Keyboard step in display pixels, and what Shift promotes it to. */
|
|
20
|
+
const KEY_STEP = 1;
|
|
21
|
+
const KEY_STEP_COARSE = 10;
|
|
18
22
|
/**
|
|
19
23
|
* Image crop UI. Pass `[src]` (URL, `File`, or `Blob`); the user drags
|
|
20
24
|
* the crop window or any of its eight handles. Optionally lock the crop
|
|
21
25
|
* to a fixed `[aspectRatio]`.
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* The crop window is also the one tab stop: arrow keys move it, Alt with an
|
|
28
|
+
* arrow key resizes it, and Shift makes either step ten pixels instead of one.
|
|
29
|
+
*
|
|
30
|
+
* `(cropped)` fires after each drag end — or each run of arrow keys — with a
|
|
31
|
+
* freshly-rendered `Blob` of the cropped region. For one-off reads use
|
|
32
|
+
* `toBlob()` / `toDataUrl()`.
|
|
25
33
|
*
|
|
26
34
|
* @example
|
|
27
35
|
* ```html
|
|
@@ -57,6 +65,17 @@ class WrImageCropper {
|
|
|
57
65
|
...(ngDevMode ? [{ debugName: "imgEl" }] : /* istanbul ignore next */ []));
|
|
58
66
|
/** Resolved object URL for `src` (so File / Blob render in `<img>`). */
|
|
59
67
|
emptyText = useI18nText(NO_OVERRIDE, 'imageCropper.empty', 'No image');
|
|
68
|
+
/** Accessible name of the crop window — the component's only tab stop. */
|
|
69
|
+
resolvedWindowLabel = useI18nText(NO_OVERRIDE, 'imageCropper.window', 'Crop region');
|
|
70
|
+
/** The key model, referenced by the window's `aria-describedby`. */
|
|
71
|
+
resolvedKeyHelp = useI18nText(NO_OVERRIDE, 'imageCropper.keyHelp', 'Arrow keys move the crop. Hold Alt with an arrow key to resize it, and Shift for larger steps. ' +
|
|
72
|
+
'The crop is announced as left, top, width and height in image pixels.');
|
|
73
|
+
/** Id linking the crop window to the help text. Random rather than counted:
|
|
74
|
+
* the crop UI only exists after a real image load, so it never prerenders. */
|
|
75
|
+
helpId = randomId('wr-image-cropper-help');
|
|
76
|
+
/** Live-region text — written by the keyboard path only (see `onWindowKeydown`). */
|
|
77
|
+
announcement = signal('', /* @ts-ignore */
|
|
78
|
+
...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
60
79
|
objectUrl = signal(null, /* @ts-ignore */
|
|
61
80
|
...(ngDevMode ? [{ debugName: "objectUrl" }] : /* istanbul ignore next */ []));
|
|
62
81
|
/** Previous object URL we created — kept off-signal so the resolve
|
|
@@ -71,6 +90,8 @@ class WrImageCropper {
|
|
|
71
90
|
/** Crop rect in display coordinates. */
|
|
72
91
|
cropDisplay = signal({ x: 0, y: 0, w: 0, h: 0 }, /* @ts-ignore */
|
|
73
92
|
...(ngDevMode ? [{ debugName: "cropDisplay" }] : /* istanbul ignore next */ []));
|
|
93
|
+
/** Watches the rendered image box — see `watchResize`. */
|
|
94
|
+
resizeObserver = null;
|
|
74
95
|
/** Currently active drag handle. */
|
|
75
96
|
active = null;
|
|
76
97
|
startPointer = { x: 0, y: 0 };
|
|
@@ -125,6 +146,7 @@ class WrImageCropper {
|
|
|
125
146
|
this.destroyRef.onDestroy(() => {
|
|
126
147
|
if (this.previousObjectUrl)
|
|
127
148
|
URL.revokeObjectURL(this.previousObjectUrl);
|
|
149
|
+
this.resizeObserver?.disconnect();
|
|
128
150
|
});
|
|
129
151
|
}
|
|
130
152
|
// Image load
|
|
@@ -133,9 +155,10 @@ class WrImageCropper {
|
|
|
133
155
|
*
|
|
134
156
|
* `display` is the field that gates everything downstream — the crop UI renders on
|
|
135
157
|
* `display().w > 0` and `cropRect` returns zeros without it — so it is the only one
|
|
136
|
-
* a test can observe. The other two are reset for coherence:
|
|
137
|
-
*
|
|
138
|
-
*
|
|
158
|
+
* a test can observe. The other two are reset for coherence: the resize observer
|
|
159
|
+
* below also writes `display`, and left alone they would resurrect a crop measured
|
|
160
|
+
* against the previous image (which is why `onResize` refuses to run on a zeroed
|
|
161
|
+
* `display` — see there).
|
|
139
162
|
*/
|
|
140
163
|
resetGeometry() {
|
|
141
164
|
this.natural.set({ w: 0, h: 0 });
|
|
@@ -155,6 +178,56 @@ class WrImageCropper {
|
|
|
155
178
|
this.natural.set(natural);
|
|
156
179
|
this.display.set(display);
|
|
157
180
|
this.cropDisplay.set(this.initialCrop(display));
|
|
181
|
+
this.watchResize(img);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Keep `display` on the box the image is actually rendered at.
|
|
185
|
+
*
|
|
186
|
+
* The stylesheet sizes the image responsively — `max-width: 100%`,
|
|
187
|
+
* `max-height: 70dvh`, `height: auto` — so the single measurement taken in the
|
|
188
|
+
* `(load)` handler stops being true as soon as the container or the viewport
|
|
189
|
+
* changes. Everything the user sees and touches is in DISPLAY pixels: the crop
|
|
190
|
+
* window's inline `left` / `top` / `width` / `height`, the backdrop cut-out, and
|
|
191
|
+
* the bounds `applyMove` / `applyResize` clamp against. Measured once, a narrowed
|
|
192
|
+
* window leaves the crop painted past the edge of the image and draggable off it.
|
|
193
|
+
*
|
|
194
|
+
* Re-observed per load rather than once, because a new `src` may mount a new
|
|
195
|
+
* `<img>`; the old observation would then be on a detached element.
|
|
196
|
+
*/
|
|
197
|
+
watchResize(el) {
|
|
198
|
+
if (typeof ResizeObserver === 'undefined')
|
|
199
|
+
return;
|
|
200
|
+
this.resizeObserver?.disconnect();
|
|
201
|
+
// Re-read the same way `onImageLoad` does rather than take the entry's
|
|
202
|
+
// `contentRect`, so the two measurements can't disagree about the box.
|
|
203
|
+
const ro = new ResizeObserver(() => this.onResize(el));
|
|
204
|
+
ro.observe(el);
|
|
205
|
+
this.resizeObserver = ro;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Rescale the crop by however much the image did, so the user keeps the region
|
|
209
|
+
* they picked instead of having it re-centred: `cropRect()` — the source-pixel
|
|
210
|
+
* value a consumer acts on — comes out the same across the resize.
|
|
211
|
+
*/
|
|
212
|
+
onResize(el) {
|
|
213
|
+
const previous = this.display();
|
|
214
|
+
// Nothing measured yet, or measured and then dropped by `resetGeometry` while a
|
|
215
|
+
// new `src` loads. There is no crop to carry over, and the load will measure.
|
|
216
|
+
if (previous.w === 0 || previous.h === 0)
|
|
217
|
+
return;
|
|
218
|
+
const rect = el.getBoundingClientRect();
|
|
219
|
+
// A hidden image (a closed tab, `display: none`) reports zeros. Scaling by that
|
|
220
|
+
// would collapse the crop and there would be nothing to scale back from, so hold
|
|
221
|
+
// the last real box until it is shown again.
|
|
222
|
+
if (rect.width === 0 || rect.height === 0)
|
|
223
|
+
return;
|
|
224
|
+
if (rect.width === previous.w && rect.height === previous.h)
|
|
225
|
+
return;
|
|
226
|
+
const sx = rect.width / previous.w;
|
|
227
|
+
const sy = rect.height / previous.h;
|
|
228
|
+
const c = this.cropDisplay();
|
|
229
|
+
this.display.set({ w: rect.width, h: rect.height });
|
|
230
|
+
this.cropDisplay.set({ x: c.x * sx, y: c.y * sy, w: c.w * sx, h: c.h * sy });
|
|
158
231
|
}
|
|
159
232
|
/** Compute a sensible initial crop — centered, respecting aspectRatio. */
|
|
160
233
|
initialCrop(display) {
|
|
@@ -206,6 +279,71 @@ class WrImageCropper {
|
|
|
206
279
|
this.active = null;
|
|
207
280
|
void this.emitCropped();
|
|
208
281
|
}
|
|
282
|
+
// Keyboard
|
|
283
|
+
/** Whether a key has moved the crop since the last `keyup`. */
|
|
284
|
+
keyboardDirty = false;
|
|
285
|
+
/**
|
|
286
|
+
* The component's only keyboard path. Arrow keys move the crop window; Alt
|
|
287
|
+
* with an arrow key resizes it from the east / south edge; Shift makes either
|
|
288
|
+
* one ten display pixels instead of one — the coarse modifier `wr-slider`,
|
|
289
|
+
* `wr-knob` and `wr-splitter` all use, which is why Alt and not Shift carries
|
|
290
|
+
* the mode switch here.
|
|
291
|
+
*
|
|
292
|
+
* Nothing mirrors under `dir="rtl"`: the image is not flipped and the window
|
|
293
|
+
* is placed with a physical `left`, so ArrowLeft moves the crop visually left
|
|
294
|
+
* in both directions.
|
|
295
|
+
*
|
|
296
|
+
* Both branches go through `applyMove` / `applyResize`, so the `aspectRatio`
|
|
297
|
+
* lock, the `minWidth` / `minHeight` floors and the canvas bounds hold for a
|
|
298
|
+
* keystroke exactly as they do for a drag. Those two measure from `startRect`
|
|
299
|
+
* rather than from the live rect, so it is re-seeded per keystroke — a 1px
|
|
300
|
+
* delta against the rect from the first press would make a held arrow stall
|
|
301
|
+
* after one pixel instead of accumulating.
|
|
302
|
+
*/
|
|
303
|
+
onWindowKeydown(event) {
|
|
304
|
+
const step = event.shiftKey ? KEY_STEP_COARSE : KEY_STEP;
|
|
305
|
+
let dx = 0;
|
|
306
|
+
let dy = 0;
|
|
307
|
+
switch (event.key) {
|
|
308
|
+
case 'ArrowLeft':
|
|
309
|
+
dx = -step;
|
|
310
|
+
break;
|
|
311
|
+
case 'ArrowRight':
|
|
312
|
+
dx = step;
|
|
313
|
+
break;
|
|
314
|
+
case 'ArrowUp':
|
|
315
|
+
dy = -step;
|
|
316
|
+
break;
|
|
317
|
+
case 'ArrowDown':
|
|
318
|
+
dy = step;
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
this.startRect = { ...this.cropDisplay() };
|
|
325
|
+
if (event.altKey)
|
|
326
|
+
this.applyResize(dx !== 0 ? 'e' : 's', dx, dy);
|
|
327
|
+
else
|
|
328
|
+
this.applyMove(dx, dy);
|
|
329
|
+
this.keyboardDirty = true;
|
|
330
|
+
const c = this.cropRect();
|
|
331
|
+
// Bare numbers on purpose — `resolvedKeyHelp` has already told the reader
|
|
332
|
+
// what the four are, and a sentence here would be a translated string.
|
|
333
|
+
this.announcement.set(`${c.x}, ${c.y}, ${c.width} × ${c.height}`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Emit once per gesture, not once per keystroke. A held arrow repeats its
|
|
337
|
+
* keydown many times a second and each `emitCropped` renders a canvas and
|
|
338
|
+
* encodes a Blob; `keyup` fires once when the run ends, which is the keyboard's
|
|
339
|
+
* equivalent of the `pointerup` the drag path emits on.
|
|
340
|
+
*/
|
|
341
|
+
onWindowKeyup() {
|
|
342
|
+
if (!this.keyboardDirty)
|
|
343
|
+
return;
|
|
344
|
+
this.keyboardDirty = false;
|
|
345
|
+
void this.emitCropped();
|
|
346
|
+
}
|
|
209
347
|
applyMove(dx, dy) {
|
|
210
348
|
const display = this.display();
|
|
211
349
|
const r = this.startRect;
|
|
@@ -314,12 +452,12 @@ class WrImageCropper {
|
|
|
314
452
|
// Swallow — invalid state (image not loaded, crop empty, etc.).
|
|
315
453
|
}
|
|
316
454
|
}
|
|
317
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.
|
|
318
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.
|
|
455
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
456
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", 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 #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\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 });
|
|
319
457
|
}
|
|
320
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.
|
|
458
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: WrImageCropper, decorators: [{
|
|
321
459
|
type: Component,
|
|
322
|
-
args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: { class: 'wr-image-cropper' }, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\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
|
|
460
|
+
args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: { class: 'wr-image-cropper' }, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\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" }]
|
|
323
461
|
}], 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 }] }], cropped: [{ type: i0.Output, args: ["cropped"] }], imgEl: [{ type: i0.ViewChild, args: ['img', { isSignal: true }] }] } });
|
|
324
462
|
|
|
325
463
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-image-cropper.mjs","sources":["../../../projects/lib/image-cropper/image-cropper.ts","../../../projects/lib/image-cropper/image-cropper.html","../../../projects/lib/image-cropper/ngwr-image-cropper.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 { coerceNumberProperty } from '@angular/cdk/coercion';\nimport {\n Component,\n DestroyRef,\n type ElementRef,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { clamp } from 'ngwr/utils';\n\nimport type { WrCropHandle, WrCropRect, WrImageOutputType } from './interfaces';\n\n/**\n * Stands in for the consumer input `useI18nText` expects — the empty state takes no\n * label input, and its string was an English literal in the template.\n */\nconst NO_OVERRIDE = signal<string | null>(null).asReadonly();\n\ninterface RectPx {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Image crop UI. Pass `[src]` (URL, `File`, or `Blob`); the user drags\n * the crop window or any of its eight handles. Optionally lock the crop\n * to a fixed `[aspectRatio]`.\n *\n * `(cropped)` fires after each drag end with a freshly-rendered `Blob`\n * of the cropped region. For one-off reads use `toBlob()` / `toDataUrl()`.\n *\n * @example\n * ```html\n * <wr-image-cropper\n * [src]=\"file\"\n * [aspectRatio]=\"1\"\n * (cropped)=\"onBlob($event)\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/image-cropper\n */\n@Component({\n selector: 'wr-image-cropper',\n templateUrl: './image-cropper.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-image-cropper' },\n})\nexport class WrImageCropper {\n /** Image source — URL string, `File`, or `Blob`. */\n readonly src = input<string | File | Blob | null>(null);\n\n /** Aspect ratio (width / height). `null` = free. @default null */\n readonly aspectRatio = input<number | null>(null);\n\n /** Minimum crop width in display pixels. @default 32 */\n readonly minWidth = input(32, { transform: (v: unknown): number => Math.max(8, coerceNumberProperty(v, 32)) });\n\n /** Minimum crop height in display pixels. @default 32 */\n readonly minHeight = input(32, { transform: (v: unknown): number => Math.max(8, coerceNumberProperty(v, 32)) });\n\n /** Default output type for `(cropped)`. @default 'image/png' */\n readonly outputType = input<WrImageOutputType>('image/png');\n\n /** JPEG / WebP quality for `(cropped)` in [0, 1]. @default 0.92 */\n readonly outputQuality = input(0.92);\n\n /** Emits a Blob after each drag end. */\n readonly cropped = output<Blob>();\n\n protected readonly imgEl = viewChild.required<ElementRef<HTMLImageElement>>('img');\n\n /** Resolved object URL for `src` (so File / Blob render in `<img>`). */\n protected readonly emptyText = useI18nText(NO_OVERRIDE, 'imageCropper.empty', 'No image');\n\n protected readonly objectUrl = signal<string | null>(null);\n\n /** Previous object URL we created — kept off-signal so the resolve\n * effect doesn't depend on its own writes (which would loop). */\n private previousObjectUrl: string | null = null;\n\n /** Natural (source) pixel dimensions. */\n protected readonly natural = signal<{ w: number; h: number }>({ w: 0, h: 0 });\n\n /** Display (rendered) pixel dimensions. */\n protected readonly display = signal<{ w: number; h: number }>({ w: 0, h: 0 });\n\n /** Crop rect in display coordinates. */\n protected readonly cropDisplay = signal<RectPx>({ x: 0, y: 0, w: 0, h: 0 });\n\n /** Currently active drag handle. */\n private active: WrCropHandle | null = null;\n private startPointer: { x: number; y: number } = { x: 0, y: 0 };\n private startRect: RectPx = { x: 0, y: 0, w: 0, h: 0 };\n\n private readonly destroyRef = inject(DestroyRef);\n\n /** Resolved crop rect in natural (source) pixel coordinates. */\n readonly cropRect = computed<WrCropRect>(() => {\n const display = this.display();\n const natural = this.natural();\n const c = this.cropDisplay();\n if (display.w === 0 || display.h === 0) return { x: 0, y: 0, width: 0, height: 0 };\n const sx = natural.w / display.w;\n const sy = natural.h / display.h;\n return {\n x: Math.round(c.x * sx),\n y: Math.round(c.y * sy),\n width: Math.round(c.w * sx),\n height: Math.round(c.h * sy),\n };\n });\n\n constructor() {\n // Resolve the source into a usable URL the <img> can render.\n // We only read `src()` here — `previousObjectUrl` is a plain field, so\n // writing to `objectUrl` doesn't re-trigger this effect (and hang the\n // tab on file upload).\n effect(() => {\n const src = this.src();\n if (this.previousObjectUrl) {\n URL.revokeObjectURL(this.previousObjectUrl);\n this.previousObjectUrl = null;\n }\n // The crop is measured against the image that WAS showing, and `cropRect` is a\n // public signal a consumer can read at any moment. Left in place it kept\n // reporting a rect scaled to the previous image for the whole gap between a new\n // `src` and its load event — long enough to crop the wrong thing.\n this.resetGeometry();\n if (!src) {\n this.objectUrl.set(null);\n return;\n }\n if (typeof src === 'string') {\n this.objectUrl.set(src);\n } else {\n const url = URL.createObjectURL(src);\n this.previousObjectUrl = url;\n this.objectUrl.set(url);\n }\n });\n\n this.destroyRef.onDestroy(() => {\n if (this.previousObjectUrl) URL.revokeObjectURL(this.previousObjectUrl);\n });\n }\n\n // Image load\n\n /**\n * Back to \"nothing measured yet\", which is also what an absent `src` means.\n *\n * `display` is the field that gates everything downstream — the crop UI renders on\n * `display().w > 0` and `cropRect` returns zeros without it — so it is the only one\n * a test can observe. The other two are reset for coherence: anything that learns to\n * set `display` from elsewhere (a resize observer is the obvious candidate for this\n * component) would otherwise resurrect a crop measured against the previous image.\n */\n private resetGeometry(): void {\n this.natural.set({ w: 0, h: 0 });\n this.display.set({ w: 0, h: 0 });\n this.cropDisplay.set({ x: 0, y: 0, w: 0, h: 0 });\n }\n\n protected onImageLoad(): void {\n const img = this.imgEl().nativeElement;\n const rect = img.getBoundingClientRect();\n const display = { w: rect.width, h: rect.height };\n // Some SVGs and odd images report zero natural dimensions — fall back\n // to the rendered size so the crop math stays well-defined.\n const natural = {\n w: img.naturalWidth || display.w,\n h: img.naturalHeight || display.h,\n };\n this.natural.set(natural);\n this.display.set(display);\n this.cropDisplay.set(this.initialCrop(display));\n }\n\n /** Compute a sensible initial crop — centered, respecting aspectRatio. */\n private initialCrop(display: { w: number; h: number }): RectPx {\n const ratio = this.aspectRatio();\n let w = display.w * 0.6;\n let h = display.h * 0.6;\n if (ratio && ratio > 0) {\n const candidate = w / ratio;\n if (candidate > h) w = h * ratio;\n else h = w / ratio;\n }\n return {\n x: (display.w - w) / 2,\n y: (display.h - h) / 2,\n w,\n h,\n };\n }\n\n // Drag handlers\n\n protected readonly handles: readonly WrCropHandle[] = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];\n\n protected onPointerDown(handle: WrCropHandle, event: PointerEvent): void {\n // Any pointerdown used to start a drag, so the right button moved the crop window and\n // a second finger could take over a drag already in progress.\n if (event.button !== 0 || !event.isPrimary) return;\n event.preventDefault();\n event.stopPropagation();\n this.active = handle;\n this.startPointer = { x: event.clientX, y: event.clientY };\n this.startRect = { ...this.cropDisplay() };\n (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);\n }\n\n protected onPointerMove(event: PointerEvent): void {\n if (!this.active) return;\n const dx = event.clientX - this.startPointer.x;\n const dy = event.clientY - this.startPointer.y;\n if (this.active === 'move') this.applyMove(dx, dy);\n else this.applyResize(this.active, dx, dy);\n }\n\n protected onPointerUp(event: PointerEvent): void {\n if (!this.active) return;\n (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);\n this.active = null;\n void this.emitCropped();\n }\n\n private applyMove(dx: number, dy: number): void {\n const display = this.display();\n const r = this.startRect;\n const next = {\n x: clamp(r.x + dx, 0, display.w - r.w),\n y: clamp(r.y + dy, 0, display.h - r.h),\n w: r.w,\n h: r.h,\n };\n this.cropDisplay.set(next);\n }\n\n private applyResize(handle: WrCropHandle, dx: number, dy: number): void {\n const display = this.display();\n const min = { w: this.minWidth(), h: this.minHeight() };\n const ratio = this.aspectRatio();\n const r = this.startRect;\n let x = r.x;\n let y = r.y;\n let w = r.w;\n let h = r.h;\n\n if (handle.includes('e')) w = clamp(r.w + dx, min.w, display.w - r.x);\n if (handle.includes('w')) {\n const nextW = clamp(r.w - dx, min.w, r.x + r.w);\n x = r.x + (r.w - nextW);\n w = nextW;\n }\n if (handle.includes('s')) h = clamp(r.h + dy, min.h, display.h - r.y);\n if (handle.includes('n')) {\n const nextH = clamp(r.h - dy, min.h, r.y + r.h);\n y = r.y + (r.h - nextH);\n h = nextH;\n }\n\n if (ratio && ratio > 0) {\n // Lock the orthogonal axis to maintain ratio. For corner handles,\n // pick whichever delta drives the larger change so we don't bounce.\n if (handle === 'e' || handle === 'w') {\n const newH = w / ratio;\n const dh = newH - r.h;\n if (handle === 'e' || handle === 'w') {\n y = r.y - dh / 2;\n h = newH;\n }\n } else if (handle === 'n' || handle === 's') {\n const newW = h * ratio;\n const dw = newW - r.w;\n x = r.x - dw / 2;\n w = newW;\n } else {\n // Corner — drive by width, then derive height.\n const newH = w / ratio;\n if (handle.includes('n')) y = r.y + r.h - newH;\n h = newH;\n }\n // Re-clamp after ratio adjustment so we never spill out of the canvas.\n x = clamp(x, 0, display.w - w);\n y = clamp(y, 0, display.h - h);\n w = clamp(w, min.w, display.w - x);\n h = clamp(h, min.h, display.h - y);\n }\n\n this.cropDisplay.set({ x, y, w, h });\n }\n\n // Public API\n\n /** Render the current crop as a Blob. */\n async toBlob(type: WrImageOutputType = this.outputType(), quality: number = this.outputQuality()): Promise<Blob> {\n const canvas = this.toCanvas();\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n blob => {\n if (blob) resolve(blob);\n else reject(new Error('toBlob returned null'));\n },\n type,\n quality\n );\n });\n }\n\n /** Render the current crop as a data URL. */\n toDataUrl(type: WrImageOutputType = this.outputType(), quality: number = this.outputQuality()): string {\n return this.toCanvas().toDataURL(type, quality);\n }\n\n /** Re-emit the crop without waiting for a drag (e.g. after a programmatic change). */\n async refresh(): Promise<void> {\n await this.emitCropped();\n }\n\n // Internals\n\n private toCanvas(): HTMLCanvasElement {\n const img = this.imgEl().nativeElement;\n const c = this.cropRect();\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, c.width);\n canvas.height = Math.max(1, c.height);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, canvas.width, canvas.height);\n return canvas;\n }\n\n private async emitCropped(): Promise<void> {\n try {\n const blob = await this.toBlob();\n this.cropped.emit(blob);\n } catch {\n // Swallow — invalid state (image not loaded, crop empty, etc.).\n }\n }\n}\n","@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\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 <!-- Crop window — drag to move, with eight resize handles. -->\n <div\n class=\"wr-image-cropper__window\"\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 >\n @for (h of handles; track h) {\n <span\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 </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\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.mjs","sources":["../../../projects/lib/image-cropper/image-cropper.ts","../../../projects/lib/image-cropper/image-cropper.html","../../../projects/lib/image-cropper/ngwr-image-cropper.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 { coerceNumberProperty } from '@angular/cdk/coercion';\nimport {\n Component,\n DestroyRef,\n type ElementRef,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { clamp, randomId } from 'ngwr/utils';\n\nimport type { WrCropHandle, WrCropRect, WrImageOutputType } from './interfaces';\n\n/**\n * Stands in for the consumer input `useI18nText` expects. None of this\n * component's three strings has a `*Label` input behind it — the catalog is the\n * override channel — so they share one empty override.\n */\nconst NO_OVERRIDE = signal<string | null>(null).asReadonly();\n\n/** Keyboard step in display pixels, and what Shift promotes it to. */\nconst KEY_STEP = 1;\nconst KEY_STEP_COARSE = 10;\n\ninterface RectPx {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Image crop UI. Pass `[src]` (URL, `File`, or `Blob`); the user drags\n * the crop window or any of its eight handles. Optionally lock the crop\n * to a fixed `[aspectRatio]`.\n *\n * The crop window is also the one tab stop: arrow keys move it, Alt with an\n * arrow key resizes it, and Shift makes either step ten pixels instead of one.\n *\n * `(cropped)` fires after each drag end — or each run of arrow keys — with a\n * freshly-rendered `Blob` of the cropped region. For one-off reads use\n * `toBlob()` / `toDataUrl()`.\n *\n * @example\n * ```html\n * <wr-image-cropper\n * [src]=\"file\"\n * [aspectRatio]=\"1\"\n * (cropped)=\"onBlob($event)\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/image-cropper\n */\n@Component({\n selector: 'wr-image-cropper',\n templateUrl: './image-cropper.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-image-cropper' },\n})\nexport class WrImageCropper {\n /** Image source — URL string, `File`, or `Blob`. */\n readonly src = input<string | File | Blob | null>(null);\n\n /** Aspect ratio (width / height). `null` = free. @default null */\n readonly aspectRatio = input<number | null>(null);\n\n /** Minimum crop width in display pixels. @default 32 */\n readonly minWidth = input(32, { transform: (v: unknown): number => Math.max(8, coerceNumberProperty(v, 32)) });\n\n /** Minimum crop height in display pixels. @default 32 */\n readonly minHeight = input(32, { transform: (v: unknown): number => Math.max(8, coerceNumberProperty(v, 32)) });\n\n /** Default output type for `(cropped)`. @default 'image/png' */\n readonly outputType = input<WrImageOutputType>('image/png');\n\n /** JPEG / WebP quality for `(cropped)` in [0, 1]. @default 0.92 */\n readonly outputQuality = input(0.92);\n\n /** Emits a Blob after each drag end. */\n readonly cropped = output<Blob>();\n\n protected readonly imgEl = viewChild.required<ElementRef<HTMLImageElement>>('img');\n\n /** Resolved object URL for `src` (so File / Blob render in `<img>`). */\n protected readonly emptyText = useI18nText(NO_OVERRIDE, 'imageCropper.empty', 'No image');\n\n /** Accessible name of the crop window — the component's only tab stop. */\n protected readonly resolvedWindowLabel = useI18nText(NO_OVERRIDE, 'imageCropper.window', 'Crop region');\n\n /** The key model, referenced by the window's `aria-describedby`. */\n protected readonly resolvedKeyHelp = useI18nText(\n NO_OVERRIDE,\n 'imageCropper.keyHelp',\n 'Arrow keys move the crop. Hold Alt with an arrow key to resize it, and Shift for larger steps. ' +\n 'The crop is announced as left, top, width and height in image pixels.'\n );\n\n /** Id linking the crop window to the help text. Random rather than counted:\n * the crop UI only exists after a real image load, so it never prerenders. */\n protected readonly helpId = randomId('wr-image-cropper-help');\n\n /** Live-region text — written by the keyboard path only (see `onWindowKeydown`). */\n protected readonly announcement = signal<string>('');\n\n protected readonly objectUrl = signal<string | null>(null);\n\n /** Previous object URL we created — kept off-signal so the resolve\n * effect doesn't depend on its own writes (which would loop). */\n private previousObjectUrl: string | null = null;\n\n /** Natural (source) pixel dimensions. */\n protected readonly natural = signal<{ w: number; h: number }>({ w: 0, h: 0 });\n\n /** Display (rendered) pixel dimensions. */\n protected readonly display = signal<{ w: number; h: number }>({ w: 0, h: 0 });\n\n /** Crop rect in display coordinates. */\n protected readonly cropDisplay = signal<RectPx>({ x: 0, y: 0, w: 0, h: 0 });\n\n /** Watches the rendered image box — see `watchResize`. */\n private resizeObserver: ResizeObserver | null = null;\n\n /** Currently active drag handle. */\n private active: WrCropHandle | null = null;\n private startPointer: { x: number; y: number } = { x: 0, y: 0 };\n private startRect: RectPx = { x: 0, y: 0, w: 0, h: 0 };\n\n private readonly destroyRef = inject(DestroyRef);\n\n /** Resolved crop rect in natural (source) pixel coordinates. */\n readonly cropRect = computed<WrCropRect>(() => {\n const display = this.display();\n const natural = this.natural();\n const c = this.cropDisplay();\n if (display.w === 0 || display.h === 0) return { x: 0, y: 0, width: 0, height: 0 };\n const sx = natural.w / display.w;\n const sy = natural.h / display.h;\n return {\n x: Math.round(c.x * sx),\n y: Math.round(c.y * sy),\n width: Math.round(c.w * sx),\n height: Math.round(c.h * sy),\n };\n });\n\n constructor() {\n // Resolve the source into a usable URL the <img> can render.\n // We only read `src()` here — `previousObjectUrl` is a plain field, so\n // writing to `objectUrl` doesn't re-trigger this effect (and hang the\n // tab on file upload).\n effect(() => {\n const src = this.src();\n if (this.previousObjectUrl) {\n URL.revokeObjectURL(this.previousObjectUrl);\n this.previousObjectUrl = null;\n }\n // The crop is measured against the image that WAS showing, and `cropRect` is a\n // public signal a consumer can read at any moment. Left in place it kept\n // reporting a rect scaled to the previous image for the whole gap between a new\n // `src` and its load event — long enough to crop the wrong thing.\n this.resetGeometry();\n if (!src) {\n this.objectUrl.set(null);\n return;\n }\n if (typeof src === 'string') {\n this.objectUrl.set(src);\n } else {\n const url = URL.createObjectURL(src);\n this.previousObjectUrl = url;\n this.objectUrl.set(url);\n }\n });\n\n this.destroyRef.onDestroy(() => {\n if (this.previousObjectUrl) URL.revokeObjectURL(this.previousObjectUrl);\n this.resizeObserver?.disconnect();\n });\n }\n\n // Image load\n\n /**\n * Back to \"nothing measured yet\", which is also what an absent `src` means.\n *\n * `display` is the field that gates everything downstream — the crop UI renders on\n * `display().w > 0` and `cropRect` returns zeros without it — so it is the only one\n * a test can observe. The other two are reset for coherence: the resize observer\n * below also writes `display`, and left alone they would resurrect a crop measured\n * against the previous image (which is why `onResize` refuses to run on a zeroed\n * `display` — see there).\n */\n private resetGeometry(): void {\n this.natural.set({ w: 0, h: 0 });\n this.display.set({ w: 0, h: 0 });\n this.cropDisplay.set({ x: 0, y: 0, w: 0, h: 0 });\n }\n\n protected onImageLoad(): void {\n const img = this.imgEl().nativeElement;\n const rect = img.getBoundingClientRect();\n const display = { w: rect.width, h: rect.height };\n // Some SVGs and odd images report zero natural dimensions — fall back\n // to the rendered size so the crop math stays well-defined.\n const natural = {\n w: img.naturalWidth || display.w,\n h: img.naturalHeight || display.h,\n };\n this.natural.set(natural);\n this.display.set(display);\n this.cropDisplay.set(this.initialCrop(display));\n this.watchResize(img);\n }\n\n /**\n * Keep `display` on the box the image is actually rendered at.\n *\n * The stylesheet sizes the image responsively — `max-width: 100%`,\n * `max-height: 70dvh`, `height: auto` — so the single measurement taken in the\n * `(load)` handler stops being true as soon as the container or the viewport\n * changes. Everything the user sees and touches is in DISPLAY pixels: the crop\n * window's inline `left` / `top` / `width` / `height`, the backdrop cut-out, and\n * the bounds `applyMove` / `applyResize` clamp against. Measured once, a narrowed\n * window leaves the crop painted past the edge of the image and draggable off it.\n *\n * Re-observed per load rather than once, because a new `src` may mount a new\n * `<img>`; the old observation would then be on a detached element.\n */\n private watchResize(el: HTMLImageElement): void {\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver?.disconnect();\n // Re-read the same way `onImageLoad` does rather than take the entry's\n // `contentRect`, so the two measurements can't disagree about the box.\n const ro = new ResizeObserver(() => this.onResize(el));\n ro.observe(el);\n this.resizeObserver = ro;\n }\n\n /**\n * Rescale the crop by however much the image did, so the user keeps the region\n * they picked instead of having it re-centred: `cropRect()` — the source-pixel\n * value a consumer acts on — comes out the same across the resize.\n */\n private onResize(el: HTMLImageElement): void {\n const previous = this.display();\n // Nothing measured yet, or measured and then dropped by `resetGeometry` while a\n // new `src` loads. There is no crop to carry over, and the load will measure.\n if (previous.w === 0 || previous.h === 0) return;\n const rect = el.getBoundingClientRect();\n // A hidden image (a closed tab, `display: none`) reports zeros. Scaling by that\n // would collapse the crop and there would be nothing to scale back from, so hold\n // the last real box until it is shown again.\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width === previous.w && rect.height === previous.h) return;\n\n const sx = rect.width / previous.w;\n const sy = rect.height / previous.h;\n const c = this.cropDisplay();\n this.display.set({ w: rect.width, h: rect.height });\n this.cropDisplay.set({ x: c.x * sx, y: c.y * sy, w: c.w * sx, h: c.h * sy });\n }\n\n /** Compute a sensible initial crop — centered, respecting aspectRatio. */\n private initialCrop(display: { w: number; h: number }): RectPx {\n const ratio = this.aspectRatio();\n let w = display.w * 0.6;\n let h = display.h * 0.6;\n if (ratio && ratio > 0) {\n const candidate = w / ratio;\n if (candidate > h) w = h * ratio;\n else h = w / ratio;\n }\n return {\n x: (display.w - w) / 2,\n y: (display.h - h) / 2,\n w,\n h,\n };\n }\n\n // Drag handlers\n\n protected readonly handles: readonly WrCropHandle[] = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];\n\n protected onPointerDown(handle: WrCropHandle, event: PointerEvent): void {\n // Any pointerdown used to start a drag, so the right button moved the crop window and\n // a second finger could take over a drag already in progress.\n if (event.button !== 0 || !event.isPrimary) return;\n event.preventDefault();\n event.stopPropagation();\n this.active = handle;\n this.startPointer = { x: event.clientX, y: event.clientY };\n this.startRect = { ...this.cropDisplay() };\n (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);\n }\n\n protected onPointerMove(event: PointerEvent): void {\n if (!this.active) return;\n const dx = event.clientX - this.startPointer.x;\n const dy = event.clientY - this.startPointer.y;\n if (this.active === 'move') this.applyMove(dx, dy);\n else this.applyResize(this.active, dx, dy);\n }\n\n protected onPointerUp(event: PointerEvent): void {\n if (!this.active) return;\n (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);\n this.active = null;\n void this.emitCropped();\n }\n\n // Keyboard\n\n /** Whether a key has moved the crop since the last `keyup`. */\n private keyboardDirty = false;\n\n /**\n * The component's only keyboard path. Arrow keys move the crop window; Alt\n * with an arrow key resizes it from the east / south edge; Shift makes either\n * one ten display pixels instead of one — the coarse modifier `wr-slider`,\n * `wr-knob` and `wr-splitter` all use, which is why Alt and not Shift carries\n * the mode switch here.\n *\n * Nothing mirrors under `dir=\"rtl\"`: the image is not flipped and the window\n * is placed with a physical `left`, so ArrowLeft moves the crop visually left\n * in both directions.\n *\n * Both branches go through `applyMove` / `applyResize`, so the `aspectRatio`\n * lock, the `minWidth` / `minHeight` floors and the canvas bounds hold for a\n * keystroke exactly as they do for a drag. Those two measure from `startRect`\n * rather than from the live rect, so it is re-seeded per keystroke — a 1px\n * delta against the rect from the first press would make a held arrow stall\n * after one pixel instead of accumulating.\n */\n protected onWindowKeydown(event: KeyboardEvent): void {\n const step = event.shiftKey ? KEY_STEP_COARSE : KEY_STEP;\n let dx = 0;\n let dy = 0;\n switch (event.key) {\n case 'ArrowLeft':\n dx = -step;\n break;\n case 'ArrowRight':\n dx = step;\n break;\n case 'ArrowUp':\n dy = -step;\n break;\n case 'ArrowDown':\n dy = step;\n break;\n default:\n return;\n }\n\n event.preventDefault();\n this.startRect = { ...this.cropDisplay() };\n if (event.altKey) this.applyResize(dx !== 0 ? 'e' : 's', dx, dy);\n else this.applyMove(dx, dy);\n\n this.keyboardDirty = true;\n const c = this.cropRect();\n // Bare numbers on purpose — `resolvedKeyHelp` has already told the reader\n // what the four are, and a sentence here would be a translated string.\n this.announcement.set(`${c.x}, ${c.y}, ${c.width} × ${c.height}`);\n }\n\n /**\n * Emit once per gesture, not once per keystroke. A held arrow repeats its\n * keydown many times a second and each `emitCropped` renders a canvas and\n * encodes a Blob; `keyup` fires once when the run ends, which is the keyboard's\n * equivalent of the `pointerup` the drag path emits on.\n */\n protected onWindowKeyup(): void {\n if (!this.keyboardDirty) return;\n this.keyboardDirty = false;\n void this.emitCropped();\n }\n\n private applyMove(dx: number, dy: number): void {\n const display = this.display();\n const r = this.startRect;\n const next = {\n x: clamp(r.x + dx, 0, display.w - r.w),\n y: clamp(r.y + dy, 0, display.h - r.h),\n w: r.w,\n h: r.h,\n };\n this.cropDisplay.set(next);\n }\n\n private applyResize(handle: WrCropHandle, dx: number, dy: number): void {\n const display = this.display();\n const min = { w: this.minWidth(), h: this.minHeight() };\n const ratio = this.aspectRatio();\n const r = this.startRect;\n let x = r.x;\n let y = r.y;\n let w = r.w;\n let h = r.h;\n\n if (handle.includes('e')) w = clamp(r.w + dx, min.w, display.w - r.x);\n if (handle.includes('w')) {\n const nextW = clamp(r.w - dx, min.w, r.x + r.w);\n x = r.x + (r.w - nextW);\n w = nextW;\n }\n if (handle.includes('s')) h = clamp(r.h + dy, min.h, display.h - r.y);\n if (handle.includes('n')) {\n const nextH = clamp(r.h - dy, min.h, r.y + r.h);\n y = r.y + (r.h - nextH);\n h = nextH;\n }\n\n if (ratio && ratio > 0) {\n // Lock the orthogonal axis to maintain ratio. For corner handles,\n // pick whichever delta drives the larger change so we don't bounce.\n if (handle === 'e' || handle === 'w') {\n const newH = w / ratio;\n const dh = newH - r.h;\n if (handle === 'e' || handle === 'w') {\n y = r.y - dh / 2;\n h = newH;\n }\n } else if (handle === 'n' || handle === 's') {\n const newW = h * ratio;\n const dw = newW - r.w;\n x = r.x - dw / 2;\n w = newW;\n } else {\n // Corner — drive by width, then derive height.\n const newH = w / ratio;\n if (handle.includes('n')) y = r.y + r.h - newH;\n h = newH;\n }\n // Re-clamp after ratio adjustment so we never spill out of the canvas.\n x = clamp(x, 0, display.w - w);\n y = clamp(y, 0, display.h - h);\n w = clamp(w, min.w, display.w - x);\n h = clamp(h, min.h, display.h - y);\n }\n\n this.cropDisplay.set({ x, y, w, h });\n }\n\n // Public API\n\n /** Render the current crop as a Blob. */\n async toBlob(type: WrImageOutputType = this.outputType(), quality: number = this.outputQuality()): Promise<Blob> {\n const canvas = this.toCanvas();\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n blob => {\n if (blob) resolve(blob);\n else reject(new Error('toBlob returned null'));\n },\n type,\n quality\n );\n });\n }\n\n /** Render the current crop as a data URL. */\n toDataUrl(type: WrImageOutputType = this.outputType(), quality: number = this.outputQuality()): string {\n return this.toCanvas().toDataURL(type, quality);\n }\n\n /** Re-emit the crop without waiting for a drag (e.g. after a programmatic change). */\n async refresh(): Promise<void> {\n await this.emitCropped();\n }\n\n // Internals\n\n private toCanvas(): HTMLCanvasElement {\n const img = this.imgEl().nativeElement;\n const c = this.cropRect();\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, c.width);\n canvas.height = Math.max(1, c.height);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, canvas.width, canvas.height);\n return canvas;\n }\n\n private async emitCropped(): Promise<void> {\n try {\n const blob = await this.toBlob();\n this.cropped.emit(blob);\n } catch {\n // Swallow — invalid state (image not loaded, crop empty, etc.).\n }\n }\n}\n","@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\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 — 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 — 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 —\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","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -226,13 +226,17 @@ class WrInputNumberHarness extends ComponentHarness {
|
|
|
226
226
|
return (await this.field()).getProperty('placeholder');
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* The field's
|
|
229
|
+
* The field's `aria-label`, or `null` when it carries none.
|
|
230
230
|
*
|
|
231
231
|
* Read from the native input, not from the host: an `aria-label` on a component
|
|
232
232
|
* element does not reach the control inside it, which is why the component
|
|
233
|
-
* takes an `ariaLabel` input and forwards it here
|
|
234
|
-
*
|
|
235
|
-
*
|
|
233
|
+
* takes an `ariaLabel` input and forwards it here.
|
|
234
|
+
*
|
|
235
|
+
* NOT the accessible name — `null` is the usual answer and usually correct.
|
|
236
|
+
* Nothing is forwarded unless `[ariaLabel]` is bound: inside a
|
|
237
|
+
* `<wr-form-field>` the name comes from the label's `for`, and standalone it
|
|
238
|
+
* comes from the `placeholder` (see {@link getPlaceholder}), neither of which
|
|
239
|
+
* is an attribute on this element.
|
|
236
240
|
*/
|
|
237
241
|
async getAriaLabel() {
|
|
238
242
|
return (await this.field()).getAttribute('aria-label');
|
|
@@ -279,12 +283,13 @@ class WrInputNumberHarness extends ComponentHarness {
|
|
|
279
283
|
/**
|
|
280
284
|
* One of the two stepper buttons, BY POSITION — ▲ first, ▼ second.
|
|
281
285
|
*
|
|
282
|
-
* Not by `aria-label`: those strings
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
286
|
+
* Not by `aria-label`: those strings resolve through the i18n catalog
|
|
287
|
+
* (`inputNumber.increment` / `.decrement`) and an `[incrementLabel]` /
|
|
288
|
+
* `[decrementLabel]` binding overrides them, so keying on them would make this
|
|
289
|
+
* harness answer in whatever locale the app under test is configured for — the
|
|
290
|
+
* same call `WrTimePanelHarness` makes about its own columns. Order is the
|
|
291
|
+
* layout contract, and the spec pins it by asserting which way each button
|
|
292
|
+
* moves the value.
|
|
288
293
|
*/
|
|
289
294
|
async stepButton(direction, method) {
|
|
290
295
|
const buttons = await this.locatorForAll('.wr-input-number__step')();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-input-number-testing.mjs","sources":["../../../projects/lib/input-number/testing/wr-input-number-harness.ts","../../../projects/lib/input-number/testing/ngwr-input-number-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 { WrInputNumberHarnessFilters } from './interfaces';\n\n/** The decimal + grouping characters `Intl` formats `locale` with. */\nfunction separatorsFor(locale?: string): { decimal: string; group: string } {\n const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);\n return {\n decimal: parts.find(part => part.type === 'decimal')?.value ?? '.',\n group: parts.find(part => part.type === 'group')?.value ?? '',\n };\n}\n\n/**\n * Locale-formatted text back into a number — `null` when the text is empty or is\n * not a number at all.\n *\n * The same two-step the component parses with: drop the grouping character, then\n * normalise the decimal one to `.`. Doing it in that order matters, because in\n * half the world's locales the two are swapped and `Number('1.234,5')` is `NaN`\n * either way.\n */\nfunction parseNumber(text: string, locale?: string): number | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n\n const { decimal, group } = separatorsFor(locale);\n let normalised = group ? trimmed.split(group).join('') : trimmed;\n if (decimal !== '.') normalised = normalised.split(decimal).join('.');\n\n const parsed = Number(normalised);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Test harness for `<wr-input-number>`.\n *\n * The control is a `type=\"text\"` field plus a ▲▼ stepper column — deliberately\n * NOT an `<input type=\"number\">` and NOT a `role=\"spinbutton\"`, because it renders\n * grouped, locale-formatted text that a numeric input cannot hold. Two things\n * follow, and they shape this whole harness:\n *\n * - There is no `aria-valuenow` / `aria-valuemin` / `aria-valuemax` trio, and no\n * `min` / `max` / `step` attribute, anywhere in the DOM. So the value is read\n * out of the field's TEXT, and the bounds are read off the stepper buttons'\n * disabled state — which is precisely what a user is given, and the reason\n * {@link isIncrementDisabled} is named after the button rather than after \"at\n * the maximum\".\n * - The text and the value are different things, and the gap between them is this\n * control's trap. {@link getValueText} answers the raw characters;\n * {@link getValue} answers the number they spell. Mid-type the two disagree\n * with the MODEL as well: the component clamps and rounds on every keystroke\n * but leaves the typed text alone until blur, so a field showing `999` under\n * `[max]=\"10\"` has already committed `10`. Call {@link blur} — which is also\n * what emits `touch` — to make the field re-render from the committed value,\n * and assert the host's own signal when what you mean is \"the model\".\n *\n * Stepping has two paths and both are here: {@link increment} / {@link decrement}\n * click the buttons, {@link stepUp} / {@link stepDown} press ArrowUp / ArrowDown\n * on the field. The keyboard is the accessible path, and in a unit test it is\n * also the honest one — jsdom has no layout, so nothing that needs a hit test can\n * be driven by coordinates. `{ shift: true }` is this control's page-up: it steps\n * by ten. There is deliberately no `pageUp()` / `home()` / `end()`, because the\n * component handles the two arrow keys and nothing else.\n *\n * @example\n * ```ts\n * const qty = await loader.getHarness(WrInputNumberHarness.with({ placeholder: 'Quantity' }));\n *\n * await qty.setValue(4);\n * await qty.increment();\n * expect(await qty.getValue()).toBe(5);\n *\n * await qty.stepUp({ shift: true });\n * expect(await qty.isIncrementDisabled()).toBe(true); // [max]=\"15\"\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrInputNumberHarness extends ComponentHarness {\n static hostSelector = 'wr-input-number';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrInputNumberHarnessFilters = {}): HarnessPredicate<WrInputNumberHarness> {\n return new HarnessPredicate(WrInputNumberHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getValueText(), text))\n .addOption('value', options.value, async (harness, value) => (await harness.parsedValue()) === value)\n .addOption('placeholder', options.placeholder, (harness, placeholder) =>\n HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder)\n )\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n private readonly field = this.locatorFor('input[wrInput]');\n\n /** The raw characters in the field — mid-typed, ungrouped or nonsense included. */\n async getValueText(): Promise<string> {\n return (await this.field()).getProperty<string>('value');\n }\n\n /**\n * The number the field is SHOWING, or `null` when it is empty.\n *\n * Empty is `null` rather than `0` because the control draws that distinction\n * too — a required-field check has to be able to tell \"nothing entered\" from \"a\n * deliberate zero\". Text that is not a number throws instead: answering `null`\n * there would report a half-typed `-` as an empty field.\n *\n * `locale` defaults to the test runtime's, which for a stock Node run is the\n * same `en-US` Angular's `LOCALE_ID` defaults to. The component formats with\n * `LOCALE_ID` and a harness cannot reach that injector, so an app that provides\n * its own must pass it: `getValue('de-DE')`. Reading `'1.234,5'` as en-US\n * silently answers `1.2345`.\n */\n async getValue(locale?: string): Promise<number | null> {\n const text = await this.getValueText();\n if (!text.trim()) return null;\n\n const parsed = parseNumber(text, locale);\n if (parsed === null) {\n throw new Error(\n `WrInputNumberHarness.getValue(): the field is showing ${JSON.stringify(text)}, which is not a number in ` +\n `${locale ? `locale \"${locale}\"` : \"the test runtime's locale\"}. Mid-type the field holds text the ` +\n 'control has not accepted — read getValueText() for the characters, or blur() to make it re-render from ' +\n 'the committed value. If the app provides a LOCALE_ID of its own, pass it: getValue(\"de-DE\").'\n );\n }\n\n return parsed;\n }\n\n /**\n * Type a number into the field, replacing whatever is there.\n *\n * Written with the LOCALE's decimal separator rather than with `String(value)`:\n * in a comma-decimal locale `'1.5'` is read as `15`, because `.` is that\n * locale's THOUSANDS separator. Grouping is deliberately not added — nothing\n * requires it, and the field regroups itself on blur anyway.\n *\n * The field is cleared first, so the value passes through `null` on the way, as\n * it does for a user who selects all and types over it. The control parses every\n * keystroke, so `min` / `max` / `decimals` are applied AS YOU TYPE while the\n * text is left exactly as typed — see {@link getValue}.\n */\n async setValue(value: number, locale?: string): Promise<void> {\n if (!Number.isFinite(value)) {\n throw new Error(\n `WrInputNumberHarness.setValue(${value}): a field can only be typed a finite number. Use clear() to empty it.`\n );\n }\n\n const { decimal } = separatorsFor(locale);\n await this.setValueText(decimal === '.' ? String(value) : String(value).replace('.', decimal));\n }\n\n /**\n * Type raw characters into the field, replacing whatever is there.\n *\n * For everything a number cannot express: a partial entry (`'-'`, `'1.'`), an\n * already-grouped string, or plain nonsense. Nothing is normalised on the way\n * in, and — as with {@link setValue} — the field is cleared first.\n */\n async setValueText(text: string): Promise<void> {\n const field = await this.field();\n await field.clear();\n // `sendKeys('')` throws — clearing was the whole request.\n if (text) await field.sendKeys(text);\n }\n\n /** Empty the field, which sets the value to `null` — not to `0`. */\n async clear(): Promise<void> {\n await (await this.field()).clear();\n }\n\n /** Whether the ▲▼ column is rendered at all — `[showSteppers]=\"false\"` drops it. */\n async hasSteppers(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-input-number__steppers')()) !== null;\n }\n\n /**\n * Click ▲ once: up by `step`, clamped to `max`.\n *\n * A button at its bound — or on a disabled / readonly control — is `disabled`,\n * and the CDK does not deliver a click to a disabled element, so this is a\n * quiet no-op there rather than a throw. Ask {@link isIncrementDisabled} rather\n * than inferring the bound from a value that did not move. It throws only when\n * there is no stepper column to click.\n */\n async increment(): Promise<void> {\n await (await this.stepButton(1, 'increment')).click();\n }\n\n /** Click ▼ once: down by `step`, clamped to `min`. See {@link increment}. */\n async decrement(): Promise<void> {\n await (await this.stepButton(-1, 'decrement')).click();\n }\n\n /**\n * Whether ▲ refuses to step.\n *\n * Deliberately not named `isAtMax()`: the button is disabled at the maximum AND\n * on a disabled or readonly control, and it cannot tell you which. That\n * conflation is the control's own — it is what the user is offered — and the\n * name says so rather than promising a bound the DOM never publishes.\n */\n async isIncrementDisabled(): Promise<boolean> {\n return (await this.stepButton(1, 'isIncrementDisabled')).getProperty<boolean>('disabled');\n }\n\n /** Whether ▼ refuses to step — at the minimum, or on a locked control. */\n async isDecrementDisabled(): Promise<boolean> {\n return (await this.stepButton(-1, 'isDecrementDisabled')).getProperty<boolean>('disabled');\n }\n\n /**\n * Press ArrowUp on the field: up by `step`, or by TEN steps with\n * `{ shift: true }` — the control's page-up, since it handles no other keys.\n */\n async stepUp(options: { shift?: boolean } = {}): Promise<void> {\n await (await this.field()).sendKeys({ shift: options.shift === true }, TestKey.UP_ARROW);\n }\n\n /** Press ArrowDown on the field. See {@link stepUp} for the `shift` multiplier. */\n async stepDown(options: { shift?: boolean } = {}): Promise<void> {\n await (await this.field()).sendKeys({ shift: options.shift === true }, TestKey.DOWN_ARROW);\n }\n\n /** The `[prefix]` label (`'$'`), or `null` when the field has none. */\n async getPrefix(): Promise<string | null> {\n const prefix = await this.locatorForOptional('.wr-input-group__affix--prefix')();\n return prefix ? prefix.text() : null;\n }\n\n /**\n * The `[suffix]` label (`'kg'`), or `null` when the field has none.\n *\n * The stepper column rides in the same suffix slot and carries the same affix\n * class, so it is excluded here by name: a bare `--suffix` query answers with\n * the buttons' (empty) text for every field that has steppers and no suffix.\n */\n async getSuffix(): Promise<string | null> {\n const suffix = await this.locatorForOptional('.wr-input-group__affix--suffix:not(.wr-input-number__steppers)')();\n return suffix ? suffix.text() : null;\n }\n\n /** The field's placeholder. */\n async getPlaceholder(): Promise<string> {\n return (await this.field()).getProperty<string>('placeholder');\n }\n\n /**\n * The field's accessible name, or `null` when it has none of its own.\n *\n * Read from the native input, not from the host: an `aria-label` on a component\n * element does not reach the control inside it, which is why the component\n * takes an `ariaLabel` input and forwards it here (falling back to the\n * placeholder). Inside a `<wr-form-field>` the name comes from the label's\n * `for`, and `null` here is then correct rather than a missing name.\n */\n async getAriaLabel(): Promise<string | null> {\n return (await this.field()).getAttribute('aria-label');\n }\n\n /**\n * Whether the field refuses interaction.\n *\n * The native control's `disabled` property, not the host's\n * `wr-input-number--disabled` modifier: the modifier is the styling hook, the\n * property is what actually blocks typing, drops the field out of the tab order\n * and is reported to assistive technology.\n */\n async isDisabled(): Promise<boolean> {\n return (await this.field()).getProperty<boolean>('disabled');\n }\n\n /** Whether the field refuses new values while staying focusable and readable. */\n async isReadonly(): Promise<boolean> {\n return (await this.field()).getProperty<boolean>('readOnly');\n }\n\n /** Move focus to the field. While it is focused the control stops reformatting the text. */\n async focus(): Promise<void> {\n return (await this.field()).focus();\n }\n\n /**\n * Blur the field, which is what commits the display: the control re-renders the\n * text from its value — grouping it and applying `decimals` — and emits `touch`\n * so a bound `[formField]` can mark itself touched.\n */\n async blur(): Promise<void> {\n return (await this.field()).blur();\n }\n\n /** Whether the field currently has focus. */\n async isFocused(): Promise<boolean> {\n return (await this.field()).isFocused();\n }\n\n /**\n * The shown value for predicates: `null` for an empty field AND for text that is\n * not a number, so a mid-typed field matches no `value` filter instead of\n * throwing the whole query.\n */\n private async parsedValue(): Promise<number | null> {\n return parseNumber(await this.getValueText());\n }\n\n /**\n * One of the two stepper buttons, BY POSITION — ▲ first, ▼ second.\n *\n * Not by `aria-label`: those strings (\"Increment\" / \"Decrement\") are still\n * hard-coded English, so keying on them would make this harness the thing that\n * breaks when they move into the i18n catalog — the same call\n * `WrTimePanelHarness` makes about its own columns. Order is the layout\n * contract, and the spec pins it by asserting which way each button moves the\n * value.\n */\n private async stepButton(direction: 1 | -1, method: string): Promise<TestElement> {\n const buttons = await this.locatorForAll('.wr-input-number__step')();\n const button = buttons[direction === 1 ? 0 : 1];\n if (!button) {\n throw new Error(\n `WrInputNumberHarness.${method}(): this field has no stepper column — it was rendered with ` +\n '[showSteppers]=\"false\". Type a value with setValue(), or step it from the keyboard with stepUp() / ' +\n 'stepDown(), which work either way.'\n );\n }\n\n return button;\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-input-number-testing.mjs","sources":["../../../projects/lib/input-number/testing/wr-input-number-harness.ts","../../../projects/lib/input-number/testing/ngwr-input-number-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 { WrInputNumberHarnessFilters } from './interfaces';\n\n/** The decimal + grouping characters `Intl` formats `locale` with. */\nfunction separatorsFor(locale?: string): { decimal: string; group: string } {\n const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);\n return {\n decimal: parts.find(part => part.type === 'decimal')?.value ?? '.',\n group: parts.find(part => part.type === 'group')?.value ?? '',\n };\n}\n\n/**\n * Locale-formatted text back into a number — `null` when the text is empty or is\n * not a number at all.\n *\n * The same two-step the component parses with: drop the grouping character, then\n * normalise the decimal one to `.`. Doing it in that order matters, because in\n * half the world's locales the two are swapped and `Number('1.234,5')` is `NaN`\n * either way.\n */\nfunction parseNumber(text: string, locale?: string): number | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n\n const { decimal, group } = separatorsFor(locale);\n let normalised = group ? trimmed.split(group).join('') : trimmed;\n if (decimal !== '.') normalised = normalised.split(decimal).join('.');\n\n const parsed = Number(normalised);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Test harness for `<wr-input-number>`.\n *\n * The control is a `type=\"text\"` field plus a ▲▼ stepper column — deliberately\n * NOT an `<input type=\"number\">` and NOT a `role=\"spinbutton\"`, because it renders\n * grouped, locale-formatted text that a numeric input cannot hold. Two things\n * follow, and they shape this whole harness:\n *\n * - There is no `aria-valuenow` / `aria-valuemin` / `aria-valuemax` trio, and no\n * `min` / `max` / `step` attribute, anywhere in the DOM. So the value is read\n * out of the field's TEXT, and the bounds are read off the stepper buttons'\n * disabled state — which is precisely what a user is given, and the reason\n * {@link isIncrementDisabled} is named after the button rather than after \"at\n * the maximum\".\n * - The text and the value are different things, and the gap between them is this\n * control's trap. {@link getValueText} answers the raw characters;\n * {@link getValue} answers the number they spell. Mid-type the two disagree\n * with the MODEL as well: the component clamps and rounds on every keystroke\n * but leaves the typed text alone until blur, so a field showing `999` under\n * `[max]=\"10\"` has already committed `10`. Call {@link blur} — which is also\n * what emits `touch` — to make the field re-render from the committed value,\n * and assert the host's own signal when what you mean is \"the model\".\n *\n * Stepping has two paths and both are here: {@link increment} / {@link decrement}\n * click the buttons, {@link stepUp} / {@link stepDown} press ArrowUp / ArrowDown\n * on the field. The keyboard is the accessible path, and in a unit test it is\n * also the honest one — jsdom has no layout, so nothing that needs a hit test can\n * be driven by coordinates. `{ shift: true }` is this control's page-up: it steps\n * by ten. There is deliberately no `pageUp()` / `home()` / `end()`, because the\n * component handles the two arrow keys and nothing else.\n *\n * @example\n * ```ts\n * const qty = await loader.getHarness(WrInputNumberHarness.with({ placeholder: 'Quantity' }));\n *\n * await qty.setValue(4);\n * await qty.increment();\n * expect(await qty.getValue()).toBe(5);\n *\n * await qty.stepUp({ shift: true });\n * expect(await qty.isIncrementDisabled()).toBe(true); // [max]=\"15\"\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrInputNumberHarness extends ComponentHarness {\n static hostSelector = 'wr-input-number';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrInputNumberHarnessFilters = {}): HarnessPredicate<WrInputNumberHarness> {\n return new HarnessPredicate(WrInputNumberHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getValueText(), text))\n .addOption('value', options.value, async (harness, value) => (await harness.parsedValue()) === value)\n .addOption('placeholder', options.placeholder, (harness, placeholder) =>\n HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder)\n )\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n private readonly field = this.locatorFor('input[wrInput]');\n\n /** The raw characters in the field — mid-typed, ungrouped or nonsense included. */\n async getValueText(): Promise<string> {\n return (await this.field()).getProperty<string>('value');\n }\n\n /**\n * The number the field is SHOWING, or `null` when it is empty.\n *\n * Empty is `null` rather than `0` because the control draws that distinction\n * too — a required-field check has to be able to tell \"nothing entered\" from \"a\n * deliberate zero\". Text that is not a number throws instead: answering `null`\n * there would report a half-typed `-` as an empty field.\n *\n * `locale` defaults to the test runtime's, which for a stock Node run is the\n * same `en-US` Angular's `LOCALE_ID` defaults to. The component formats with\n * `LOCALE_ID` and a harness cannot reach that injector, so an app that provides\n * its own must pass it: `getValue('de-DE')`. Reading `'1.234,5'` as en-US\n * silently answers `1.2345`.\n */\n async getValue(locale?: string): Promise<number | null> {\n const text = await this.getValueText();\n if (!text.trim()) return null;\n\n const parsed = parseNumber(text, locale);\n if (parsed === null) {\n throw new Error(\n `WrInputNumberHarness.getValue(): the field is showing ${JSON.stringify(text)}, which is not a number in ` +\n `${locale ? `locale \"${locale}\"` : \"the test runtime's locale\"}. Mid-type the field holds text the ` +\n 'control has not accepted — read getValueText() for the characters, or blur() to make it re-render from ' +\n 'the committed value. If the app provides a LOCALE_ID of its own, pass it: getValue(\"de-DE\").'\n );\n }\n\n return parsed;\n }\n\n /**\n * Type a number into the field, replacing whatever is there.\n *\n * Written with the LOCALE's decimal separator rather than with `String(value)`:\n * in a comma-decimal locale `'1.5'` is read as `15`, because `.` is that\n * locale's THOUSANDS separator. Grouping is deliberately not added — nothing\n * requires it, and the field regroups itself on blur anyway.\n *\n * The field is cleared first, so the value passes through `null` on the way, as\n * it does for a user who selects all and types over it. The control parses every\n * keystroke, so `min` / `max` / `decimals` are applied AS YOU TYPE while the\n * text is left exactly as typed — see {@link getValue}.\n */\n async setValue(value: number, locale?: string): Promise<void> {\n if (!Number.isFinite(value)) {\n throw new Error(\n `WrInputNumberHarness.setValue(${value}): a field can only be typed a finite number. Use clear() to empty it.`\n );\n }\n\n const { decimal } = separatorsFor(locale);\n await this.setValueText(decimal === '.' ? String(value) : String(value).replace('.', decimal));\n }\n\n /**\n * Type raw characters into the field, replacing whatever is there.\n *\n * For everything a number cannot express: a partial entry (`'-'`, `'1.'`), an\n * already-grouped string, or plain nonsense. Nothing is normalised on the way\n * in, and — as with {@link setValue} — the field is cleared first.\n */\n async setValueText(text: string): Promise<void> {\n const field = await this.field();\n await field.clear();\n // `sendKeys('')` throws — clearing was the whole request.\n if (text) await field.sendKeys(text);\n }\n\n /** Empty the field, which sets the value to `null` — not to `0`. */\n async clear(): Promise<void> {\n await (await this.field()).clear();\n }\n\n /** Whether the ▲▼ column is rendered at all — `[showSteppers]=\"false\"` drops it. */\n async hasSteppers(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-input-number__steppers')()) !== null;\n }\n\n /**\n * Click ▲ once: up by `step`, clamped to `max`.\n *\n * A button at its bound — or on a disabled / readonly control — is `disabled`,\n * and the CDK does not deliver a click to a disabled element, so this is a\n * quiet no-op there rather than a throw. Ask {@link isIncrementDisabled} rather\n * than inferring the bound from a value that did not move. It throws only when\n * there is no stepper column to click.\n */\n async increment(): Promise<void> {\n await (await this.stepButton(1, 'increment')).click();\n }\n\n /** Click ▼ once: down by `step`, clamped to `min`. See {@link increment}. */\n async decrement(): Promise<void> {\n await (await this.stepButton(-1, 'decrement')).click();\n }\n\n /**\n * Whether ▲ refuses to step.\n *\n * Deliberately not named `isAtMax()`: the button is disabled at the maximum AND\n * on a disabled or readonly control, and it cannot tell you which. That\n * conflation is the control's own — it is what the user is offered — and the\n * name says so rather than promising a bound the DOM never publishes.\n */\n async isIncrementDisabled(): Promise<boolean> {\n return (await this.stepButton(1, 'isIncrementDisabled')).getProperty<boolean>('disabled');\n }\n\n /** Whether ▼ refuses to step — at the minimum, or on a locked control. */\n async isDecrementDisabled(): Promise<boolean> {\n return (await this.stepButton(-1, 'isDecrementDisabled')).getProperty<boolean>('disabled');\n }\n\n /**\n * Press ArrowUp on the field: up by `step`, or by TEN steps with\n * `{ shift: true }` — the control's page-up, since it handles no other keys.\n */\n async stepUp(options: { shift?: boolean } = {}): Promise<void> {\n await (await this.field()).sendKeys({ shift: options.shift === true }, TestKey.UP_ARROW);\n }\n\n /** Press ArrowDown on the field. See {@link stepUp} for the `shift` multiplier. */\n async stepDown(options: { shift?: boolean } = {}): Promise<void> {\n await (await this.field()).sendKeys({ shift: options.shift === true }, TestKey.DOWN_ARROW);\n }\n\n /** The `[prefix]` label (`'$'`), or `null` when the field has none. */\n async getPrefix(): Promise<string | null> {\n const prefix = await this.locatorForOptional('.wr-input-group__affix--prefix')();\n return prefix ? prefix.text() : null;\n }\n\n /**\n * The `[suffix]` label (`'kg'`), or `null` when the field has none.\n *\n * The stepper column rides in the same suffix slot and carries the same affix\n * class, so it is excluded here by name: a bare `--suffix` query answers with\n * the buttons' (empty) text for every field that has steppers and no suffix.\n */\n async getSuffix(): Promise<string | null> {\n const suffix = await this.locatorForOptional('.wr-input-group__affix--suffix:not(.wr-input-number__steppers)')();\n return suffix ? suffix.text() : null;\n }\n\n /** The field's placeholder. */\n async getPlaceholder(): Promise<string> {\n return (await this.field()).getProperty<string>('placeholder');\n }\n\n /**\n * The field's `aria-label`, or `null` when it carries none.\n *\n * Read from the native input, not from the host: an `aria-label` on a component\n * element does not reach the control inside it, which is why the component\n * takes an `ariaLabel` input and forwards it here.\n *\n * NOT the accessible name — `null` is the usual answer and usually correct.\n * Nothing is forwarded unless `[ariaLabel]` is bound: inside a\n * `<wr-form-field>` the name comes from the label's `for`, and standalone it\n * comes from the `placeholder` (see {@link getPlaceholder}), neither of which\n * is an attribute on this element.\n */\n async getAriaLabel(): Promise<string | null> {\n return (await this.field()).getAttribute('aria-label');\n }\n\n /**\n * Whether the field refuses interaction.\n *\n * The native control's `disabled` property, not the host's\n * `wr-input-number--disabled` modifier: the modifier is the styling hook, the\n * property is what actually blocks typing, drops the field out of the tab order\n * and is reported to assistive technology.\n */\n async isDisabled(): Promise<boolean> {\n return (await this.field()).getProperty<boolean>('disabled');\n }\n\n /** Whether the field refuses new values while staying focusable and readable. */\n async isReadonly(): Promise<boolean> {\n return (await this.field()).getProperty<boolean>('readOnly');\n }\n\n /** Move focus to the field. While it is focused the control stops reformatting the text. */\n async focus(): Promise<void> {\n return (await this.field()).focus();\n }\n\n /**\n * Blur the field, which is what commits the display: the control re-renders the\n * text from its value — grouping it and applying `decimals` — and emits `touch`\n * so a bound `[formField]` can mark itself touched.\n */\n async blur(): Promise<void> {\n return (await this.field()).blur();\n }\n\n /** Whether the field currently has focus. */\n async isFocused(): Promise<boolean> {\n return (await this.field()).isFocused();\n }\n\n /**\n * The shown value for predicates: `null` for an empty field AND for text that is\n * not a number, so a mid-typed field matches no `value` filter instead of\n * throwing the whole query.\n */\n private async parsedValue(): Promise<number | null> {\n return parseNumber(await this.getValueText());\n }\n\n /**\n * One of the two stepper buttons, BY POSITION — ▲ first, ▼ second.\n *\n * Not by `aria-label`: those strings resolve through the i18n catalog\n * (`inputNumber.increment` / `.decrement`) and an `[incrementLabel]` /\n * `[decrementLabel]` binding overrides them, so keying on them would make this\n * harness answer in whatever locale the app under test is configured for — the\n * same call `WrTimePanelHarness` makes about its own columns. Order is the\n * layout contract, and the spec pins it by asserting which way each button\n * moves the value.\n */\n private async stepButton(direction: 1 | -1, method: string): Promise<TestElement> {\n const buttons = await this.locatorForAll('.wr-input-number__step')();\n const button = buttons[direction === 1 ? 0 : 1];\n if (!button) {\n throw new Error(\n `WrInputNumberHarness.${method}(): this field has no stepper column — it was rendered with ` +\n '[showSteppers]=\"false\". Type a value with setValue(), or step it from the keyboard with stepUp() / ' +\n 'stepDown(), which work either way.'\n );\n }\n\n return button;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|