snice 2.5.4 → 3.2.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 +537 -869
- package/bin/templates/base/src/components/counter-button.ts +13 -26
- package/bin/templates/base/src/controllers/counter-controller.ts +3 -3
- package/dist/components/accordion/snice-accordion-item.d.ts +4 -5
- package/dist/components/accordion/snice-accordion-item.js +37 -39
- package/dist/components/accordion/snice-accordion-item.js.map +1 -1
- package/dist/components/accordion/snice-accordion.d.ts +5 -11
- package/dist/components/accordion/snice-accordion.js +51 -52
- package/dist/components/accordion/snice-accordion.js.map +1 -1
- package/dist/components/alert/snice-alert.d.ts +2 -6
- package/dist/components/alert/snice-alert.js +41 -56
- package/dist/components/alert/snice-alert.js.map +1 -1
- package/dist/components/avatar/snice-avatar.d.ts +2 -6
- package/dist/components/avatar/snice-avatar.js +64 -71
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/badge/snice-badge.d.ts +2 -3
- package/dist/components/badge/snice-badge.js +22 -23
- package/dist/components/badge/snice-badge.js.map +1 -1
- package/dist/components/banner/snice-banner.d.ts +22 -0
- package/dist/components/banner/snice-banner.js +180 -0
- package/dist/components/banner/snice-banner.js.map +1 -0
- package/dist/components/banner/snice-banner.types.d.ts +14 -0
- package/dist/components/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
- package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
- package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
- package/dist/components/button/snice-button.d.ts +3 -7
- package/dist/components/button/snice-button.js +37 -58
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/card/snice-card.d.ts +5 -8
- package/dist/components/card/snice-card.js +71 -56
- package/dist/components/card/snice-card.js.map +1 -1
- package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
- package/dist/components/checkbox/snice-checkbox.js +66 -137
- package/dist/components/checkbox/snice-checkbox.js.map +1 -1
- package/dist/components/chip/snice-chip.d.ts +5 -11
- package/dist/components/chip/snice-chip.js +44 -47
- package/dist/components/chip/snice-chip.js.map +1 -1
- package/dist/components/color-display/snice-color-display.d.ts +14 -0
- package/dist/components/color-display/snice-color-display.js +151 -0
- package/dist/components/color-display/snice-color-display.js.map +1 -0
- package/dist/components/color-display/snice-color-display.types.d.ts +10 -0
- package/dist/components/color-picker/snice-color-picker.d.ts +50 -0
- package/dist/components/color-picker/snice-color-picker.js +489 -0
- package/dist/components/color-picker/snice-color-picker.js.map +1 -0
- package/dist/components/color-picker/snice-color-picker.types.d.ts +19 -0
- package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
- package/dist/components/date-picker/snice-date-picker.js +134 -133
- package/dist/components/date-picker/snice-date-picker.js.map +1 -1
- package/dist/components/divider/snice-divider.d.ts +2 -4
- package/dist/components/divider/snice-divider.js +14 -22
- package/dist/components/divider/snice-divider.js.map +1 -1
- package/dist/components/drawer/snice-drawer.d.ts +4 -4
- package/dist/components/drawer/snice-drawer.js +25 -19
- package/dist/components/drawer/snice-drawer.js.map +1 -1
- package/dist/components/empty-state/snice-empty-state.d.ts +13 -0
- package/dist/components/empty-state/snice-empty-state.js +121 -0
- package/dist/components/empty-state/snice-empty-state.js.map +1 -0
- package/dist/components/empty-state/snice-empty-state.types.d.ts +9 -0
- package/dist/components/file-upload/snice-file-upload.d.ts +45 -0
- package/dist/components/file-upload/snice-file-upload.js +394 -0
- package/dist/components/file-upload/snice-file-upload.js.map +1 -0
- package/dist/components/file-upload/snice-file-upload.types.d.ts +22 -0
- package/dist/components/image/snice-image.d.ts +22 -0
- package/dist/components/image/snice-image.js +201 -0
- package/dist/components/image/snice-image.js.map +1 -0
- package/dist/components/image/snice-image.types.d.ts +17 -0
- package/dist/components/input/snice-input.d.ts +8 -6
- package/dist/components/input/snice-input.js +122 -105
- package/dist/components/input/snice-input.js.map +1 -1
- package/dist/components/kpi/snice-kpi.d.ts +16 -0
- package/dist/components/kpi/snice-kpi.js +162 -0
- package/dist/components/kpi/snice-kpi.js.map +1 -0
- package/dist/components/kpi/snice-kpi.types.d.ts +12 -0
- package/dist/components/layout/snice-layout-blog.d.ts +4 -4
- package/dist/components/layout/snice-layout-blog.js +21 -19
- package/dist/components/layout/snice-layout-blog.js.map +1 -1
- package/dist/components/layout/snice-layout-card.d.ts +2 -2
- package/dist/components/layout/snice-layout-card.js +16 -9
- package/dist/components/layout/snice-layout-card.js.map +1 -1
- package/dist/components/layout/snice-layout-centered.d.ts +2 -2
- package/dist/components/layout/snice-layout-centered.js +14 -7
- package/dist/components/layout/snice-layout-centered.js.map +1 -1
- package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
- package/dist/components/layout/snice-layout-dashboard.js +38 -30
- package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
- package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
- package/dist/components/layout/snice-layout-fullscreen.js +17 -10
- package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
- package/dist/components/layout/snice-layout-landing.d.ts +4 -4
- package/dist/components/layout/snice-layout-landing.js +21 -19
- package/dist/components/layout/snice-layout-landing.js.map +1 -1
- package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
- package/dist/components/layout/snice-layout-minimal.js +17 -6
- package/dist/components/layout/snice-layout-minimal.js.map +1 -1
- package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
- package/dist/components/layout/snice-layout-sidebar.js +42 -20
- package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
- package/dist/components/layout/snice-layout-split.d.ts +2 -2
- package/dist/components/layout/snice-layout-split.js +14 -7
- package/dist/components/layout/snice-layout-split.js.map +1 -1
- package/dist/components/layout/snice-layout.d.ts +4 -4
- package/dist/components/layout/snice-layout.js +16 -10
- package/dist/components/layout/snice-layout.js.map +1 -1
- package/dist/components/link/snice-link.d.ts +13 -0
- package/dist/components/link/snice-link.js +137 -0
- package/dist/components/link/snice-link.js.map +1 -0
- package/dist/components/link/snice-link.types.d.ts +11 -0
- package/dist/components/login/snice-login.d.ts +6 -11
- package/dist/components/login/snice-login.js +97 -71
- package/dist/components/login/snice-login.js.map +1 -1
- package/dist/components/modal/snice-modal.d.ts +5 -9
- package/dist/components/modal/snice-modal.js +47 -78
- package/dist/components/modal/snice-modal.js.map +1 -1
- package/dist/components/nav/snice-nav.d.ts +13 -7
- package/dist/components/nav/snice-nav.js +191 -100
- package/dist/components/nav/snice-nav.js.map +1 -1
- package/dist/components/nav/snice-nav.types.d.ts +3 -3
- package/dist/components/pagination/snice-pagination.d.ts +6 -7
- package/dist/components/pagination/snice-pagination.js +94 -81
- package/dist/components/pagination/snice-pagination.js.map +1 -1
- package/dist/components/progress/snice-progress.d.ts +2 -7
- package/dist/components/progress/snice-progress.js +41 -98
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/radio/snice-radio.d.ts +4 -4
- package/dist/components/radio/snice-radio.js +52 -44
- package/dist/components/radio/snice-radio.js.map +1 -1
- package/dist/components/select/snice-option.d.ts +2 -1
- package/dist/components/select/snice-option.js +12 -5
- package/dist/components/select/snice-option.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +9 -21
- package/dist/components/select/snice-select.js +98 -170
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
- package/dist/components/skeleton/snice-skeleton.js +18 -49
- package/dist/components/skeleton/snice-skeleton.js.map +1 -1
- package/dist/components/slider/snice-slider.d.ts +53 -0
- package/dist/components/slider/snice-slider.js +479 -0
- package/dist/components/slider/snice-slider.js.map +1 -0
- package/dist/components/slider/snice-slider.types.d.ts +26 -0
- package/dist/components/snice-cell-C0slgOpe.js +4 -0
- package/dist/components/snice-cell-C0slgOpe.js.map +1 -0
- package/dist/components/sparkline/snice-sparkline.d.ts +21 -0
- package/dist/components/sparkline/snice-sparkline.js +228 -0
- package/dist/components/sparkline/snice-sparkline.js.map +1 -0
- package/dist/components/sparkline/snice-sparkline.types.d.ts +16 -0
- package/dist/components/spinner/snice-spinner.d.ts +10 -0
- package/dist/components/spinner/snice-spinner.js +109 -0
- package/dist/components/spinner/snice-spinner.js.map +1 -0
- package/dist/components/spinner/snice-spinner.types.d.ts +8 -0
- package/dist/components/stepper/snice-stepper-panel.d.ts +8 -0
- package/dist/components/stepper/snice-stepper-panel.js +70 -0
- package/dist/components/stepper/snice-stepper-panel.js.map +1 -0
- package/dist/components/stepper/snice-stepper-panel.types.d.ts +4 -0
- package/dist/components/stepper/snice-stepper.d.ts +15 -0
- package/dist/components/stepper/snice-stepper.js +163 -0
- package/dist/components/stepper/snice-stepper.js.map +1 -0
- package/dist/components/stepper/snice-stepper.types.d.ts +13 -0
- package/dist/components/switch/snice-switch.d.ts +2 -2
- package/dist/components/switch/snice-switch.js +38 -26
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-cell-actions.d.ts +24 -0
- package/dist/components/table/snice-cell-actions.js +149 -0
- package/dist/components/table/snice-cell-actions.js.map +1 -0
- package/dist/components/table/snice-cell-boolean.d.ts +2 -2
- package/dist/components/table/snice-cell-boolean.js +13 -7
- package/dist/components/table/snice-cell-boolean.js.map +1 -1
- package/dist/components/table/snice-cell-color.d.ts +18 -0
- package/dist/components/table/snice-cell-color.js +149 -0
- package/dist/components/table/snice-cell-color.js.map +1 -0
- package/dist/components/table/snice-cell-currency.d.ts +24 -0
- package/dist/components/table/snice-cell-currency.js +235 -0
- package/dist/components/table/snice-cell-currency.js.map +1 -0
- package/dist/components/table/snice-cell-date.d.ts +2 -2
- package/dist/components/table/snice-cell-date.js +14 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-duration.d.ts +2 -2
- package/dist/components/table/snice-cell-duration.js +12 -6
- package/dist/components/table/snice-cell-duration.js.map +1 -1
- package/dist/components/table/snice-cell-email.d.ts +15 -0
- package/dist/components/table/snice-cell-email.js +125 -0
- package/dist/components/table/snice-cell-email.js.map +1 -0
- package/dist/components/table/snice-cell-filesize.d.ts +2 -2
- package/dist/components/table/snice-cell-filesize.js +12 -6
- package/dist/components/table/snice-cell-filesize.js.map +1 -1
- package/dist/components/table/snice-cell-image.d.ts +20 -0
- package/dist/components/table/snice-cell-image.js +162 -0
- package/dist/components/table/snice-cell-image.js.map +1 -0
- package/dist/components/table/snice-cell-json.d.ts +20 -0
- package/dist/components/table/snice-cell-json.js +186 -0
- package/dist/components/table/snice-cell-json.js.map +1 -0
- package/dist/components/table/snice-cell-link.d.ts +17 -0
- package/dist/components/table/snice-cell-link.js +142 -0
- package/dist/components/table/snice-cell-link.js.map +1 -0
- package/dist/components/table/snice-cell-location.d.ts +19 -0
- package/dist/components/table/snice-cell-location.js +185 -0
- package/dist/components/table/snice-cell-location.js.map +1 -0
- package/dist/components/table/snice-cell-number.d.ts +2 -2
- package/dist/components/table/snice-cell-number.js +12 -6
- package/dist/components/table/snice-cell-number.js.map +1 -1
- package/dist/components/table/snice-cell-percentage.d.ts +22 -0
- package/dist/components/table/snice-cell-percentage.js +208 -0
- package/dist/components/table/snice-cell-percentage.js.map +1 -0
- package/dist/components/table/snice-cell-phone.d.ts +18 -0
- package/dist/components/table/snice-cell-phone.js +153 -0
- package/dist/components/table/snice-cell-phone.js.map +1 -0
- package/dist/components/table/snice-cell-progress.d.ts +2 -2
- package/dist/components/table/snice-cell-progress.js +12 -6
- package/dist/components/table/snice-cell-progress.js.map +1 -1
- package/dist/components/table/snice-cell-rating.d.ts +2 -2
- package/dist/components/table/snice-cell-rating.js +12 -6
- package/dist/components/table/snice-cell-rating.js.map +1 -1
- package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
- package/dist/components/table/snice-cell-sparkline.js +13 -7
- package/dist/components/table/snice-cell-sparkline.js.map +1 -1
- package/dist/components/table/snice-cell-status.d.ts +17 -0
- package/dist/components/table/snice-cell-status.js +144 -0
- package/dist/components/table/snice-cell-status.js.map +1 -0
- package/dist/components/table/snice-cell-tag.d.ts +16 -0
- package/dist/components/table/snice-cell-tag.js +131 -0
- package/dist/components/table/snice-cell-tag.js.map +1 -0
- package/dist/components/table/snice-cell-text.d.ts +2 -2
- package/dist/components/table/snice-cell-text.js +14 -8
- package/dist/components/table/snice-cell-text.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +2 -2
- package/dist/components/table/snice-cell.js +12 -6
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-column.d.ts +1 -1
- package/dist/components/table/snice-column.js +6 -3
- package/dist/components/table/snice-column.js.map +1 -1
- package/dist/components/table/snice-header.d.ts +5 -5
- package/dist/components/table/snice-header.js +60 -50
- package/dist/components/table/snice-header.js.map +1 -1
- package/dist/components/table/snice-progress.d.ts +2 -2
- package/dist/components/table/snice-progress.js +18 -11
- package/dist/components/table/snice-progress.js.map +1 -1
- package/dist/components/table/snice-rating.d.ts +2 -2
- package/dist/components/table/snice-rating.js +15 -8
- package/dist/components/table/snice-rating.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +17 -6
- package/dist/components/table/snice-row.js +95 -44
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +18 -10
- package/dist/components/table/snice-table.js +355 -173
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +101 -2
- package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
- package/dist/components/tabs/snice-tab-panel.js +12 -6
- package/dist/components/tabs/snice-tab-panel.js.map +1 -1
- package/dist/components/tabs/snice-tab.d.ts +6 -5
- package/dist/components/tabs/snice-tab.js +36 -19
- package/dist/components/tabs/snice-tab.js.map +1 -1
- package/dist/components/tabs/snice-tabs.d.ts +5 -5
- package/dist/components/tabs/snice-tabs.js +38 -28
- package/dist/components/tabs/snice-tabs.js.map +1 -1
- package/dist/components/textarea/snice-textarea.d.ts +52 -0
- package/dist/components/textarea/snice-textarea.js +407 -0
- package/dist/components/textarea/snice-textarea.js.map +1 -0
- package/dist/components/textarea/snice-textarea.types.d.ts +30 -0
- package/dist/components/timeline/snice-timeline.d.ts +11 -0
- package/dist/components/timeline/snice-timeline.js +112 -0
- package/dist/components/timeline/snice-timeline.js.map +1 -0
- package/dist/components/timeline/snice-timeline.types.d.ts +16 -0
- package/dist/components/toast/snice-toast-container.d.ts +7 -7
- package/dist/components/toast/snice-toast-container.js +19 -12
- package/dist/components/toast/snice-toast-container.js.map +1 -1
- package/dist/components/toast/snice-toast.d.ts +3 -15
- package/dist/components/toast/snice-toast.js +49 -108
- package/dist/components/toast/snice-toast.js.map +1 -1
- package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
- package/dist/components/tooltip/snice-tooltip.js +15 -8
- package/dist/components/tooltip/snice-tooltip.js.map +1 -1
- package/dist/context.d.ts +44 -0
- package/dist/element-ready.d.ts +40 -0
- package/dist/{types/element.d.ts → element.d.ts} +2 -8
- package/dist/{types/events.d.ts → events.d.ts} +0 -4
- package/dist/index.cjs +2556 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +2535 -604
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +2556 -605
- package/dist/index.iife.js.map +1 -1
- package/dist/method-decorators.d.ts +121 -0
- package/dist/on.d.ts +59 -0
- package/dist/parts.d.ts +156 -0
- package/dist/render-debug.d.ts +27 -0
- package/dist/render-tracker.d.ts +14 -0
- package/dist/render.d.ts +96 -0
- package/dist/symbols.cjs +163 -0
- package/dist/symbols.cjs.map +1 -1
- package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
- package/dist/symbols.esm.js +27 -3
- package/dist/symbols.esm.js.map +1 -1
- package/dist/template.d.ts +99 -0
- package/dist/transitions.cjs +219 -0
- package/dist/transitions.esm.js +2 -2
- package/dist/types/context.d.ts +48 -0
- package/dist/types/element-options.d.ts +26 -0
- package/dist/types/index.d.ts +25 -9
- package/dist/types/nav-context.d.ts +19 -0
- package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
- package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
- package/docs/ai/README.md +26 -0
- package/docs/ai/api.md +175 -0
- package/docs/ai/architecture.md +160 -0
- package/docs/ai/components/accordion.md +174 -0
- package/docs/ai/components/alert.md +77 -0
- package/docs/ai/components/avatar.md +61 -0
- package/docs/ai/components/badge.md +69 -0
- package/docs/ai/components/banner.md +84 -0
- package/docs/ai/components/breadcrumbs.md +74 -0
- package/docs/ai/components/button.md +75 -0
- package/docs/ai/components/card.md +61 -0
- package/docs/ai/components/checkbox.md +74 -0
- package/docs/ai/components/chip.md +73 -0
- package/docs/ai/components/color-display.md +48 -0
- package/docs/ai/components/color-picker.md +75 -0
- package/docs/ai/components/date-picker.md +75 -0
- package/docs/ai/components/divider.md +66 -0
- package/docs/ai/components/drawer.md +80 -0
- package/docs/ai/components/empty-state.md +72 -0
- package/docs/ai/components/file-upload.md +93 -0
- package/docs/ai/components/image.md +60 -0
- package/docs/ai/components/input.md +111 -0
- package/docs/ai/components/kpi.md +158 -0
- package/docs/ai/components/link.md +77 -0
- package/docs/ai/components/login.md +109 -0
- package/docs/ai/components/modal.md +67 -0
- package/docs/ai/components/nav.md +76 -0
- package/docs/ai/components/pagination.md +55 -0
- package/docs/ai/components/progress.md +72 -0
- package/docs/ai/components/radio.md +79 -0
- package/docs/ai/components/select.md +92 -0
- package/docs/ai/components/skeleton.md +57 -0
- package/docs/ai/components/slider.md +87 -0
- package/docs/ai/components/sparkline.md +168 -0
- package/docs/ai/components/spinner.md +47 -0
- package/docs/ai/components/stepper.md +216 -0
- package/docs/ai/components/switch.md +53 -0
- package/docs/ai/components/table.md +227 -0
- package/docs/ai/components/tabs.md +83 -0
- package/docs/ai/components/textarea.md +87 -0
- package/docs/ai/components/timeline.md +77 -0
- package/docs/ai/components/toast.md +140 -0
- package/docs/ai/components/tooltip.md +146 -0
- package/docs/ai/patterns.md +244 -0
- package/docs/components/accordion.md +558 -0
- package/docs/components/banner.md +106 -0
- package/docs/components/color-display.md +96 -0
- package/docs/components/color-picker.md +81 -0
- package/docs/components/drawer.md +602 -0
- package/docs/components/empty-state.md +79 -0
- package/docs/components/file-upload.md +263 -0
- package/docs/components/image.md +110 -0
- package/docs/components/kpi.md +251 -0
- package/docs/components/link.md +229 -0
- package/docs/components/modal.md +558 -0
- package/docs/components/nav.md +239 -0
- package/docs/components/pagination.md +289 -0
- package/docs/components/select.md +599 -0
- package/docs/components/slider.md +297 -0
- package/docs/components/sparkline.md +293 -0
- package/docs/components/spinner.md +63 -0
- package/docs/components/stepper.md +410 -0
- package/docs/components/switch.md +354 -0
- package/docs/components/tabs.md +546 -0
- package/docs/components/textarea.md +235 -0
- package/docs/components/timeline.md +192 -0
- package/docs/components/toast.md +506 -0
- package/docs/components/tooltip.md +523 -0
- package/docs/controllers.md +744 -0
- package/docs/elements.md +855 -0
- package/docs/events.md +807 -0
- package/docs/migration-v2-to-v3.md +569 -0
- package/docs/observe.md +588 -0
- package/docs/placards.md +401 -0
- package/docs/request-response.md +852 -0
- package/docs/routing.md +1186 -0
- package/package.json +11 -11
- package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
- package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
- package/dist/types/types/index.d.ts +0 -23
- /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
- /package/dist/{types/global.d.ts → global.d.ts} +0 -0
- /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
- /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
- /package/dist/{types/router.d.ts → router.d.ts} +0 -0
- /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
- /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
- /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
- /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
- /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
- /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
- /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
- /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
- /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
- /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
- /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
- /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
- /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
- /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
- /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
- /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
- /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
- /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
- /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
- /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
- /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
- /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
- /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
- /package/dist/{types/utils.d.ts → utils.d.ts} +0 -0
package/dist/index.iife.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* snice
|
|
3
|
-
* Imperative TypeScript framework for building vanilla web components with decorators, routing, and controllers. No virtual DOM, no build complexity.
|
|
2
|
+
* snice v3.1.0
|
|
3
|
+
* Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
|
|
4
4
|
* (c) 2024
|
|
5
5
|
* Released under the MIT License.
|
|
6
6
|
*/
|
|
@@ -39,10 +39,12 @@ var Snice = (function (exports) {
|
|
|
39
39
|
// Internal element state symbols
|
|
40
40
|
const READY_PROMISE = getSymbol('ready-promise');
|
|
41
41
|
const READY_RESOLVE = getSymbol('ready-resolve');
|
|
42
|
+
getSymbol('rendered-promise');
|
|
43
|
+
getSymbol('rendered-resolve');
|
|
42
44
|
const CONTROLLER = getSymbol('controller');
|
|
43
45
|
const INITIALIZED = getSymbol('initialized');
|
|
44
46
|
// Event handler symbols
|
|
45
|
-
|
|
47
|
+
getSymbol('on-handlers');
|
|
46
48
|
// Controller symbols
|
|
47
49
|
const CONTROLLER_KEY = getSymbol('controller-key');
|
|
48
50
|
const CONTROLLER_NAME_KEY = getSymbol('controller-name');
|
|
@@ -54,13 +56,14 @@ var Snice = (function (exports) {
|
|
|
54
56
|
// Property symbols
|
|
55
57
|
const PROPERTIES = getSymbol('properties');
|
|
56
58
|
const PROPERTY_VALUES = getSymbol('property-values');
|
|
59
|
+
const PRE_INIT_PROPERTY_VALUES = getSymbol('pre-init-property-values');
|
|
57
60
|
const PROPERTIES_INITIALIZED = getSymbol('properties-initialized');
|
|
58
61
|
const PROPERTY_WATCHERS = getSymbol('property-watchers');
|
|
59
62
|
const EXPLICITLY_SET_PROPERTIES = getSymbol('explicitly-set-properties');
|
|
60
63
|
// Router context symbol
|
|
61
64
|
const ROUTER_CONTEXT = getSymbol('router-context');
|
|
62
65
|
getSymbol('current-page-marker');
|
|
63
|
-
|
|
66
|
+
getSymbol('context-request-handler');
|
|
64
67
|
const PAGE_TRANSITION = getSymbol('page-transition');
|
|
65
68
|
const CREATED_AT = getSymbol('created-at');
|
|
66
69
|
// Lifecycle symbols
|
|
@@ -71,313 +74,34 @@ var Snice = (function (exports) {
|
|
|
71
74
|
// Observer symbols
|
|
72
75
|
const OBSERVERS = getSymbol('observers');
|
|
73
76
|
// Part symbols
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
getSymbol('parts');
|
|
78
|
+
getSymbol('part-timers');
|
|
76
79
|
// Lifecycle callback timers
|
|
77
80
|
const MOVED_TIMERS = getSymbol('moved-timers');
|
|
78
81
|
const ADOPTED_TIMERS = getSymbol('adopted-timers');
|
|
79
82
|
// Dispatch timing symbols
|
|
80
83
|
const DISPATCH_TIMERS = getSymbol('dispatch-timers');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Normalize to array and expand at decoration time
|
|
103
|
-
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
104
|
-
// Create a handler entry for each event
|
|
105
|
-
for (const event of eventNames) {
|
|
106
|
-
constructor.prototype[ON_HANDLERS].push({
|
|
107
|
-
eventName: event,
|
|
108
|
-
selector,
|
|
109
|
-
methodName: propertyKey,
|
|
110
|
-
method: target,
|
|
111
|
-
options: opts
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
// Helper to setup event handlers for elements
|
|
118
|
-
function setupEventHandlers(instance, element) {
|
|
119
|
-
const handlers = instance.constructor.prototype[ON_HANDLERS];
|
|
120
|
-
if (!handlers)
|
|
121
|
-
return;
|
|
122
|
-
// Initialize cleanup object if needed
|
|
123
|
-
if (!instance[CLEANUP]) {
|
|
124
|
-
instance[CLEANUP] = { events: [], channels: [] };
|
|
125
|
-
}
|
|
126
|
-
for (const handler of handlers) {
|
|
127
|
-
// Get the current method from the instance (preserves decorator stacking)
|
|
128
|
-
const currentMethod = instance[handler.method.name];
|
|
129
|
-
const originalMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
|
|
130
|
-
const handlerOptions = handler.options || {};
|
|
131
|
-
// Parse event name for key modifiers
|
|
132
|
-
const [baseEventName, keyModifier] = handler.eventName.split(':');
|
|
133
|
-
// Create debounced/throttled wrapper if needed
|
|
134
|
-
const createTimedWrapper = (method) => {
|
|
135
|
-
if (handlerOptions.debounce) {
|
|
136
|
-
let timeoutId;
|
|
137
|
-
return function (...args) {
|
|
138
|
-
clearTimeout(timeoutId);
|
|
139
|
-
timeoutId = setTimeout(() => method.apply(this, args), handlerOptions.debounce);
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (handlerOptions.throttle) {
|
|
143
|
-
let lastCall = 0;
|
|
144
|
-
let timeoutId;
|
|
145
|
-
return function (...args) {
|
|
146
|
-
const now = Date.now();
|
|
147
|
-
const remaining = handlerOptions.throttle - (now - lastCall);
|
|
148
|
-
if (remaining <= 0) {
|
|
149
|
-
clearTimeout(timeoutId);
|
|
150
|
-
lastCall = now;
|
|
151
|
-
method.apply(this, args);
|
|
152
|
-
}
|
|
153
|
-
else if (!timeoutId) {
|
|
154
|
-
timeoutId = setTimeout(() => {
|
|
155
|
-
lastCall = Date.now();
|
|
156
|
-
timeoutId = null;
|
|
157
|
-
method.apply(this, args);
|
|
158
|
-
}, remaining);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return method;
|
|
163
|
-
};
|
|
164
|
-
// Create the event handler with key modifier support
|
|
165
|
-
const createEventHandler = (method) => {
|
|
166
|
-
if (keyModifier && (baseEventName === 'keydown' || baseEventName === 'keyup' || baseEventName === 'keypress')) {
|
|
167
|
-
return (event) => {
|
|
168
|
-
const keyEvent = event;
|
|
169
|
-
// Helper to normalize key names (e.g., "Space" -> " ")
|
|
170
|
-
const normalizeKey = (key) => {
|
|
171
|
-
if (key === 'Space')
|
|
172
|
-
return ' ';
|
|
173
|
-
return key;
|
|
174
|
-
};
|
|
175
|
-
// Check for "any modifiers" match with ~ prefix
|
|
176
|
-
if (keyModifier.startsWith('~')) {
|
|
177
|
-
const key = normalizeKey(keyModifier.slice(1)); // Remove the ~ and normalize
|
|
178
|
-
// Match if key matches, regardless of modifiers
|
|
179
|
-
if (keyEvent.key === key) {
|
|
180
|
-
method(event);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
// Check for modifier combinations using +
|
|
185
|
-
if (keyModifier.includes('+')) {
|
|
186
|
-
const parts = keyModifier.split('+');
|
|
187
|
-
const key = normalizeKey(parts[parts.length - 1]); // Last part is the actual key
|
|
188
|
-
const modifiers = parts.slice(0, -1); // Everything else is modifiers
|
|
189
|
-
// Check the actual key
|
|
190
|
-
if (keyEvent.key !== key)
|
|
191
|
-
return;
|
|
192
|
-
// Create a set of expected modifiers
|
|
193
|
-
const expectedModifiers = new Set(modifiers.map((m) => m.toLowerCase()));
|
|
194
|
-
const hasCtrl = expectedModifiers.has('ctrl');
|
|
195
|
-
const hasShift = expectedModifiers.has('shift');
|
|
196
|
-
const hasAlt = expectedModifiers.has('alt');
|
|
197
|
-
const hasMeta = expectedModifiers.has('meta') || expectedModifiers.has('cmd');
|
|
198
|
-
// Check that expected modifiers are pressed and unexpected ones are not
|
|
199
|
-
const modifiersMatch = keyEvent.ctrlKey === hasCtrl &&
|
|
200
|
-
keyEvent.shiftKey === hasShift &&
|
|
201
|
-
keyEvent.altKey === hasAlt &&
|
|
202
|
-
keyEvent.metaKey === hasMeta;
|
|
203
|
-
if (modifiersMatch) {
|
|
204
|
-
method(event);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
// Default: exact match (no modifiers allowed)
|
|
209
|
-
const key = normalizeKey(keyModifier);
|
|
210
|
-
// Only match if key matches AND no modifiers are pressed
|
|
211
|
-
if (keyEvent.key === key &&
|
|
212
|
-
!keyEvent.ctrlKey &&
|
|
213
|
-
!keyEvent.shiftKey &&
|
|
214
|
-
!keyEvent.altKey &&
|
|
215
|
-
!keyEvent.metaKey) {
|
|
216
|
-
method(event);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return method;
|
|
222
|
-
};
|
|
223
|
-
// Apply timing wrapper (debounce/throttle)
|
|
224
|
-
const timedMethod = createTimedWrapper(originalMethod);
|
|
225
|
-
// Wrap boundMethod in try-catch for error isolation
|
|
226
|
-
const wrappedMethod = createEventHandler((event) => {
|
|
227
|
-
try {
|
|
228
|
-
// Apply automatic preventDefault/stopPropagation if configured
|
|
229
|
-
if (handlerOptions.preventDefault) {
|
|
230
|
-
event.preventDefault();
|
|
231
|
-
}
|
|
232
|
-
if (handlerOptions.stopPropagation) {
|
|
233
|
-
event.stopPropagation();
|
|
234
|
-
}
|
|
235
|
-
return timedMethod(event);
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
239
|
-
// Don't rethrow - allow other handlers to continue
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
if (handler.selector) {
|
|
243
|
-
// Delegated event handling - use shadow root if available
|
|
244
|
-
const eventRoot = element.shadowRoot || element;
|
|
245
|
-
const delegatedHandler = (event) => {
|
|
246
|
-
const target = event.target;
|
|
247
|
-
let shouldHandle = false;
|
|
248
|
-
if (target.matches && target.matches(handler.selector)) {
|
|
249
|
-
shouldHandle = true;
|
|
250
|
-
}
|
|
251
|
-
else if (target.closest) {
|
|
252
|
-
const closest = target.closest(handler.selector);
|
|
253
|
-
if (closest) {
|
|
254
|
-
shouldHandle = true;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (shouldHandle) {
|
|
258
|
-
// Apply automatic preventDefault/stopPropagation only if we're handling this event
|
|
259
|
-
if (handlerOptions.preventDefault) {
|
|
260
|
-
event.preventDefault();
|
|
261
|
-
}
|
|
262
|
-
if (handlerOptions.stopPropagation) {
|
|
263
|
-
event.stopPropagation();
|
|
264
|
-
event.stopImmediatePropagation(); // Also stop other handlers on same element
|
|
265
|
-
}
|
|
266
|
-
wrappedMethod(event);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
const listenerOptions = {
|
|
270
|
-
capture: handlerOptions.capture || false,
|
|
271
|
-
once: handlerOptions.once || false,
|
|
272
|
-
passive: handlerOptions.passive || false
|
|
273
|
-
};
|
|
274
|
-
eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
275
|
-
instance[CLEANUP].events.push(() => {
|
|
276
|
-
eventRoot.removeEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
// Direct event handling - always on the element itself
|
|
281
|
-
const listenerOptions = {
|
|
282
|
-
capture: handlerOptions.capture || false,
|
|
283
|
-
once: handlerOptions.once || false,
|
|
284
|
-
passive: handlerOptions.passive || false
|
|
285
|
-
};
|
|
286
|
-
element.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
287
|
-
instance[CLEANUP].events.push(() => {
|
|
288
|
-
element.removeEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// Helper to cleanup event handlers
|
|
294
|
-
function cleanupEventHandlers(instance) {
|
|
295
|
-
if (instance[CLEANUP]?.events) {
|
|
296
|
-
for (const cleanup of instance[CLEANUP].events) {
|
|
297
|
-
cleanup();
|
|
298
|
-
}
|
|
299
|
-
instance[CLEANUP].events = [];
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Decorator that automatically dispatches a custom event after a method is called.
|
|
304
|
-
* The return value of the method becomes the event detail.
|
|
305
|
-
*
|
|
306
|
-
* @param eventName The name of the event to dispatch
|
|
307
|
-
* @param options Optional configuration extending EventInit
|
|
308
|
-
*/
|
|
309
|
-
function dispatch(eventName, options) {
|
|
310
|
-
return function (originalMethod, _context) {
|
|
311
|
-
return function (...args) {
|
|
312
|
-
// Create timing wrappers for dispatch (per-instance)
|
|
313
|
-
if (!this[DISPATCH_TIMERS]) {
|
|
314
|
-
this[DISPATCH_TIMERS] = new Map();
|
|
315
|
-
}
|
|
316
|
-
const timerKey = `${eventName}_${_context.name}`;
|
|
317
|
-
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
318
|
-
this[DISPATCH_TIMERS].set(timerKey, {
|
|
319
|
-
debounceTimeout: null,
|
|
320
|
-
throttleLastCall: 0,
|
|
321
|
-
throttleTimeout: null
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
325
|
-
// Call the original method with preserved this context
|
|
326
|
-
const result = originalMethod.apply(this, args);
|
|
327
|
-
// Helper to dispatch the event
|
|
328
|
-
const doDispatch = (detail) => {
|
|
329
|
-
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
330
|
-
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
// Create event with spread operator for options
|
|
334
|
-
const event = new CustomEvent(eventName, {
|
|
335
|
-
bubbles: true, // Default to true for component events
|
|
336
|
-
composed: true, // Allow crossing shadow DOM boundaries
|
|
337
|
-
...options, // Spread all EventInit options
|
|
338
|
-
detail
|
|
339
|
-
});
|
|
340
|
-
this.dispatchEvent(event);
|
|
341
|
-
};
|
|
342
|
-
// Helper to handle timed dispatch
|
|
343
|
-
const timedDispatch = (detail) => {
|
|
344
|
-
if (options?.debounce) {
|
|
345
|
-
clearTimeout(timers.debounceTimeout);
|
|
346
|
-
timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
|
|
347
|
-
}
|
|
348
|
-
else if (options?.throttle) {
|
|
349
|
-
const now = Date.now();
|
|
350
|
-
const remaining = options.throttle - (now - timers.throttleLastCall);
|
|
351
|
-
if (remaining <= 0) {
|
|
352
|
-
clearTimeout(timers.throttleTimeout);
|
|
353
|
-
timers.throttleLastCall = now;
|
|
354
|
-
doDispatch(detail);
|
|
355
|
-
}
|
|
356
|
-
else if (!timers.throttleTimeout) {
|
|
357
|
-
timers.throttleTimeout = setTimeout(() => {
|
|
358
|
-
timers.throttleLastCall = Date.now();
|
|
359
|
-
timers.throttleTimeout = null;
|
|
360
|
-
doDispatch(detail);
|
|
361
|
-
}, remaining);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
doDispatch(detail);
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
// Handle async methods
|
|
369
|
-
if (result instanceof Promise) {
|
|
370
|
-
return result.then((resolvedResult) => {
|
|
371
|
-
timedDispatch(resolvedResult);
|
|
372
|
-
return resolvedResult;
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
// Sync method
|
|
376
|
-
timedDispatch(result);
|
|
377
|
-
return result;
|
|
378
|
-
};
|
|
379
|
-
};
|
|
380
|
-
}
|
|
84
|
+
// Render symbols (v3.0.0)
|
|
85
|
+
const RENDER_METHOD = getSymbol('render-method');
|
|
86
|
+
const RENDER_OPTIONS = getSymbol('render-options');
|
|
87
|
+
const RENDER_INSTANCE = getSymbol('render-instance');
|
|
88
|
+
const RENDER_SCHEDULED = getSymbol('render-scheduled');
|
|
89
|
+
const RENDER_TIMERS = getSymbol('render-timers');
|
|
90
|
+
const RENDER_CALLBACKS = getSymbol('render-callbacks');
|
|
91
|
+
const STYLES_METHOD = getSymbol('styles-method');
|
|
92
|
+
const STYLES_APPLIED = getSymbol('styles-applied');
|
|
93
|
+
// Navigation context symbols
|
|
94
|
+
const CONTEXT_HANDLER = getSymbol('context-handler');
|
|
95
|
+
getSymbol('context-method-name');
|
|
96
|
+
const NAVIGATION_CONTEXT_INSTANCE = getSymbol('navigation-context-instance');
|
|
97
|
+
const REGISTERED_ELEMENTS = getSymbol('registered-elements');
|
|
98
|
+
const IS_UPDATING = getSymbol('is-updating');
|
|
99
|
+
const CONTEXT_REGISTER = getSymbol('context-register');
|
|
100
|
+
const CONTEXT_UNREGISTER = getSymbol('context-unregister');
|
|
101
|
+
const CONTEXT_NOTIFY_ELEMENT = getSymbol('context-notify-element');
|
|
102
|
+
getSymbol('context-options');
|
|
103
|
+
const CONTEXT_TIMER = getSymbol('context-timer');
|
|
104
|
+
const CONTEXT_CALLED = getSymbol('context-called');
|
|
381
105
|
|
|
382
106
|
// Global cache for MediaQueryList objects
|
|
383
107
|
const mediaQueryCache = new Map();
|
|
@@ -404,21 +128,22 @@ var Snice = (function (exports) {
|
|
|
404
128
|
}
|
|
405
129
|
return function (target, context) {
|
|
406
130
|
const propertyKey = context.name;
|
|
131
|
+
const initKey = `__observe_init_${propertyKey}`;
|
|
407
132
|
context.addInitializer(function () {
|
|
408
133
|
const constructor = this.constructor;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
134
|
+
if (constructor[initKey])
|
|
135
|
+
return;
|
|
136
|
+
constructor[initKey] = true;
|
|
137
|
+
if (!constructor[OBSERVERS]) {
|
|
138
|
+
constructor[OBSERVERS] = [];
|
|
412
139
|
}
|
|
413
|
-
// Normalize to array
|
|
414
140
|
const observeTargets = Array.isArray(observeTarget) ? observeTarget : [observeTarget];
|
|
415
|
-
// Create an observer entry for each target
|
|
416
141
|
for (const targetString of observeTargets) {
|
|
417
|
-
// Parse the observation type from the observeTarget string
|
|
418
142
|
const [type, ...modifiers] = targetString.split(':');
|
|
419
|
-
|
|
143
|
+
const targetStr = modifiers.join(':');
|
|
144
|
+
constructor[OBSERVERS].push({
|
|
420
145
|
type,
|
|
421
|
-
target:
|
|
146
|
+
target: targetStr,
|
|
422
147
|
selector,
|
|
423
148
|
methodName: propertyKey,
|
|
424
149
|
method: target,
|
|
@@ -430,8 +155,7 @@ var Snice = (function (exports) {
|
|
|
430
155
|
}
|
|
431
156
|
// Helper to setup observers for elements
|
|
432
157
|
function setupObservers(instance, element) {
|
|
433
|
-
|
|
434
|
-
const observers = instance.constructor.prototype[OBSERVERS];
|
|
158
|
+
const observers = instance.constructor[OBSERVERS];
|
|
435
159
|
if (!observers || !Array.isArray(observers) || observers.length === 0) {
|
|
436
160
|
return;
|
|
437
161
|
}
|
|
@@ -859,14 +583,16 @@ var Snice = (function (exports) {
|
|
|
859
583
|
function respond(requestName, options) {
|
|
860
584
|
return function (target, context) {
|
|
861
585
|
const propertyKey = context.name;
|
|
586
|
+
const initKey = `__respond_init_${requestName}_${propertyKey}`;
|
|
862
587
|
context.addInitializer(function () {
|
|
863
588
|
const constructor = this.constructor;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
589
|
+
if (constructor[initKey])
|
|
590
|
+
return;
|
|
591
|
+
constructor[initKey] = true;
|
|
592
|
+
if (!constructor[CHANNEL_HANDLERS]) {
|
|
593
|
+
constructor[CHANNEL_HANDLERS] = [];
|
|
868
594
|
}
|
|
869
|
-
constructor
|
|
595
|
+
constructor[CHANNEL_HANDLERS].push({
|
|
870
596
|
channelName: requestName,
|
|
871
597
|
methodName: propertyKey,
|
|
872
598
|
method: target,
|
|
@@ -877,7 +603,7 @@ var Snice = (function (exports) {
|
|
|
877
603
|
}
|
|
878
604
|
// Helper to setup response handlers for elements and controllers
|
|
879
605
|
function setupResponseHandlers(instance, element) {
|
|
880
|
-
const handlers = instance.constructor
|
|
606
|
+
const handlers = instance.constructor[CHANNEL_HANDLERS];
|
|
881
607
|
if (!handlers)
|
|
882
608
|
return;
|
|
883
609
|
// Store cleanup functions
|
|
@@ -977,71 +703,1263 @@ var Snice = (function (exports) {
|
|
|
977
703
|
}
|
|
978
704
|
}
|
|
979
705
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
706
|
+
/**
|
|
707
|
+
* Template system for Snice v3.0.0
|
|
708
|
+
* Provides html`` and css`` tagged template processors with differential rendering
|
|
709
|
+
*/
|
|
710
|
+
// Unique symbols for type checking
|
|
711
|
+
const HTML_RESULT = Symbol('html-result');
|
|
712
|
+
const CSS_RESULT = Symbol('css-result');
|
|
713
|
+
/**
|
|
714
|
+
* Tagged template function for creating HTML templates
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```typescript
|
|
718
|
+
* html`<div class="card">
|
|
719
|
+
* <h1>${this.title}</h1>
|
|
720
|
+
* <button @click=${this.handleClick}>Click me</button>
|
|
721
|
+
* </div>`
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
function html(strings, ...values) {
|
|
725
|
+
return {
|
|
726
|
+
_$litType$: HTML_RESULT,
|
|
727
|
+
strings,
|
|
728
|
+
values
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Tagged template function for creating CSS
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```typescript
|
|
736
|
+
* css`:host {
|
|
737
|
+
* display: block;
|
|
738
|
+
* padding: 1rem;
|
|
739
|
+
* }`
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
function css(strings, ...values) {
|
|
743
|
+
// Combine strings and values into final CSS text
|
|
744
|
+
let cssText = strings[0];
|
|
745
|
+
for (let i = 0; i < values.length; i++) {
|
|
746
|
+
cssText += String(values[i]) + strings[i + 1];
|
|
1005
747
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
748
|
+
const result = {
|
|
749
|
+
_$litType$: CSS_RESULT,
|
|
750
|
+
cssText
|
|
751
|
+
};
|
|
752
|
+
// Try to create constructable stylesheet for better performance
|
|
753
|
+
// This will be cached and reused across instances
|
|
754
|
+
if (typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in Document.prototype) {
|
|
1010
755
|
try {
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
756
|
+
const sheet = new CSSStyleSheet();
|
|
757
|
+
sheet.replaceSync(cssText);
|
|
758
|
+
result.styleSheet = sheet;
|
|
1014
759
|
}
|
|
1015
|
-
catch (
|
|
1016
|
-
|
|
1017
|
-
throw error;
|
|
760
|
+
catch (e) {
|
|
761
|
+
// Fall back to regular <style> tag if constructable stylesheets fail
|
|
1018
762
|
}
|
|
1019
763
|
}
|
|
764
|
+
return result;
|
|
1020
765
|
}
|
|
1021
766
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
* @param name The name to register the controller under
|
|
767
|
+
* Check if a value is a TemplateResult
|
|
1024
768
|
*/
|
|
1025
|
-
function
|
|
1026
|
-
return
|
|
1027
|
-
snice.controllerRegistry.set(name, constructor);
|
|
1028
|
-
// Mark as controller class for channel decorator detection
|
|
1029
|
-
constructor.prototype[IS_CONTROLLER_CLASS] = true;
|
|
1030
|
-
return constructor;
|
|
1031
|
-
};
|
|
769
|
+
function isTemplateResult(value) {
|
|
770
|
+
return value && value._$litType$ === HTML_RESULT;
|
|
1032
771
|
}
|
|
1033
772
|
/**
|
|
1034
|
-
*
|
|
1035
|
-
* @param element The element to attach the controller to
|
|
1036
|
-
* @param controllerName The name of the controller to attach
|
|
773
|
+
* Check if a value is a CSSResult
|
|
1037
774
|
*/
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
775
|
+
function isCSSResult(value) {
|
|
776
|
+
return value && value._$litType$ === CSS_RESULT;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Nothing - represents no value (different from null/undefined)
|
|
780
|
+
* Used to remove content from templates
|
|
781
|
+
*/
|
|
782
|
+
const nothing = Symbol('nothing');
|
|
783
|
+
// Unique symbol for unsafe HTML
|
|
784
|
+
const UNSAFE_HTML = Symbol('unsafe-html');
|
|
785
|
+
/**
|
|
786
|
+
* Mark a string as raw HTML that should not be escaped
|
|
787
|
+
* WARNING: Only use with sanitized content - using user input can lead to XSS!
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```typescript
|
|
791
|
+
* const htmlString = '<span class="bold">Hello</span>';
|
|
792
|
+
* html`<div>${unsafeHTML(htmlString)}</div>`
|
|
793
|
+
* ```
|
|
794
|
+
*/
|
|
795
|
+
function unsafeHTML(html) {
|
|
796
|
+
return {
|
|
797
|
+
_$litType$: UNSAFE_HTML,
|
|
798
|
+
html
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Check if a value is an UnsafeHTML wrapper
|
|
803
|
+
*/
|
|
804
|
+
function isUnsafeHTML(value) {
|
|
805
|
+
return value && value._$litType$ === UNSAFE_HTML;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Unique marker for dynamic parts
|
|
809
|
+
// This parses as a comment node but doesn't get escaped in attributes
|
|
810
|
+
const marker = `snice$${Math.random().toFixed(9).slice(2)}$`;
|
|
811
|
+
const markerMatch = '?' + marker;
|
|
812
|
+
const nodeMarker = `<${markerMatch}>`;
|
|
813
|
+
const markerRegex = new RegExp(marker, 'g');
|
|
814
|
+
// Template cache - templates with same string array can be reused
|
|
815
|
+
const templateCache = new WeakMap();
|
|
816
|
+
/**
|
|
817
|
+
* A prepared template ready for rendering
|
|
818
|
+
*/
|
|
819
|
+
class Template {
|
|
820
|
+
constructor(result, element, attrNamesForParts) {
|
|
821
|
+
this.parts = [];
|
|
822
|
+
this.element = element;
|
|
823
|
+
const walker = document.createTreeWalker(element.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
824
|
+
let partIndex = 0;
|
|
825
|
+
const nodesToRemove = [];
|
|
826
|
+
let node;
|
|
827
|
+
while ((node = walker.nextNode()) !== null) {
|
|
828
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
829
|
+
const element = node;
|
|
830
|
+
const tagName = element.tagName.toLowerCase();
|
|
831
|
+
// Handle virtual elements: <if>, <case>
|
|
832
|
+
// Keep them in the DOM with display:contents for now
|
|
833
|
+
// Will optimize later with proper template extraction
|
|
834
|
+
if (tagName === 'if') {
|
|
835
|
+
// <if value="${condition}">children</if>
|
|
836
|
+
const valueAttr = element.getAttribute('value');
|
|
837
|
+
if (valueAttr && valueAttr.includes(marker)) {
|
|
838
|
+
// Remove the value attribute
|
|
839
|
+
element.removeAttribute('value');
|
|
840
|
+
this.parts.push({
|
|
841
|
+
type: 'conditional-if',
|
|
842
|
+
index: partIndex++,
|
|
843
|
+
element // Keep the <if> element
|
|
844
|
+
});
|
|
845
|
+
// Continue processing children normally
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
// Handle <case> element
|
|
850
|
+
if (tagName === 'case') {
|
|
851
|
+
// <case value="${value}">children</case>
|
|
852
|
+
const valueAttr = element.getAttribute('value');
|
|
853
|
+
if (valueAttr && valueAttr.includes(marker)) {
|
|
854
|
+
// Remove the value attribute
|
|
855
|
+
element.removeAttribute('value');
|
|
856
|
+
this.parts.push({
|
|
857
|
+
type: 'conditional-case',
|
|
858
|
+
index: partIndex++,
|
|
859
|
+
element // Keep the <case> element
|
|
860
|
+
});
|
|
861
|
+
// Continue processing children normally
|
|
862
|
+
}
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (element.hasAttributes()) {
|
|
866
|
+
const attributes = element.attributes;
|
|
867
|
+
const attrsToRemove = [];
|
|
868
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
869
|
+
const attr = attributes[i];
|
|
870
|
+
const value = attr.value;
|
|
871
|
+
// Check for attribute bindings
|
|
872
|
+
if (value.includes(marker)) {
|
|
873
|
+
attrsToRemove.push(attr);
|
|
874
|
+
// Get original attribute name with preserved case
|
|
875
|
+
const originalName = attrNamesForParts[partIndex] || attr.name;
|
|
876
|
+
// Extract static string segments by splitting on marker
|
|
877
|
+
const attrStrings = value.split(marker);
|
|
878
|
+
if (originalName.startsWith('@')) {
|
|
879
|
+
// Event binding
|
|
880
|
+
this.parts.push({
|
|
881
|
+
type: 'event',
|
|
882
|
+
index: partIndex++,
|
|
883
|
+
name: originalName.slice(1),
|
|
884
|
+
element
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
else if (originalName.startsWith('.')) {
|
|
888
|
+
// Property binding - preserve original case for JavaScript properties
|
|
889
|
+
this.parts.push({
|
|
890
|
+
type: 'property',
|
|
891
|
+
index: partIndex++,
|
|
892
|
+
name: originalName.slice(1),
|
|
893
|
+
element
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
else if (originalName.startsWith('?')) {
|
|
897
|
+
// Boolean attribute
|
|
898
|
+
this.parts.push({
|
|
899
|
+
type: 'boolean-attribute',
|
|
900
|
+
index: partIndex++,
|
|
901
|
+
name: originalName.slice(1),
|
|
902
|
+
element
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
// Regular attribute - use lowercased name from DOM
|
|
907
|
+
// Store static string segments for interpolation
|
|
908
|
+
this.parts.push({
|
|
909
|
+
type: 'attribute',
|
|
910
|
+
index: partIndex++,
|
|
911
|
+
name: attr.name,
|
|
912
|
+
element,
|
|
913
|
+
attrStrings
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Remove marker attributes
|
|
919
|
+
for (const attr of attrsToRemove) {
|
|
920
|
+
element.removeAttribute(attr.name);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
925
|
+
const comment = node;
|
|
926
|
+
// Check for marker match (processing instruction becomes comment)
|
|
927
|
+
if (comment.data === markerMatch) {
|
|
928
|
+
// Node part
|
|
929
|
+
const parent = comment.parentNode;
|
|
930
|
+
const endNode = document.createComment('');
|
|
931
|
+
parent.insertBefore(endNode, comment.nextSibling);
|
|
932
|
+
this.parts.push({
|
|
933
|
+
type: 'node',
|
|
934
|
+
index: partIndex++,
|
|
935
|
+
startNode: comment,
|
|
936
|
+
endNode
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
else if (node.nodeType === Node.TEXT_NODE) {
|
|
941
|
+
const text = node;
|
|
942
|
+
const data = text.data;
|
|
943
|
+
if (data.includes(marker)) {
|
|
944
|
+
// Split text node at markers
|
|
945
|
+
const parent = text.parentNode;
|
|
946
|
+
const parts = data.split(markerRegex);
|
|
947
|
+
const lastIndex = parts.length - 1;
|
|
948
|
+
for (let i = 0; i < lastIndex; i++) {
|
|
949
|
+
parent.insertBefore(document.createTextNode(parts[i]), text);
|
|
950
|
+
const comment = document.createComment('');
|
|
951
|
+
const endNode = document.createComment('');
|
|
952
|
+
parent.insertBefore(comment, text);
|
|
953
|
+
parent.insertBefore(endNode, text);
|
|
954
|
+
this.parts.push({
|
|
955
|
+
type: 'node',
|
|
956
|
+
index: partIndex++,
|
|
957
|
+
startNode: comment,
|
|
958
|
+
endNode
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
// Last part
|
|
962
|
+
if (parts[lastIndex] !== '') {
|
|
963
|
+
text.data = parts[lastIndex];
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
nodesToRemove.push(text);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Remove marker nodes
|
|
972
|
+
for (const node of nodesToRemove) {
|
|
973
|
+
node.parentNode?.removeChild(node);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Prepare a template for rendering
|
|
979
|
+
*/
|
|
980
|
+
function prepareTemplate(result) {
|
|
981
|
+
// Check cache first
|
|
982
|
+
const { strings } = result;
|
|
983
|
+
const cached = templateCache.get(strings);
|
|
984
|
+
if (cached) {
|
|
985
|
+
return cached;
|
|
986
|
+
}
|
|
987
|
+
// Build HTML with markers and extract original attribute names
|
|
988
|
+
const htmlParts = [];
|
|
989
|
+
const attrNamesForParts = [];
|
|
990
|
+
for (let i = 0; i < strings.length; i++) {
|
|
991
|
+
const str = strings[i];
|
|
992
|
+
htmlParts.push(str);
|
|
993
|
+
if (i < strings.length - 1) {
|
|
994
|
+
// Check if we're in an attribute context
|
|
995
|
+
// Look backwards for = sign
|
|
996
|
+
const lastEquals = str.lastIndexOf('=');
|
|
997
|
+
const lastCloseTag = str.lastIndexOf('>');
|
|
998
|
+
if (lastEquals > lastCloseTag) {
|
|
999
|
+
// We're in an attribute value - extract and preserve the original attribute name
|
|
1000
|
+
let attrStart = lastEquals - 1;
|
|
1001
|
+
while (attrStart >= 0 && /\S/.test(str[attrStart])) {
|
|
1002
|
+
attrStart--;
|
|
1003
|
+
}
|
|
1004
|
+
const attrName = str.substring(attrStart + 1, lastEquals).trim();
|
|
1005
|
+
attrNamesForParts.push(attrName);
|
|
1006
|
+
htmlParts.push(marker);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
// Check if this is a meta element (<if> or <case>) by looking backwards
|
|
1010
|
+
// Match pattern: <if or <case followed by whitespace or >
|
|
1011
|
+
const metaElementMatch = str.match(/<(if|case)\s*$/);
|
|
1012
|
+
if (metaElementMatch) {
|
|
1013
|
+
// This is a meta element - add value attribute
|
|
1014
|
+
attrNamesForParts.push('value');
|
|
1015
|
+
htmlParts.push(`value="${marker}"`);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
// We're in node content
|
|
1019
|
+
attrNamesForParts.push(''); // Empty string for node parts
|
|
1020
|
+
htmlParts.push(nodeMarker);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const html = htmlParts.join('');
|
|
1026
|
+
const template = document.createElement('template');
|
|
1027
|
+
template.innerHTML = html;
|
|
1028
|
+
const tmpl = new Template(result, template, attrNamesForParts);
|
|
1029
|
+
// Cache the template for reuse
|
|
1030
|
+
templateCache.set(strings, tmpl);
|
|
1031
|
+
return tmpl;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Instance of a rendered template
|
|
1035
|
+
*/
|
|
1036
|
+
class TemplateInstance {
|
|
1037
|
+
constructor(result) {
|
|
1038
|
+
this.parts = [];
|
|
1039
|
+
this.fragment = null;
|
|
1040
|
+
this.conditionalParts = []; // if/case parts with their indices
|
|
1041
|
+
this.regularParts = []; // all other parts with their indices
|
|
1042
|
+
this.template = prepareTemplate(result);
|
|
1043
|
+
}
|
|
1044
|
+
renderFragment() {
|
|
1045
|
+
if (!this.fragment) {
|
|
1046
|
+
// First render - clone template and create parts
|
|
1047
|
+
this.fragment = this.template.element.content.cloneNode(true);
|
|
1048
|
+
// Build a map of nodes from template to cloned fragment
|
|
1049
|
+
const walker = document.createTreeWalker(this.template.element.content, NodeFilter.SHOW_ALL);
|
|
1050
|
+
const clonedWalker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ALL);
|
|
1051
|
+
const nodeMap = new Map();
|
|
1052
|
+
let templateNode = walker.currentNode;
|
|
1053
|
+
let clonedNode = clonedWalker.currentNode;
|
|
1054
|
+
while (templateNode && clonedNode) {
|
|
1055
|
+
nodeMap.set(templateNode, clonedNode);
|
|
1056
|
+
templateNode = walker.nextNode();
|
|
1057
|
+
clonedNode = clonedWalker.nextNode();
|
|
1058
|
+
}
|
|
1059
|
+
for (let i = 0; i < this.template.parts.length; i++) {
|
|
1060
|
+
const partDef = this.template.parts[i];
|
|
1061
|
+
let part;
|
|
1062
|
+
switch (partDef.type) {
|
|
1063
|
+
case 'node':
|
|
1064
|
+
const startNode = nodeMap.get(partDef.startNode);
|
|
1065
|
+
const endNode = nodeMap.get(partDef.endNode);
|
|
1066
|
+
part = new NodePart(startNode, endNode);
|
|
1067
|
+
break;
|
|
1068
|
+
case 'attribute':
|
|
1069
|
+
const attrElement = nodeMap.get(partDef.element);
|
|
1070
|
+
part = new AttributePart(attrElement, partDef.name, partDef.attrStrings);
|
|
1071
|
+
break;
|
|
1072
|
+
case 'property':
|
|
1073
|
+
const propElement = nodeMap.get(partDef.element);
|
|
1074
|
+
part = new PropertyPart(propElement, partDef.name);
|
|
1075
|
+
break;
|
|
1076
|
+
case 'boolean-attribute':
|
|
1077
|
+
const boolElement = nodeMap.get(partDef.element);
|
|
1078
|
+
part = new BooleanAttributePart(boolElement, partDef.name);
|
|
1079
|
+
break;
|
|
1080
|
+
case 'event':
|
|
1081
|
+
const eventElement = nodeMap.get(partDef.element);
|
|
1082
|
+
part = new EventPart(eventElement, partDef.name);
|
|
1083
|
+
break;
|
|
1084
|
+
case 'conditional-if':
|
|
1085
|
+
const conditionalIfElement = nodeMap.get(partDef.element);
|
|
1086
|
+
part = new ConditionalIfPart(conditionalIfElement);
|
|
1087
|
+
break;
|
|
1088
|
+
case 'conditional-case':
|
|
1089
|
+
const conditionalCaseElement = nodeMap.get(partDef.element);
|
|
1090
|
+
part = new ConditionalCasePart(conditionalCaseElement);
|
|
1091
|
+
break;
|
|
1092
|
+
default:
|
|
1093
|
+
throw new Error(`Unknown part type: ${partDef.type}`);
|
|
1094
|
+
}
|
|
1095
|
+
this.parts.push(part);
|
|
1096
|
+
// Separate conditional parts from regular parts for optimized update
|
|
1097
|
+
if (part instanceof ConditionalIfPart || part instanceof ConditionalCasePart) {
|
|
1098
|
+
this.conditionalParts.push({ part, index: i });
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
this.regularParts.push({ part, index: i });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return this.fragment;
|
|
1106
|
+
}
|
|
1107
|
+
render(values) {
|
|
1108
|
+
const fragment = this.renderFragment();
|
|
1109
|
+
// Commit values to parts
|
|
1110
|
+
this.update(values);
|
|
1111
|
+
return fragment;
|
|
1112
|
+
}
|
|
1113
|
+
update(values) {
|
|
1114
|
+
// Optimized: Process conditional parts first (if any), then regular parts
|
|
1115
|
+
// Using pre-separated arrays with cached indices avoids instanceof and indexOf calls
|
|
1116
|
+
// Process conditional parts first (they control visibility)
|
|
1117
|
+
for (const { part, index } of this.conditionalParts) {
|
|
1118
|
+
part.commit(values[index]);
|
|
1119
|
+
}
|
|
1120
|
+
// Then process regular parts
|
|
1121
|
+
for (const { part, index } of this.regularParts) {
|
|
1122
|
+
part.commit(values[index]);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
clear() {
|
|
1126
|
+
for (const part of this.parts) {
|
|
1127
|
+
part.clear();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Base class for all parts
|
|
1133
|
+
*/
|
|
1134
|
+
class Part {
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* NodePart handles text content and nested templates
|
|
1138
|
+
*/
|
|
1139
|
+
class NodePart extends Part {
|
|
1140
|
+
constructor(startNode, endNode) {
|
|
1141
|
+
super();
|
|
1142
|
+
this.value = undefined;
|
|
1143
|
+
this.startNode = startNode;
|
|
1144
|
+
this.endNode = endNode;
|
|
1145
|
+
}
|
|
1146
|
+
commit(value) {
|
|
1147
|
+
if (value === this.value)
|
|
1148
|
+
return;
|
|
1149
|
+
this.value = value;
|
|
1150
|
+
// Handle arrays
|
|
1151
|
+
if (Array.isArray(value)) {
|
|
1152
|
+
this.commitArray(value);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
// Handle nested templates
|
|
1156
|
+
if (isTemplateResult(value)) {
|
|
1157
|
+
this.commitTemplate(value);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
// Handle unsafe HTML
|
|
1161
|
+
if (isUnsafeHTML(value)) {
|
|
1162
|
+
this.commitUnsafeHTML(value);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
// Handle primitives
|
|
1166
|
+
this.commitPrimitive(value);
|
|
1167
|
+
}
|
|
1168
|
+
commitPrimitive(value) {
|
|
1169
|
+
this.clear();
|
|
1170
|
+
const text = value === null || value === undefined ? '' : String(value);
|
|
1171
|
+
const textNode = document.createTextNode(text);
|
|
1172
|
+
this.insertBefore(textNode);
|
|
1173
|
+
}
|
|
1174
|
+
commitTemplate(template) {
|
|
1175
|
+
this.clear();
|
|
1176
|
+
const instance = new TemplateInstance(template);
|
|
1177
|
+
const fragment = instance.render(template.values);
|
|
1178
|
+
this.insertBefore(fragment);
|
|
1179
|
+
}
|
|
1180
|
+
commitArray(values) {
|
|
1181
|
+
this.clear();
|
|
1182
|
+
// Template caching (via prepareTemplate) still provides significant performance benefit
|
|
1183
|
+
for (const value of values) {
|
|
1184
|
+
if (isTemplateResult(value)) {
|
|
1185
|
+
const instance = new TemplateInstance(value);
|
|
1186
|
+
const fragment = instance.render(value.values);
|
|
1187
|
+
this.insertBefore(fragment);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
const text = value === null || value === undefined ? '' : String(value);
|
|
1191
|
+
const textNode = document.createTextNode(text);
|
|
1192
|
+
this.insertBefore(textNode);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
commitUnsafeHTML(value) {
|
|
1197
|
+
this.clear();
|
|
1198
|
+
// Create a temporary container to parse the HTML
|
|
1199
|
+
const temp = document.createElement('template');
|
|
1200
|
+
temp.innerHTML = value.html;
|
|
1201
|
+
// Insert all parsed nodes
|
|
1202
|
+
const fragment = temp.content;
|
|
1203
|
+
this.insertBefore(fragment);
|
|
1204
|
+
}
|
|
1205
|
+
insertBefore(node) {
|
|
1206
|
+
this.endNode.parentNode?.insertBefore(node, this.endNode);
|
|
1207
|
+
}
|
|
1208
|
+
clear() {
|
|
1209
|
+
const parent = this.startNode.parentNode;
|
|
1210
|
+
if (!parent)
|
|
1211
|
+
return;
|
|
1212
|
+
let node = this.startNode.nextSibling;
|
|
1213
|
+
while (node && node !== this.endNode) {
|
|
1214
|
+
const next = node.nextSibling;
|
|
1215
|
+
parent.removeChild(node);
|
|
1216
|
+
node = next;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* AttributePart handles regular attribute updates
|
|
1222
|
+
*/
|
|
1223
|
+
class AttributePart extends Part {
|
|
1224
|
+
constructor(element, name, attrStrings) {
|
|
1225
|
+
super();
|
|
1226
|
+
this.value = undefined;
|
|
1227
|
+
this.element = element;
|
|
1228
|
+
this.name = name;
|
|
1229
|
+
this.attrStrings = attrStrings;
|
|
1230
|
+
}
|
|
1231
|
+
commit(value) {
|
|
1232
|
+
if (value === this.value)
|
|
1233
|
+
return;
|
|
1234
|
+
this.value = value;
|
|
1235
|
+
if (value === null || value === undefined) {
|
|
1236
|
+
this.element.removeAttribute(this.name);
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
// Inline attribute value computation for performance
|
|
1240
|
+
let finalValue;
|
|
1241
|
+
if (!this.attrStrings || this.attrStrings.length === 0) {
|
|
1242
|
+
finalValue = String(value);
|
|
1243
|
+
}
|
|
1244
|
+
else if (this.attrStrings.length === 1) {
|
|
1245
|
+
finalValue = this.attrStrings[0];
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
// Multiple segments: "prefix" + value + "suffix"
|
|
1249
|
+
finalValue = this.attrStrings[0] + String(value) + this.attrStrings[1];
|
|
1250
|
+
}
|
|
1251
|
+
this.element.setAttribute(this.name, finalValue);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
clear() {
|
|
1255
|
+
this.element.removeAttribute(this.name);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* PropertyPart handles property bindings
|
|
1260
|
+
*/
|
|
1261
|
+
class PropertyPart extends Part {
|
|
1262
|
+
constructor(element, name) {
|
|
1263
|
+
super();
|
|
1264
|
+
this.value = undefined;
|
|
1265
|
+
this.element = element;
|
|
1266
|
+
this.name = name;
|
|
1267
|
+
}
|
|
1268
|
+
commit(value) {
|
|
1269
|
+
if (value === this.value)
|
|
1270
|
+
return;
|
|
1271
|
+
this.value = value;
|
|
1272
|
+
this.element[this.name] = value;
|
|
1273
|
+
}
|
|
1274
|
+
clear() {
|
|
1275
|
+
this.element[this.name] = undefined;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* BooleanAttributePart handles boolean attributes
|
|
1280
|
+
*/
|
|
1281
|
+
class BooleanAttributePart extends Part {
|
|
1282
|
+
constructor(element, name) {
|
|
1283
|
+
super();
|
|
1284
|
+
this.value = undefined;
|
|
1285
|
+
this.element = element;
|
|
1286
|
+
this.name = name;
|
|
1287
|
+
}
|
|
1288
|
+
commit(value) {
|
|
1289
|
+
if (value === this.value)
|
|
1290
|
+
return;
|
|
1291
|
+
this.value = value;
|
|
1292
|
+
if (value) {
|
|
1293
|
+
this.element.setAttribute(this.name, '');
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
this.element.removeAttribute(this.name);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
clear() {
|
|
1300
|
+
this.element.removeAttribute(this.name);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* EventPart handles event listener bindings with keyboard shortcut support
|
|
1305
|
+
*/
|
|
1306
|
+
class EventPart extends Part {
|
|
1307
|
+
constructor(element, eventName) {
|
|
1308
|
+
super();
|
|
1309
|
+
this.listener = null;
|
|
1310
|
+
this.value = undefined;
|
|
1311
|
+
this.keyFilter = null;
|
|
1312
|
+
this.host = null; // Cache host element
|
|
1313
|
+
this.element = element;
|
|
1314
|
+
// Parse keyboard shortcuts:
|
|
1315
|
+
// Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter) to match @on decorator
|
|
1316
|
+
const dotIndex = eventName.indexOf('.');
|
|
1317
|
+
const colonIndex = eventName.indexOf(':');
|
|
1318
|
+
// Use whichever delimiter comes first (dot or colon)
|
|
1319
|
+
const delimiterIndex = dotIndex > 0 && colonIndex > 0
|
|
1320
|
+
? Math.min(dotIndex, colonIndex)
|
|
1321
|
+
: Math.max(dotIndex, colonIndex);
|
|
1322
|
+
if (delimiterIndex > 0) {
|
|
1323
|
+
const baseEvent = eventName.substring(0, delimiterIndex);
|
|
1324
|
+
const keySpec = eventName.substring(delimiterIndex + 1);
|
|
1325
|
+
this.eventName = baseEvent;
|
|
1326
|
+
this.keyFilter = parseKeyboardFilter(keySpec);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
this.eventName = eventName;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
commit(value) {
|
|
1333
|
+
// Skip if same value (but null/undefined always triggers update)
|
|
1334
|
+
if (value === this.value && value !== null && value !== undefined)
|
|
1335
|
+
return;
|
|
1336
|
+
// Remove old listener
|
|
1337
|
+
if (this.listener) {
|
|
1338
|
+
this.element.removeEventListener(this.eventName, this.listener);
|
|
1339
|
+
this.listener = null;
|
|
1340
|
+
}
|
|
1341
|
+
this.value = value;
|
|
1342
|
+
// Add new listener
|
|
1343
|
+
if (value === null || value === undefined) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof value === 'function') {
|
|
1347
|
+
// Auto-bind to host element (the custom element with shadow root)
|
|
1348
|
+
// Cache host lookup for performance
|
|
1349
|
+
if (!this.host) {
|
|
1350
|
+
const rootNode = this.element.getRootNode();
|
|
1351
|
+
this.host = rootNode.host || null;
|
|
1352
|
+
}
|
|
1353
|
+
// Create a wrapper that calls the handler with the host as context
|
|
1354
|
+
if (this.host) {
|
|
1355
|
+
const host = this.host; // Capture for closure
|
|
1356
|
+
this.listener = ((event) => {
|
|
1357
|
+
if (this.keyFilter && !matchesKeyboardFilter(event, this.keyFilter)) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
value.call(host, event);
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
this.listener = ((event) => {
|
|
1365
|
+
if (this.keyFilter && !matchesKeyboardFilter(event, this.keyFilter)) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
value.call(null, event);
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
this.element.addEventListener(this.eventName, this.listener);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
clear() {
|
|
1375
|
+
if (this.listener) {
|
|
1376
|
+
this.element.removeEventListener(this.eventName, this.listener);
|
|
1377
|
+
this.listener = null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Parse keyboard shortcut specification
|
|
1383
|
+
* Examples:
|
|
1384
|
+
* "enter" -> { key: "Enter" }
|
|
1385
|
+
* "ctrl+s" -> { key: "s", ctrl: true }
|
|
1386
|
+
* "ctrl+shift+s" -> { key: "s", ctrl: true, shift: true }
|
|
1387
|
+
* "~enter" -> { key: "Enter", anyModifiers: true }
|
|
1388
|
+
*/
|
|
1389
|
+
function parseKeyboardFilter(spec) {
|
|
1390
|
+
// Handle ~ prefix for matching regardless of modifiers
|
|
1391
|
+
const anyModifiers = spec.startsWith('~');
|
|
1392
|
+
if (anyModifiers) {
|
|
1393
|
+
spec = spec.substring(1);
|
|
1394
|
+
}
|
|
1395
|
+
const parts = spec.split('+');
|
|
1396
|
+
const filter = {
|
|
1397
|
+
key: '',
|
|
1398
|
+
anyModifiers
|
|
1399
|
+
};
|
|
1400
|
+
for (const part of parts) {
|
|
1401
|
+
const lower = part.toLowerCase();
|
|
1402
|
+
if (lower === 'ctrl' || lower === 'control') {
|
|
1403
|
+
filter.ctrl = true;
|
|
1404
|
+
}
|
|
1405
|
+
else if (lower === 'alt') {
|
|
1406
|
+
filter.alt = true;
|
|
1407
|
+
}
|
|
1408
|
+
else if (lower === 'shift') {
|
|
1409
|
+
filter.shift = true;
|
|
1410
|
+
}
|
|
1411
|
+
else if (lower === 'meta' || lower === 'cmd' || lower === 'command') {
|
|
1412
|
+
filter.meta = true;
|
|
1413
|
+
}
|
|
1414
|
+
else {
|
|
1415
|
+
// This is the key itself - normalize common keys
|
|
1416
|
+
filter.key = normalizeKey(part);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return filter;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Normalize key names to match KeyboardEvent.key
|
|
1423
|
+
*/
|
|
1424
|
+
function normalizeKey(key) {
|
|
1425
|
+
const keyMap = {
|
|
1426
|
+
'esc': 'Escape',
|
|
1427
|
+
'escape': 'Escape',
|
|
1428
|
+
'enter': 'Enter',
|
|
1429
|
+
'return': 'Enter',
|
|
1430
|
+
'space': ' ',
|
|
1431
|
+
'spacebar': ' ',
|
|
1432
|
+
'up': 'ArrowUp',
|
|
1433
|
+
'down': 'ArrowDown',
|
|
1434
|
+
'left': 'ArrowLeft',
|
|
1435
|
+
'right': 'ArrowRight',
|
|
1436
|
+
'arrowup': 'ArrowUp',
|
|
1437
|
+
'arrowdown': 'ArrowDown',
|
|
1438
|
+
'arrowleft': 'ArrowLeft',
|
|
1439
|
+
'arrowright': 'ArrowRight',
|
|
1440
|
+
'delete': 'Delete',
|
|
1441
|
+
'del': 'Delete',
|
|
1442
|
+
'backspace': 'Backspace',
|
|
1443
|
+
'tab': 'Tab',
|
|
1444
|
+
'home': 'Home',
|
|
1445
|
+
'end': 'End',
|
|
1446
|
+
'pageup': 'PageUp',
|
|
1447
|
+
'pagedown': 'PageDown'
|
|
1448
|
+
};
|
|
1449
|
+
const lower = key.toLowerCase();
|
|
1450
|
+
return keyMap[lower] || key;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Check if keyboard event matches the filter
|
|
1454
|
+
*/
|
|
1455
|
+
function matchesKeyboardFilter(event, filter) {
|
|
1456
|
+
// Check key match
|
|
1457
|
+
if (event.key !== filter.key) {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
// If anyModifiers is true, we don't care about modifiers
|
|
1461
|
+
if (filter.anyModifiers) {
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
// Check modifiers - by default, exact match
|
|
1465
|
+
// If filter specifies ctrl: true, event must have ctrlKey
|
|
1466
|
+
// If filter doesn't specify ctrl, event must NOT have ctrlKey
|
|
1467
|
+
const ctrlMatch = filter.ctrl ? event.ctrlKey : !event.ctrlKey;
|
|
1468
|
+
const altMatch = filter.alt ? event.altKey : !event.altKey;
|
|
1469
|
+
const shiftMatch = filter.shift ? event.shiftKey : !event.shiftKey;
|
|
1470
|
+
const metaMatch = filter.meta ? event.metaKey : !event.metaKey;
|
|
1471
|
+
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* ConditionalIfPart handles <if> conditional rendering
|
|
1475
|
+
* Removes/inserts DOM nodes based on condition
|
|
1476
|
+
*/
|
|
1477
|
+
class ConditionalIfPart extends Part {
|
|
1478
|
+
constructor(ifElement) {
|
|
1479
|
+
super();
|
|
1480
|
+
this.value = undefined;
|
|
1481
|
+
this.fragment = null;
|
|
1482
|
+
this.ifElement = ifElement;
|
|
1483
|
+
this.ifElement.style.display = 'contents';
|
|
1484
|
+
}
|
|
1485
|
+
commit(value) {
|
|
1486
|
+
const condition = Boolean(value);
|
|
1487
|
+
if (this.value === value)
|
|
1488
|
+
return;
|
|
1489
|
+
this.value = value;
|
|
1490
|
+
if (condition) {
|
|
1491
|
+
// Show: restore children from fragment
|
|
1492
|
+
if (this.fragment && this.fragment.hasChildNodes()) {
|
|
1493
|
+
this.ifElement.appendChild(this.fragment);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
// Hide: move children to fragment
|
|
1498
|
+
if (!this.fragment) {
|
|
1499
|
+
this.fragment = document.createDocumentFragment();
|
|
1500
|
+
}
|
|
1501
|
+
while (this.ifElement.firstChild) {
|
|
1502
|
+
this.fragment.appendChild(this.ifElement.firstChild);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
clear() {
|
|
1507
|
+
if (!this.fragment) {
|
|
1508
|
+
this.fragment = document.createDocumentFragment();
|
|
1509
|
+
}
|
|
1510
|
+
while (this.ifElement.firstChild) {
|
|
1511
|
+
this.fragment.appendChild(this.ifElement.firstChild);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* ConditionalCasePart handles <case>/<when>/<default> conditional rendering
|
|
1517
|
+
* Removes/inserts matching branch based on value
|
|
1518
|
+
*/
|
|
1519
|
+
class ConditionalCasePart extends Part {
|
|
1520
|
+
constructor(caseElement) {
|
|
1521
|
+
super();
|
|
1522
|
+
this.value = undefined;
|
|
1523
|
+
this.childrenMap = new Map();
|
|
1524
|
+
this.fragments = new Map();
|
|
1525
|
+
this.defaultChild = null;
|
|
1526
|
+
this.currentChild = null;
|
|
1527
|
+
this.caseElement = caseElement;
|
|
1528
|
+
// Build map and store children in fragments initially
|
|
1529
|
+
for (const child of Array.from(this.caseElement.children)) {
|
|
1530
|
+
const childTag = child.tagName.toLowerCase();
|
|
1531
|
+
if (childTag === 'when') {
|
|
1532
|
+
const whenValue = child.getAttribute('value') || '';
|
|
1533
|
+
this.childrenMap.set(whenValue, child);
|
|
1534
|
+
const fragment = document.createDocumentFragment();
|
|
1535
|
+
fragment.appendChild(child);
|
|
1536
|
+
this.fragments.set(child, fragment);
|
|
1537
|
+
}
|
|
1538
|
+
else if (childTag === 'default') {
|
|
1539
|
+
this.defaultChild = child;
|
|
1540
|
+
const fragment = document.createDocumentFragment();
|
|
1541
|
+
fragment.appendChild(child);
|
|
1542
|
+
this.fragments.set(child, fragment);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
commit(value) {
|
|
1547
|
+
if (this.value === value)
|
|
1548
|
+
return;
|
|
1549
|
+
this.value = value;
|
|
1550
|
+
const valueStr = String(value);
|
|
1551
|
+
// Remove current child
|
|
1552
|
+
if (this.currentChild) {
|
|
1553
|
+
const fragment = this.fragments.get(this.currentChild);
|
|
1554
|
+
if (fragment && !fragment.hasChildNodes()) {
|
|
1555
|
+
fragment.appendChild(this.currentChild);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// Insert matching child
|
|
1559
|
+
const matchingChild = this.childrenMap.get(valueStr) || this.defaultChild;
|
|
1560
|
+
if (matchingChild) {
|
|
1561
|
+
const fragment = this.fragments.get(matchingChild);
|
|
1562
|
+
if (fragment && fragment.hasChildNodes()) {
|
|
1563
|
+
this.caseElement.appendChild(fragment);
|
|
1564
|
+
}
|
|
1565
|
+
this.currentChild = matchingChild;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
clear() {
|
|
1569
|
+
if (this.currentChild) {
|
|
1570
|
+
const fragment = this.fragments.get(this.currentChild);
|
|
1571
|
+
if (fragment && !fragment.hasChildNodes()) {
|
|
1572
|
+
fragment.appendChild(this.currentChild);
|
|
1573
|
+
}
|
|
1574
|
+
this.currentChild = null;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* @on decorator for listening to events
|
|
1581
|
+
* Use in elements or controllers to listen to DOM events or custom events
|
|
1582
|
+
*/
|
|
1583
|
+
const ON_HANDLERS = getSymbol('on-handlers');
|
|
1584
|
+
/**
|
|
1585
|
+
* @on decorator for listening to events
|
|
1586
|
+
*
|
|
1587
|
+
* Works in both elements and controllers with full event delegation support.
|
|
1588
|
+
*
|
|
1589
|
+
* @param eventName - Event name(s) to listen for
|
|
1590
|
+
* @param selector - Optional CSS selector for event delegation
|
|
1591
|
+
* @param options - Event listener options including debounce/throttle
|
|
1592
|
+
*
|
|
1593
|
+
* @example
|
|
1594
|
+
* ```typescript
|
|
1595
|
+
* // In elements
|
|
1596
|
+
* @element('my-button')
|
|
1597
|
+
* class MyButton extends HTMLElement {
|
|
1598
|
+
* @on('click', 'button')
|
|
1599
|
+
* handleClick(e: MouseEvent) {
|
|
1600
|
+
* console.log('Button clicked!', e);
|
|
1601
|
+
* }
|
|
1602
|
+
*
|
|
1603
|
+
* @on('input', 'input', { debounce: 300 })
|
|
1604
|
+
* handleInput(e: Event) {
|
|
1605
|
+
* console.log('Input changed:', (e.target as HTMLInputElement).value);
|
|
1606
|
+
* }
|
|
1607
|
+
* }
|
|
1608
|
+
*
|
|
1609
|
+
* // In controllers
|
|
1610
|
+
* @controller('my-controller')
|
|
1611
|
+
* class MyController {
|
|
1612
|
+
* element!: HTMLElement;
|
|
1613
|
+
*
|
|
1614
|
+
* @on('count-changed')
|
|
1615
|
+
* handleCountChanged(e: CustomEvent) {
|
|
1616
|
+
* console.log('Count changed to:', e.detail.count);
|
|
1617
|
+
* }
|
|
1618
|
+
*
|
|
1619
|
+
* @on('click', '.item', { throttle: 100 })
|
|
1620
|
+
* handleItemClick(e: MouseEvent) {
|
|
1621
|
+
* console.log('Item clicked');
|
|
1622
|
+
* }
|
|
1623
|
+
* }
|
|
1624
|
+
* ```
|
|
1625
|
+
*/
|
|
1626
|
+
function on(eventName, selectorOrOptions, options) {
|
|
1627
|
+
// Parse arguments to support multiple call signatures
|
|
1628
|
+
let selector = null;
|
|
1629
|
+
let opts = {};
|
|
1630
|
+
if (typeof selectorOrOptions === 'string') {
|
|
1631
|
+
// With selector: (eventName, selector, options)
|
|
1632
|
+
selector = selectorOrOptions;
|
|
1633
|
+
opts = options || {};
|
|
1634
|
+
}
|
|
1635
|
+
else if (selectorOrOptions === null && options) {
|
|
1636
|
+
// With null selector: (eventName, null, options)
|
|
1637
|
+
opts = options;
|
|
1638
|
+
}
|
|
1639
|
+
else if (selectorOrOptions && typeof selectorOrOptions === 'object') {
|
|
1640
|
+
// Without selector: (eventName, options)
|
|
1641
|
+
opts = selectorOrOptions;
|
|
1642
|
+
}
|
|
1643
|
+
return function (originalMethod, context) {
|
|
1644
|
+
const methodName = context.name;
|
|
1645
|
+
const initKey = `__on_init_${methodName}_${selector || ''}_${JSON.stringify(eventName)}`;
|
|
1646
|
+
context.addInitializer(function () {
|
|
1647
|
+
const constructor = this.constructor;
|
|
1648
|
+
// Only initialize once per class, not per instance
|
|
1649
|
+
if (constructor[initKey])
|
|
1650
|
+
return;
|
|
1651
|
+
constructor[initKey] = true;
|
|
1652
|
+
if (!constructor[ON_HANDLERS]) {
|
|
1653
|
+
constructor[ON_HANDLERS] = [];
|
|
1654
|
+
}
|
|
1655
|
+
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
1656
|
+
for (const event of eventNames) {
|
|
1657
|
+
constructor[ON_HANDLERS].push({
|
|
1658
|
+
eventName: event,
|
|
1659
|
+
selector,
|
|
1660
|
+
methodName,
|
|
1661
|
+
method: originalMethod,
|
|
1662
|
+
options: opts
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
return originalMethod;
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Create a debounced version of a function
|
|
1671
|
+
*/
|
|
1672
|
+
function debounce$1(fn, delay) {
|
|
1673
|
+
let timeoutId = null;
|
|
1674
|
+
return function (...args) {
|
|
1675
|
+
if (timeoutId !== null) {
|
|
1676
|
+
clearTimeout(timeoutId);
|
|
1677
|
+
}
|
|
1678
|
+
timeoutId = setTimeout(() => {
|
|
1679
|
+
fn.apply(this, args);
|
|
1680
|
+
timeoutId = null;
|
|
1681
|
+
}, delay);
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Create a throttled version of a function (leading edge only)
|
|
1686
|
+
*/
|
|
1687
|
+
function throttle$1(fn, delay) {
|
|
1688
|
+
let lastCall = 0;
|
|
1689
|
+
return function (...args) {
|
|
1690
|
+
const now = Date.now();
|
|
1691
|
+
const timeSinceLastCall = now - lastCall;
|
|
1692
|
+
if (timeSinceLastCall >= delay) {
|
|
1693
|
+
lastCall = now;
|
|
1694
|
+
fn.apply(this, args);
|
|
1695
|
+
}
|
|
1696
|
+
// Events within the throttle window are simply ignored (leading edge only)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Events that don't bubble - these require capture phase for delegation
|
|
1701
|
+
*/
|
|
1702
|
+
const NON_BUBBLING_EVENTS = new Set([
|
|
1703
|
+
'scroll',
|
|
1704
|
+
'focus',
|
|
1705
|
+
'blur',
|
|
1706
|
+
'load',
|
|
1707
|
+
'unload',
|
|
1708
|
+
'error',
|
|
1709
|
+
'resize',
|
|
1710
|
+
'abort',
|
|
1711
|
+
'mouseenter',
|
|
1712
|
+
'mouseleave',
|
|
1713
|
+
'pointerenter',
|
|
1714
|
+
'pointerleave',
|
|
1715
|
+
]);
|
|
1716
|
+
/**
|
|
1717
|
+
* Setup event listeners for an element or controller instance
|
|
1718
|
+
* Called automatically during element connection or controller attachment
|
|
1719
|
+
*/
|
|
1720
|
+
function setupEventHandlers(instance, targetElement) {
|
|
1721
|
+
const handlers = instance.constructor[ON_HANDLERS];
|
|
1722
|
+
if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
// Initialize cleanup object if needed
|
|
1726
|
+
if (!instance[CLEANUP]) {
|
|
1727
|
+
instance[CLEANUP] = { events: [], channels: [], observers: [] };
|
|
1728
|
+
}
|
|
1729
|
+
else if (!instance[CLEANUP].events) {
|
|
1730
|
+
instance[CLEANUP].events = [];
|
|
1731
|
+
}
|
|
1732
|
+
else if (instance[CLEANUP].events.length > 0) {
|
|
1733
|
+
// Events already set up - clean them up first to avoid duplicates
|
|
1734
|
+
cleanupEventHandlers(instance);
|
|
1735
|
+
}
|
|
1736
|
+
for (const handler of handlers) {
|
|
1737
|
+
// Get current method from instance (preserves decorator stacking)
|
|
1738
|
+
const currentMethod = instance[handler.methodName];
|
|
1739
|
+
let boundMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
|
|
1740
|
+
const handlerOptions = handler.options || {};
|
|
1741
|
+
// Parse event name for key modifiers
|
|
1742
|
+
// Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
|
|
1743
|
+
const dotIndex = handler.eventName.indexOf('.');
|
|
1744
|
+
const colonIndex = handler.eventName.indexOf(':');
|
|
1745
|
+
const delimiterIndex = dotIndex > 0 && colonIndex > 0
|
|
1746
|
+
? Math.min(dotIndex, colonIndex)
|
|
1747
|
+
: Math.max(dotIndex, colonIndex);
|
|
1748
|
+
const baseEventName = delimiterIndex > 0
|
|
1749
|
+
? handler.eventName.substring(0, delimiterIndex)
|
|
1750
|
+
: handler.eventName;
|
|
1751
|
+
const keyModifier = delimiterIndex > 0
|
|
1752
|
+
? handler.eventName.substring(delimiterIndex + 1)
|
|
1753
|
+
: null;
|
|
1754
|
+
// Apply debounce if specified
|
|
1755
|
+
if (handlerOptions.debounce && handlerOptions.debounce > 0) {
|
|
1756
|
+
boundMethod = debounce$1(boundMethod, handlerOptions.debounce);
|
|
1757
|
+
}
|
|
1758
|
+
// Apply throttle if specified (debounce takes precedence)
|
|
1759
|
+
else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
|
|
1760
|
+
boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
|
|
1761
|
+
}
|
|
1762
|
+
// Create event handler with key modifier support
|
|
1763
|
+
// Uses shared keyboard filter implementation from parts.ts
|
|
1764
|
+
let keyFilter = null;
|
|
1765
|
+
if (keyModifier && ['keydown', 'keyup', 'keypress'].includes(baseEventName)) {
|
|
1766
|
+
keyFilter = parseKeyboardFilter(keyModifier);
|
|
1767
|
+
}
|
|
1768
|
+
const createKeyModifierHandler = (method) => {
|
|
1769
|
+
if (!keyFilter) {
|
|
1770
|
+
return method;
|
|
1771
|
+
}
|
|
1772
|
+
return (event) => {
|
|
1773
|
+
const keyEvent = event;
|
|
1774
|
+
if (matchesKeyboardFilter(keyEvent, keyFilter)) {
|
|
1775
|
+
method(event);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
};
|
|
1779
|
+
// Apply key modifier wrapper
|
|
1780
|
+
const keyModifierMethod = createKeyModifierHandler(boundMethod);
|
|
1781
|
+
// Main event handler with error handling and event delegation
|
|
1782
|
+
if (handler.selector) {
|
|
1783
|
+
// Delegated event handling - use shadow root if available
|
|
1784
|
+
const eventRoot = targetElement.shadowRoot || targetElement;
|
|
1785
|
+
const delegatedHandler = (event) => {
|
|
1786
|
+
const target = event.target;
|
|
1787
|
+
let matchingElement = null;
|
|
1788
|
+
// Check if target itself matches the selector
|
|
1789
|
+
if (target.matches && target.matches(handler.selector)) {
|
|
1790
|
+
matchingElement = target;
|
|
1791
|
+
}
|
|
1792
|
+
// Check if any parent matches the selector (event delegation)
|
|
1793
|
+
else if (target.closest) {
|
|
1794
|
+
matchingElement = target.closest(handler.selector);
|
|
1795
|
+
}
|
|
1796
|
+
// Only handle if we found a match
|
|
1797
|
+
// Note: No need to check contains() since the event bubbled to eventRoot
|
|
1798
|
+
if (matchingElement) {
|
|
1799
|
+
// Apply automatic preventDefault/stopPropagation
|
|
1800
|
+
if (handlerOptions.preventDefault) {
|
|
1801
|
+
event.preventDefault();
|
|
1802
|
+
}
|
|
1803
|
+
if (handlerOptions.stopPropagation) {
|
|
1804
|
+
event.stopPropagation();
|
|
1805
|
+
event.stopImmediatePropagation();
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
keyModifierMethod(event);
|
|
1809
|
+
}
|
|
1810
|
+
catch (error) {
|
|
1811
|
+
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
// Auto-enable capture for non-bubbling events when using delegation
|
|
1816
|
+
const needsCapture = NON_BUBBLING_EVENTS.has(baseEventName);
|
|
1817
|
+
const useCapture = handlerOptions.capture !== undefined
|
|
1818
|
+
? handlerOptions.capture
|
|
1819
|
+
: needsCapture;
|
|
1820
|
+
const listenerOptions = {
|
|
1821
|
+
capture: useCapture,
|
|
1822
|
+
once: handlerOptions.once || false,
|
|
1823
|
+
passive: handlerOptions.passive || false,
|
|
1824
|
+
};
|
|
1825
|
+
eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
1826
|
+
instance[CLEANUP].events.push({
|
|
1827
|
+
target: eventRoot,
|
|
1828
|
+
eventName: baseEventName,
|
|
1829
|
+
handler: delegatedHandler,
|
|
1830
|
+
options: listenerOptions,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
// Direct event handling - on the element itself
|
|
1835
|
+
// If element has shadow root, listen on both shadow root AND host element
|
|
1836
|
+
// to catch events from inside shadow DOM (with correct target) and on host itself
|
|
1837
|
+
const shadowRoot = targetElement.shadowRoot;
|
|
1838
|
+
const handledSymbol = Symbol('snice-event-handled');
|
|
1839
|
+
const wrappedMethod = (event) => {
|
|
1840
|
+
// Prevent double-triggering when listening on both shadow root and host
|
|
1841
|
+
if (event[handledSymbol]) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
event[handledSymbol] = true;
|
|
1845
|
+
try {
|
|
1846
|
+
// Apply automatic preventDefault/stopPropagation
|
|
1847
|
+
if (handlerOptions.preventDefault) {
|
|
1848
|
+
event.preventDefault();
|
|
1849
|
+
}
|
|
1850
|
+
if (handlerOptions.stopPropagation) {
|
|
1851
|
+
event.stopPropagation();
|
|
1852
|
+
}
|
|
1853
|
+
keyModifierMethod(event);
|
|
1854
|
+
}
|
|
1855
|
+
catch (error) {
|
|
1856
|
+
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
const listenerOptions = {
|
|
1860
|
+
capture: handlerOptions.capture || false,
|
|
1861
|
+
once: handlerOptions.once || false,
|
|
1862
|
+
passive: handlerOptions.passive || false,
|
|
1863
|
+
};
|
|
1864
|
+
if (shadowRoot) {
|
|
1865
|
+
// Listen on shadow root for events inside shadow DOM
|
|
1866
|
+
shadowRoot.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
1867
|
+
instance[CLEANUP].events.push({
|
|
1868
|
+
target: shadowRoot,
|
|
1869
|
+
eventName: baseEventName,
|
|
1870
|
+
handler: wrappedMethod,
|
|
1871
|
+
options: listenerOptions,
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
// Also listen on host element (for clicks on host itself or when no shadow root)
|
|
1875
|
+
targetElement.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
1876
|
+
instance[CLEANUP].events.push({
|
|
1877
|
+
target: targetElement,
|
|
1878
|
+
eventName: baseEventName,
|
|
1879
|
+
handler: wrappedMethod,
|
|
1880
|
+
options: listenerOptions,
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Cleanup event listeners for a controller instance
|
|
1887
|
+
* Called automatically by the controller system during detach
|
|
1888
|
+
*/
|
|
1889
|
+
function cleanupEventHandlers(instance) {
|
|
1890
|
+
if (!instance[CLEANUP]?.events)
|
|
1891
|
+
return;
|
|
1892
|
+
for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
|
|
1893
|
+
target.removeEventListener(eventName, handler, options);
|
|
1894
|
+
}
|
|
1895
|
+
instance[CLEANUP].events = [];
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Controller-scoped cleanup registry
|
|
1899
|
+
class ControllerScope {
|
|
1900
|
+
constructor() {
|
|
1901
|
+
this.cleanupFns = new Map();
|
|
1902
|
+
this.pendingOperations = new Set();
|
|
1903
|
+
}
|
|
1904
|
+
register(key, cleanup) {
|
|
1905
|
+
this.cleanupFns.set(key, cleanup);
|
|
1906
|
+
}
|
|
1907
|
+
unregister(key) {
|
|
1908
|
+
this.cleanupFns.delete(key);
|
|
1909
|
+
}
|
|
1910
|
+
async cleanup() {
|
|
1911
|
+
// Wait for all pending operations
|
|
1912
|
+
await Promise.all(this.pendingOperations);
|
|
1913
|
+
// Run all cleanup functions
|
|
1914
|
+
for (const cleanup of this.cleanupFns.values()) {
|
|
1915
|
+
try {
|
|
1916
|
+
await cleanup();
|
|
1917
|
+
}
|
|
1918
|
+
catch (error) {
|
|
1919
|
+
console.error('Error during cleanup:', error);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
this.cleanupFns.clear();
|
|
1923
|
+
}
|
|
1924
|
+
async runOperation(operation) {
|
|
1925
|
+
const promise = operation();
|
|
1926
|
+
const voidPromise = promise.then(() => { }, () => { });
|
|
1927
|
+
this.pendingOperations.add(voidPromise);
|
|
1928
|
+
try {
|
|
1929
|
+
const result = await promise;
|
|
1930
|
+
this.pendingOperations.delete(voidPromise);
|
|
1931
|
+
return result;
|
|
1932
|
+
}
|
|
1933
|
+
catch (error) {
|
|
1934
|
+
this.pendingOperations.delete(voidPromise);
|
|
1935
|
+
throw error;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Decorator to register a controller class with a name
|
|
1941
|
+
* @param name The name to register the controller under
|
|
1942
|
+
*/
|
|
1943
|
+
function controller(name) {
|
|
1944
|
+
return function (constructor, _context) {
|
|
1945
|
+
snice.controllerRegistry.set(name, constructor);
|
|
1946
|
+
// Mark as controller class for channel decorator detection
|
|
1947
|
+
constructor.prototype[IS_CONTROLLER_CLASS] = true;
|
|
1948
|
+
return constructor;
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Attaches a controller to an element
|
|
1953
|
+
* @param element The element to attach the controller to
|
|
1954
|
+
* @param controllerName The name of the controller to attach
|
|
1955
|
+
*/
|
|
1956
|
+
async function attachController(element, controllerName) {
|
|
1957
|
+
const existingController = element[CONTROLLER_KEY];
|
|
1958
|
+
const existingName = element[CONTROLLER_NAME_KEY];
|
|
1959
|
+
// For native elements, check if this is actually the desired controller
|
|
1960
|
+
const nativeController = element[NATIVE_CONTROLLER];
|
|
1961
|
+
if (nativeController !== undefined && nativeController !== controllerName) {
|
|
1962
|
+
// This attachment is outdated, skip it
|
|
1045
1963
|
return;
|
|
1046
1964
|
}
|
|
1047
1965
|
if (existingName === controllerName && existingController) {
|
|
@@ -1080,12 +1998,12 @@ var Snice = (function (exports) {
|
|
|
1080
1998
|
await scope.runOperation(async () => {
|
|
1081
1999
|
await controllerInstance.attach(element);
|
|
1082
2000
|
});
|
|
1083
|
-
// Setup @on event handlers for controller
|
|
1084
|
-
setupEventHandlers(controllerInstance, element);
|
|
1085
2001
|
// Setup @observe observers for controller
|
|
1086
2002
|
setupObservers(controllerInstance, element);
|
|
1087
2003
|
// Setup @channel handlers for controller
|
|
1088
2004
|
setupResponseHandlers(controllerInstance, element);
|
|
2005
|
+
// Setup @on event handlers for controller
|
|
2006
|
+
setupEventHandlers(controllerInstance, element);
|
|
1089
2007
|
element.dispatchEvent(new CustomEvent('@snice/controller-attached', {
|
|
1090
2008
|
detail: { name: controllerName, controller: controllerInstance }
|
|
1091
2009
|
}));
|
|
@@ -1111,12 +2029,12 @@ var Snice = (function (exports) {
|
|
|
1111
2029
|
await controllerInstance.detach(element);
|
|
1112
2030
|
}
|
|
1113
2031
|
controllerInstance.element = null;
|
|
1114
|
-
// Cleanup @on event handlers for controller
|
|
1115
|
-
cleanupEventHandlers(controllerInstance);
|
|
1116
2032
|
// Cleanup @observe observers for controller
|
|
1117
2033
|
cleanupObservers(controllerInstance);
|
|
1118
2034
|
// Cleanup @channel handlers for controller
|
|
1119
2035
|
cleanupResponseHandlers(controllerInstance);
|
|
2036
|
+
// Cleanup @on event handlers for controller
|
|
2037
|
+
cleanupEventHandlers(controllerInstance);
|
|
1120
2038
|
// Cleanup the controller scope
|
|
1121
2039
|
if (scope) {
|
|
1122
2040
|
await scope.cleanup();
|
|
@@ -1199,33 +2117,168 @@ var Snice = (function (exports) {
|
|
|
1199
2117
|
});
|
|
1200
2118
|
}
|
|
1201
2119
|
}
|
|
1202
|
-
});
|
|
1203
|
-
// Start observing when DOM is ready
|
|
1204
|
-
if (document.readyState === 'loading') {
|
|
1205
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1206
|
-
// Process existing elements (excluding custom elements)
|
|
1207
|
-
document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
1208
|
-
// Start observing
|
|
1209
|
-
observer.observe(document.body, {
|
|
1210
|
-
attributes: true,
|
|
1211
|
-
attributeFilter: ['controller'],
|
|
1212
|
-
childList: true,
|
|
1213
|
-
subtree: true
|
|
1214
|
-
});
|
|
1215
|
-
});
|
|
2120
|
+
});
|
|
2121
|
+
// Start observing when DOM is ready
|
|
2122
|
+
if (document.readyState === 'loading') {
|
|
2123
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2124
|
+
// Process existing elements (excluding custom elements)
|
|
2125
|
+
document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
2126
|
+
// Start observing
|
|
2127
|
+
observer.observe(document.body, {
|
|
2128
|
+
attributes: true,
|
|
2129
|
+
attributeFilter: ['controller'],
|
|
2130
|
+
childList: true,
|
|
2131
|
+
subtree: true
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
// DOM already loaded
|
|
2137
|
+
document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
2138
|
+
observer.observe(document.body, {
|
|
2139
|
+
attributes: true,
|
|
2140
|
+
attributeFilter: ['controller'],
|
|
2141
|
+
childList: true,
|
|
2142
|
+
subtree: true
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
// Store observer reference for cleanup if needed
|
|
2146
|
+
globalThis.sniceNativeControllerObserver = observer;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* @context decorator for receiving router context updates
|
|
2151
|
+
*/
|
|
2152
|
+
const CONTEXT_HANDLERS = getSymbol('context-handlers');
|
|
2153
|
+
/**
|
|
2154
|
+
* @context decorator for receiving router context updates
|
|
2155
|
+
*
|
|
2156
|
+
* @example
|
|
2157
|
+
* ```typescript
|
|
2158
|
+
* @element('my-layout')
|
|
2159
|
+
* class MyLayout extends HTMLElement {
|
|
2160
|
+
* @context
|
|
2161
|
+
* handleContext(ctx: Context) {
|
|
2162
|
+
* this.renderNav(ctx.placards, ctx.currentRoute);
|
|
2163
|
+
* }
|
|
2164
|
+
*
|
|
2165
|
+
* @context({ debounce: 300 })
|
|
2166
|
+
* handleContextDebounced(ctx: Context) {
|
|
2167
|
+
* // Called after 300ms of no updates
|
|
2168
|
+
* }
|
|
2169
|
+
* }
|
|
2170
|
+
* ```
|
|
2171
|
+
*/
|
|
2172
|
+
function context$1(options = {}) {
|
|
2173
|
+
return function (originalMethod, context) {
|
|
2174
|
+
const methodName = context.name;
|
|
2175
|
+
const initKey = `__context_init_${methodName}`;
|
|
2176
|
+
context.addInitializer(function () {
|
|
2177
|
+
const constructor = this.constructor;
|
|
2178
|
+
if (constructor[initKey])
|
|
2179
|
+
return;
|
|
2180
|
+
constructor[initKey] = true;
|
|
2181
|
+
if (!constructor[CONTEXT_HANDLERS]) {
|
|
2182
|
+
constructor[CONTEXT_HANDLERS] = [];
|
|
2183
|
+
}
|
|
2184
|
+
constructor[CONTEXT_HANDLERS].push({
|
|
2185
|
+
methodName,
|
|
2186
|
+
method: originalMethod,
|
|
2187
|
+
options,
|
|
2188
|
+
});
|
|
2189
|
+
});
|
|
2190
|
+
return originalMethod;
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Setup context handler for an element instance
|
|
2195
|
+
* Called automatically during element connection
|
|
2196
|
+
*/
|
|
2197
|
+
function setupContextHandler(element) {
|
|
2198
|
+
const handlers = element.constructor[CONTEXT_HANDLERS];
|
|
2199
|
+
if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
// Get the Context instance from the router
|
|
2203
|
+
const ctx = element[CONTEXT_HANDLER];
|
|
2204
|
+
if (!ctx) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
// Store the Context instance for cleanup
|
|
2208
|
+
element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
|
|
2209
|
+
// Register each handler with the Context
|
|
2210
|
+
for (const handler of handlers) {
|
|
2211
|
+
const { methodName, method, options } = handler;
|
|
2212
|
+
const wrappedMethodName = `__wrapped_${methodName}`;
|
|
2213
|
+
// Create wrapped method with timing controls
|
|
2214
|
+
element[wrappedMethodName] = function (context) {
|
|
2215
|
+
// Skip if already called once
|
|
2216
|
+
if (options.once && element[CONTEXT_CALLED]) {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const callMethod = () => {
|
|
2220
|
+
method.call(element, context);
|
|
2221
|
+
// Handle once option
|
|
2222
|
+
if (options.once) {
|
|
2223
|
+
element[CONTEXT_CALLED] = true;
|
|
2224
|
+
// Unregister after first call
|
|
2225
|
+
const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
|
|
2226
|
+
if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
|
|
2227
|
+
ctx[CONTEXT_UNREGISTER](element);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
// Handle debounce
|
|
2232
|
+
if (options.debounce) {
|
|
2233
|
+
clearTimeout(element[CONTEXT_TIMER]);
|
|
2234
|
+
element[CONTEXT_TIMER] = setTimeout(callMethod, options.debounce);
|
|
2235
|
+
}
|
|
2236
|
+
// Handle throttle
|
|
2237
|
+
else if (options.throttle) {
|
|
2238
|
+
const now = Date.now();
|
|
2239
|
+
const lastCall = element[CONTEXT_TIMER] || 0;
|
|
2240
|
+
if (now - lastCall >= options.throttle) {
|
|
2241
|
+
element[CONTEXT_TIMER] = now;
|
|
2242
|
+
callMethod();
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
// No timing options - call immediately
|
|
2246
|
+
else {
|
|
2247
|
+
callMethod();
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
// Register with the Context using the wrapped method name
|
|
2251
|
+
if (typeof ctx[CONTEXT_REGISTER] === 'function') {
|
|
2252
|
+
ctx[CONTEXT_REGISTER](element, wrappedMethodName);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Cleanup context handler for an element instance
|
|
2258
|
+
* Called automatically during element disconnection
|
|
2259
|
+
*/
|
|
2260
|
+
function cleanupContextHandler(element) {
|
|
2261
|
+
const handlers = element.constructor[CONTEXT_HANDLERS];
|
|
2262
|
+
if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
// Clear any pending debounce timer
|
|
2266
|
+
for (const handler of handlers) {
|
|
2267
|
+
if (handler.options.debounce && element[CONTEXT_TIMER]) {
|
|
2268
|
+
clearTimeout(element[CONTEXT_TIMER]);
|
|
2269
|
+
delete element[CONTEXT_TIMER];
|
|
2270
|
+
}
|
|
2271
|
+
// Clean up wrapped method
|
|
2272
|
+
const wrappedMethodName = `__wrapped_${handler.methodName}`;
|
|
2273
|
+
delete element[wrappedMethodName];
|
|
1216
2274
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
attributes: true,
|
|
1222
|
-
attributeFilter: ['controller'],
|
|
1223
|
-
childList: true,
|
|
1224
|
-
subtree: true
|
|
1225
|
-
});
|
|
2275
|
+
// Unregister from Context if available
|
|
2276
|
+
const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
|
|
2277
|
+
if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
|
|
2278
|
+
ctx[CONTEXT_UNREGISTER](element);
|
|
1226
2279
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
2280
|
+
delete element[NAVIGATION_CONTEXT_INSTANCE];
|
|
2281
|
+
delete element[CONTEXT_CALLED];
|
|
1229
2282
|
}
|
|
1230
2283
|
|
|
1231
2284
|
/**
|
|
@@ -1286,6 +2339,87 @@ var Snice = (function (exports) {
|
|
|
1286
2339
|
}
|
|
1287
2340
|
}
|
|
1288
2341
|
|
|
2342
|
+
var _a, _b, _c;
|
|
2343
|
+
// Symbol for storing the Set of elements
|
|
2344
|
+
const REGISTERED_ELEMENTS_SET = Symbol('registered-elements-set');
|
|
2345
|
+
// Counter for generating unique context IDs
|
|
2346
|
+
let contextIdCounter = 0;
|
|
2347
|
+
/**
|
|
2348
|
+
* Represents the bundled router state that can notify registered elements of changes
|
|
2349
|
+
*/
|
|
2350
|
+
class Context {
|
|
2351
|
+
constructor(context = {}, placards = [], currentRoute = '', routeParams = {}) {
|
|
2352
|
+
this[_a] = new WeakMap();
|
|
2353
|
+
this[_b] = new Set();
|
|
2354
|
+
this[_c] = false;
|
|
2355
|
+
this.id = contextIdCounter++;
|
|
2356
|
+
this.application = context;
|
|
2357
|
+
this.navigation = {
|
|
2358
|
+
placards,
|
|
2359
|
+
route: currentRoute,
|
|
2360
|
+
params: routeParams
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Register an element to receive context updates
|
|
2365
|
+
* @internal Used by @context decorator
|
|
2366
|
+
*/
|
|
2367
|
+
[(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
|
|
2368
|
+
this[REGISTERED_ELEMENTS].set(element, methodName);
|
|
2369
|
+
this[REGISTERED_ELEMENTS_SET].add(element);
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Unregister an element from receiving context updates
|
|
2373
|
+
* @internal Used by @context decorator cleanup
|
|
2374
|
+
*/
|
|
2375
|
+
[CONTEXT_UNREGISTER](element) {
|
|
2376
|
+
this[REGISTERED_ELEMENTS].delete(element);
|
|
2377
|
+
this[REGISTERED_ELEMENTS_SET].delete(element);
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Update the context and notify all registered elements
|
|
2381
|
+
* Prevents infinite loops by tracking update state
|
|
2382
|
+
*/
|
|
2383
|
+
update(context, placards, currentRoute, routeParams) {
|
|
2384
|
+
// Prevent infinite loops
|
|
2385
|
+
if (this[IS_UPDATING]) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
this[IS_UPDATING] = true;
|
|
2389
|
+
// Update properties (id remains immutable)
|
|
2390
|
+
this.application = context;
|
|
2391
|
+
this.navigation.placards = placards;
|
|
2392
|
+
this.navigation.route = currentRoute;
|
|
2393
|
+
this.navigation.params = routeParams;
|
|
2394
|
+
// Notify all registered elements by calling their methods directly
|
|
2395
|
+
const elementsSet = this[REGISTERED_ELEMENTS_SET];
|
|
2396
|
+
const elementsMap = this[REGISTERED_ELEMENTS];
|
|
2397
|
+
for (const element of elementsSet) {
|
|
2398
|
+
const methodName = elementsMap.get(element);
|
|
2399
|
+
if (methodName && typeof element[methodName] === 'function') {
|
|
2400
|
+
try {
|
|
2401
|
+
element[methodName](this);
|
|
2402
|
+
}
|
|
2403
|
+
catch (error) {
|
|
2404
|
+
// Log error but continue notifying other elements
|
|
2405
|
+
console.error(`Error calling @context method ${methodName}:`, error);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
this[IS_UPDATING] = false;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Notify a specific element of the current context state
|
|
2413
|
+
* @internal Used by @context decorator
|
|
2414
|
+
*/
|
|
2415
|
+
[CONTEXT_NOTIFY_ELEMENT](element) {
|
|
2416
|
+
const methodName = this[REGISTERED_ELEMENTS].get(element);
|
|
2417
|
+
if (methodName && typeof element[methodName] === 'function') {
|
|
2418
|
+
element[methodName](this);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
1289
2423
|
/**
|
|
1290
2424
|
* Detects the type constructor from an initial value
|
|
1291
2425
|
* @param initialValue - The default value assigned to a property field (e.g., `name = "default"` -> "default")
|
|
@@ -1414,6 +2548,297 @@ var Snice = (function (exports) {
|
|
|
1414
2548
|
}
|
|
1415
2549
|
}
|
|
1416
2550
|
|
|
2551
|
+
/**
|
|
2552
|
+
* @render and @styles decorators for Snice v3.0.0
|
|
2553
|
+
* Provides automatic differential rendering on property changes
|
|
2554
|
+
*/
|
|
2555
|
+
/**
|
|
2556
|
+
* Global render scheduler for microtask batching
|
|
2557
|
+
* Batches multiple property changes into a single render
|
|
2558
|
+
*/
|
|
2559
|
+
class RenderScheduler {
|
|
2560
|
+
constructor() {
|
|
2561
|
+
this.pending = new Set();
|
|
2562
|
+
this.scheduled = false;
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Schedule an element for rendering
|
|
2566
|
+
* Batches renders in a microtask unless sync option is enabled
|
|
2567
|
+
*/
|
|
2568
|
+
schedule(element, options) {
|
|
2569
|
+
// Sync rendering - execute immediately
|
|
2570
|
+
if (options.sync) {
|
|
2571
|
+
performRender(element, options);
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
// Async rendering - batch in microtask
|
|
2575
|
+
this.pending.add(element);
|
|
2576
|
+
if (!this.scheduled) {
|
|
2577
|
+
this.scheduled = true;
|
|
2578
|
+
queueMicrotask(() => this.flush());
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Flush all pending renders
|
|
2583
|
+
*/
|
|
2584
|
+
flush() {
|
|
2585
|
+
const elements = Array.from(this.pending);
|
|
2586
|
+
this.pending.clear();
|
|
2587
|
+
this.scheduled = false;
|
|
2588
|
+
for (const element of elements) {
|
|
2589
|
+
const options = element[RENDER_OPTIONS] || {};
|
|
2590
|
+
performRender(element, options);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
const renderScheduler = new RenderScheduler();
|
|
2595
|
+
/**
|
|
2596
|
+
* Perform the actual render of an element
|
|
2597
|
+
*/
|
|
2598
|
+
function performRender(element, options, precomputedResult) {
|
|
2599
|
+
const renderMethod = element[RENDER_METHOD];
|
|
2600
|
+
if (!renderMethod)
|
|
2601
|
+
return;
|
|
2602
|
+
// If once is true and we've already rendered, skip
|
|
2603
|
+
if (options.once && element[RENDER_INSTANCE]) {
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
try {
|
|
2607
|
+
// Use precomputed result if provided, otherwise call the render method
|
|
2608
|
+
const result = precomputedResult !== undefined ? precomputedResult : renderMethod.call(element);
|
|
2609
|
+
// Check if differential rendering is disabled (expects string)
|
|
2610
|
+
const differential = options.differential !== false;
|
|
2611
|
+
if (!differential) {
|
|
2612
|
+
// Non-differential expects string
|
|
2613
|
+
if (typeof result !== 'string') {
|
|
2614
|
+
console.warn('Render method with differential: false must return a string');
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
// Simple string rendering
|
|
2618
|
+
if (!element.shadowRoot) {
|
|
2619
|
+
element.attachShadow({ mode: 'open' });
|
|
2620
|
+
}
|
|
2621
|
+
element.shadowRoot.innerHTML = result;
|
|
2622
|
+
// Mark scheduled flag as false
|
|
2623
|
+
element[RENDER_SCHEDULED] = false;
|
|
2624
|
+
// Call all registered render callbacks
|
|
2625
|
+
const callbacks = element[RENDER_CALLBACKS];
|
|
2626
|
+
if (callbacks && callbacks.length > 0) {
|
|
2627
|
+
const cbs = [...callbacks];
|
|
2628
|
+
element[RENDER_CALLBACKS] = [];
|
|
2629
|
+
cbs.forEach(cb => cb());
|
|
2630
|
+
}
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
if (!isTemplateResult(result)) {
|
|
2634
|
+
console.warn('Render method must return html`` template result');
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
// Get or create template instance (differential rendering)
|
|
2638
|
+
let instance = element[RENDER_INSTANCE];
|
|
2639
|
+
if (!instance) {
|
|
2640
|
+
// First render - create shadow root if needed and initial instance
|
|
2641
|
+
if (!element.shadowRoot) {
|
|
2642
|
+
element.attachShadow({ mode: 'open' });
|
|
2643
|
+
}
|
|
2644
|
+
instance = new TemplateInstance(result);
|
|
2645
|
+
element[RENDER_INSTANCE] = instance;
|
|
2646
|
+
// Create the fragment but don't commit values yet
|
|
2647
|
+
const fragment = instance.renderFragment();
|
|
2648
|
+
// Append to shadow root first so getRootNode() works in event handlers
|
|
2649
|
+
element.shadowRoot.appendChild(fragment);
|
|
2650
|
+
// Now commit values (this binds event handlers with correct host)
|
|
2651
|
+
instance.update(result.values);
|
|
2652
|
+
}
|
|
2653
|
+
else {
|
|
2654
|
+
// Subsequent render - just update the parts (differential rendering!)
|
|
2655
|
+
instance.update(result.values);
|
|
2656
|
+
}
|
|
2657
|
+
// Mark scheduled flag as false
|
|
2658
|
+
element[RENDER_SCHEDULED] = false;
|
|
2659
|
+
// Call all registered render callbacks (for testing/debugging)
|
|
2660
|
+
const callbacks = element[RENDER_CALLBACKS];
|
|
2661
|
+
if (callbacks && callbacks.length > 0) {
|
|
2662
|
+
const cbs = [...callbacks];
|
|
2663
|
+
element[RENDER_CALLBACKS] = [];
|
|
2664
|
+
cbs.forEach(cb => cb());
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
catch (error) {
|
|
2668
|
+
console.error('Error rendering element:', error);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Request a render for an element
|
|
2673
|
+
* Respects debounce/throttle/once/sync options
|
|
2674
|
+
* @param immediate - Force immediate render (used for initial render)
|
|
2675
|
+
*/
|
|
2676
|
+
function requestRender(element, immediate = false) {
|
|
2677
|
+
const options = element[RENDER_OPTIONS] || {};
|
|
2678
|
+
// Handle once option
|
|
2679
|
+
if (options.once && element[RENDER_INSTANCE]) {
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
// Force immediate render (for initial render)
|
|
2683
|
+
if (immediate) {
|
|
2684
|
+
performRender(element, options);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
// Handle debounce
|
|
2688
|
+
if (options.debounce !== undefined && options.debounce > 0) {
|
|
2689
|
+
if (!element[RENDER_TIMERS]) {
|
|
2690
|
+
element[RENDER_TIMERS] = {};
|
|
2691
|
+
}
|
|
2692
|
+
clearTimeout(element[RENDER_TIMERS].debounce);
|
|
2693
|
+
element[RENDER_TIMERS].debounce = setTimeout(() => {
|
|
2694
|
+
renderScheduler.schedule(element, options);
|
|
2695
|
+
}, options.debounce);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
// Handle throttle
|
|
2699
|
+
if (options.throttle !== undefined && options.throttle > 0) {
|
|
2700
|
+
if (!element[RENDER_TIMERS]) {
|
|
2701
|
+
element[RENDER_TIMERS] = { lastThrottle: 0 };
|
|
2702
|
+
}
|
|
2703
|
+
const timers = element[RENDER_TIMERS];
|
|
2704
|
+
const now = Date.now();
|
|
2705
|
+
if (timers.lastThrottle === 0 || now - timers.lastThrottle >= options.throttle) {
|
|
2706
|
+
timers.lastThrottle = now;
|
|
2707
|
+
renderScheduler.schedule(element, options);
|
|
2708
|
+
}
|
|
2709
|
+
else {
|
|
2710
|
+
// Schedule for later if not already scheduled
|
|
2711
|
+
if (!timers.throttleTimer) {
|
|
2712
|
+
const remaining = options.throttle - (now - timers.lastThrottle);
|
|
2713
|
+
timers.throttleTimer = setTimeout(() => {
|
|
2714
|
+
timers.throttleTimer = null;
|
|
2715
|
+
timers.lastThrottle = Date.now();
|
|
2716
|
+
renderScheduler.schedule(element, options);
|
|
2717
|
+
}, remaining);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
// Normal rendering (with microtask batching unless sync)
|
|
2723
|
+
renderScheduler.schedule(element, options);
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* @render decorator for component rendering
|
|
2727
|
+
*
|
|
2728
|
+
* Marks a method as the render method for the component.
|
|
2729
|
+
* The method should return html`...` template.
|
|
2730
|
+
* Automatically re-renders when properties change (unless once: true).
|
|
2731
|
+
*
|
|
2732
|
+
* @example
|
|
2733
|
+
* ```typescript
|
|
2734
|
+
* @render()
|
|
2735
|
+
* renderContent() {
|
|
2736
|
+
* return html`<div>${this.count}</div>`;
|
|
2737
|
+
* }
|
|
2738
|
+
* ```
|
|
2739
|
+
*
|
|
2740
|
+
* @example
|
|
2741
|
+
* ```typescript
|
|
2742
|
+
* // Debounced rendering
|
|
2743
|
+
* @render({ debounce: 100 })
|
|
2744
|
+
* renderContent() {
|
|
2745
|
+
* return html`<div>${this.searchTerm}</div>`;
|
|
2746
|
+
* }
|
|
2747
|
+
* ```
|
|
2748
|
+
*
|
|
2749
|
+
* @example
|
|
2750
|
+
* ```typescript
|
|
2751
|
+
* // Render only once (manual re-renders only)
|
|
2752
|
+
* @render({ once: true })
|
|
2753
|
+
* renderContent() {
|
|
2754
|
+
* return html`<div>Static content</div>`;
|
|
2755
|
+
* }
|
|
2756
|
+
* ```
|
|
2757
|
+
*/
|
|
2758
|
+
function render(options = {}) {
|
|
2759
|
+
return function (originalMethod, context) {
|
|
2760
|
+
context.name;
|
|
2761
|
+
context.addInitializer(function () {
|
|
2762
|
+
// Store the render method and options
|
|
2763
|
+
this[RENDER_METHOD] = originalMethod;
|
|
2764
|
+
this[RENDER_OPTIONS] = options;
|
|
2765
|
+
});
|
|
2766
|
+
// Return wrapped method that triggers re-render when called manually
|
|
2767
|
+
return function (...args) {
|
|
2768
|
+
// Call original method to get the template
|
|
2769
|
+
const result = originalMethod.apply(this, args);
|
|
2770
|
+
// Always render when method is called manually (even if once: true)
|
|
2771
|
+
// Force immediate render to bypass all options, pass precomputed result to avoid calling method twice
|
|
2772
|
+
performRender(this, {}, result);
|
|
2773
|
+
return result;
|
|
2774
|
+
};
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* @styles decorator for component styles
|
|
2779
|
+
*
|
|
2780
|
+
* Marks a method as the styles method for the component.
|
|
2781
|
+
* The method should return css`...` template.
|
|
2782
|
+
* Styles are applied once when the component is connected.
|
|
2783
|
+
*
|
|
2784
|
+
* @example
|
|
2785
|
+
* ```typescript
|
|
2786
|
+
* @styles()
|
|
2787
|
+
* styles() {
|
|
2788
|
+
* return css`:host { display: block; }`;
|
|
2789
|
+
* }
|
|
2790
|
+
* ```
|
|
2791
|
+
*/
|
|
2792
|
+
function styles() {
|
|
2793
|
+
return function (originalMethod, context) {
|
|
2794
|
+
context.addInitializer(function () {
|
|
2795
|
+
// Store the styles method
|
|
2796
|
+
this[STYLES_METHOD] = originalMethod;
|
|
2797
|
+
});
|
|
2798
|
+
return originalMethod;
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Apply styles to an element
|
|
2803
|
+
* Called during element initialization
|
|
2804
|
+
*/
|
|
2805
|
+
function applyStyles(element) {
|
|
2806
|
+
const stylesMethod = element[STYLES_METHOD];
|
|
2807
|
+
if (!stylesMethod)
|
|
2808
|
+
return;
|
|
2809
|
+
// Only apply once
|
|
2810
|
+
if (element[STYLES_APPLIED])
|
|
2811
|
+
return;
|
|
2812
|
+
element[STYLES_APPLIED] = true;
|
|
2813
|
+
try {
|
|
2814
|
+
const result = stylesMethod.call(element);
|
|
2815
|
+
if (!isCSSResult(result)) {
|
|
2816
|
+
console.warn('Styles method must return css`` template result');
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
// Ensure shadow root exists
|
|
2820
|
+
if (!element.shadowRoot) {
|
|
2821
|
+
element.attachShadow({ mode: 'open' });
|
|
2822
|
+
}
|
|
2823
|
+
// Create base styles for meta elements (if, case)
|
|
2824
|
+
const baseStyleSheet = new CSSStyleSheet();
|
|
2825
|
+
baseStyleSheet.replaceSync('if, case { display: contents; }');
|
|
2826
|
+
// Try to use constructable stylesheets for better performance
|
|
2827
|
+
if (element.shadowRoot && result.styleSheet && 'adoptedStyleSheets' in element.shadowRoot) {
|
|
2828
|
+
element.shadowRoot.adoptedStyleSheets = [baseStyleSheet, result.styleSheet];
|
|
2829
|
+
}
|
|
2830
|
+
else if (element.shadowRoot) {
|
|
2831
|
+
// Fallback to <style> tag
|
|
2832
|
+
const style = document.createElement('style');
|
|
2833
|
+
style.textContent = 'if, case { display: contents; }\n' + result.cssText;
|
|
2834
|
+
element.shadowRoot.appendChild(style);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
catch (error) {
|
|
2838
|
+
console.error('Error applying styles:', error);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
1417
2842
|
/**
|
|
1418
2843
|
* Applies core element functionality to a constructor
|
|
1419
2844
|
* This is shared between @element and @page decorators
|
|
@@ -1458,6 +2883,9 @@ var Snice = (function (exports) {
|
|
|
1458
2883
|
enumerable: true,
|
|
1459
2884
|
configurable: true
|
|
1460
2885
|
});
|
|
2886
|
+
// Note: rendered promise is stored via symbols RENDERED_PROMISE and RENDERED_RESOLVE
|
|
2887
|
+
// It's not exposed as a public property - only accessible via test utilities
|
|
2888
|
+
// This prevents accidental misuse in production code
|
|
1461
2889
|
// Add controller property
|
|
1462
2890
|
Object.defineProperty(constructor.prototype, 'controller', {
|
|
1463
2891
|
get() {
|
|
@@ -1495,6 +2923,7 @@ var Snice = (function (exports) {
|
|
|
1495
2923
|
// Re-establish handlers that get cleaned up on disconnect
|
|
1496
2924
|
setupEventHandlers(this, this);
|
|
1497
2925
|
setupResponseHandlers(this, this);
|
|
2926
|
+
setupContextHandler(this);
|
|
1498
2927
|
// Re-establish observers that get cleaned up on disconnect
|
|
1499
2928
|
try {
|
|
1500
2929
|
setupObservers(this, this);
|
|
@@ -1509,6 +2938,9 @@ var Snice = (function (exports) {
|
|
|
1509
2938
|
return;
|
|
1510
2939
|
}
|
|
1511
2940
|
try {
|
|
2941
|
+
// Mark that properties are being initialized from attributes
|
|
2942
|
+
// This allows property setters to work during initialization
|
|
2943
|
+
this[PROPERTIES_INITIALIZED] = true;
|
|
1512
2944
|
// Initialize properties from attributes before rendering
|
|
1513
2945
|
const properties = constructor[PROPERTIES];
|
|
1514
2946
|
if (properties) {
|
|
@@ -1527,82 +2959,69 @@ var Snice = (function (exports) {
|
|
|
1527
2959
|
}
|
|
1528
2960
|
}
|
|
1529
2961
|
}
|
|
1530
|
-
//
|
|
1531
|
-
|
|
2962
|
+
// Apply any properties that were set before element was connected
|
|
2963
|
+
// BUT only if they don't have an HTML attribute (don't override HTML attributes)
|
|
2964
|
+
if (this[PRE_INIT_PROPERTY_VALUES]) {
|
|
2965
|
+
for (const [propName, propValue] of this[PRE_INIT_PROPERTY_VALUES]) {
|
|
2966
|
+
// Remove from map first so getter doesn't return it during setter call
|
|
2967
|
+
this[PRE_INIT_PROPERTY_VALUES].delete(propName);
|
|
2968
|
+
// Only apply pre-init value if NO HTML attribute exists
|
|
2969
|
+
// This prevents field initializers from overwriting HTML attributes
|
|
2970
|
+
const propOptions = properties?.get(propName);
|
|
2971
|
+
const attributeName = typeof propOptions?.attribute === 'string' ? propOptions.attribute : propName.toLowerCase();
|
|
2972
|
+
if (!this.hasAttribute(attributeName)) {
|
|
2973
|
+
this[propName] = propValue;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
// Clear the pre-init values map
|
|
2977
|
+
delete this[PRE_INIT_PROPERTY_VALUES];
|
|
2978
|
+
}
|
|
1532
2979
|
// Properties are now stateless and read from DOM attributes only
|
|
1533
2980
|
// Initial values are not automatically reflected
|
|
1534
|
-
//
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
//
|
|
1539
|
-
|
|
1540
|
-
//
|
|
1541
|
-
if (this
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
shadowContent += htmlContent;
|
|
2981
|
+
// v3.0.0: Apply @styles decorator if present
|
|
2982
|
+
// This creates the shadow root and applies styles
|
|
2983
|
+
applyStyles(this);
|
|
2984
|
+
// v3.0.0: Perform initial @render if present
|
|
2985
|
+
// This uses differential rendering with template system
|
|
2986
|
+
// Defer initial render to next microtask to allow property bindings
|
|
2987
|
+
// from parent to be set first (avoids infinite loops in nested elements)
|
|
2988
|
+
if (this[RENDER_METHOD]) {
|
|
2989
|
+
queueMicrotask(() => {
|
|
2990
|
+
requestRender(this, true);
|
|
2991
|
+
// Setup observers after first render completes so shadow DOM content exists
|
|
2992
|
+
try {
|
|
2993
|
+
setupObservers(this, this);
|
|
1548
2994
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2995
|
+
catch (error) {
|
|
2996
|
+
console.error(`Error setting up observers for ${this.tagName}:`, error);
|
|
2997
|
+
}
|
|
2998
|
+
// Mark element as ready after initial render completes
|
|
2999
|
+
if (this[READY_RESOLVE]) {
|
|
3000
|
+
this[READY_RESOLVE]();
|
|
3001
|
+
this[READY_RESOLVE] = null;
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
1553
3004
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
3005
|
+
else {
|
|
3006
|
+
// No render method, setup observers immediately
|
|
1556
3007
|
try {
|
|
1557
|
-
|
|
1558
|
-
// Handle both async and sync css
|
|
1559
|
-
const cssResolved = cssResult instanceof Promise ? await cssResult : cssResult;
|
|
1560
|
-
if (cssResolved) {
|
|
1561
|
-
// Handle both string and array of strings
|
|
1562
|
-
const cssContent = Array.isArray(cssResolved) ? cssResolved.join('\n') : cssResolved;
|
|
1563
|
-
// No need for scoping with Shadow DOM, but add data attribute for compatibility
|
|
1564
|
-
shadowContent += `<style data-component-css>${cssContent}</style>`;
|
|
1565
|
-
}
|
|
3008
|
+
setupObservers(this, this);
|
|
1566
3009
|
}
|
|
1567
3010
|
catch (error) {
|
|
1568
|
-
console.error(`Error
|
|
3011
|
+
console.error(`Error setting up observers for ${this.tagName}:`, error);
|
|
1569
3012
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
}
|
|
1575
|
-
// Render all @part methods into their corresponding elements
|
|
1576
|
-
const parts = constructor[PARTS];
|
|
1577
|
-
if (parts && this.shadowRoot) {
|
|
1578
|
-
for (const [partName, partHandler] of parts) {
|
|
1579
|
-
try {
|
|
1580
|
-
const partElement = this.shadowRoot.querySelector(`[part="${partName}"]`);
|
|
1581
|
-
if (partElement) {
|
|
1582
|
-
// For initial render, call original method directly to avoid timing restrictions
|
|
1583
|
-
const partResult = partHandler.method.call(this);
|
|
1584
|
-
const partContent = partResult instanceof Promise ? await partResult : partResult;
|
|
1585
|
-
if (partContent !== undefined) {
|
|
1586
|
-
partElement.innerHTML = partContent;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
catch (error) {
|
|
1591
|
-
console.error(`Error rendering @part('${partName}') in ${this.tagName}:`, error);
|
|
1592
|
-
}
|
|
3013
|
+
// Mark element as ready immediately if no render method
|
|
3014
|
+
if (this[READY_RESOLVE]) {
|
|
3015
|
+
this[READY_RESOLVE]();
|
|
3016
|
+
this[READY_RESOLVE] = null;
|
|
1593
3017
|
}
|
|
1594
3018
|
}
|
|
1595
|
-
// Setup @on event handlers
|
|
3019
|
+
// Setup @on event handlers (v2.5.4 compatibility restored!)
|
|
1596
3020
|
setupEventHandlers(this, this);
|
|
1597
3021
|
// Setup @respond handlers for elements
|
|
1598
3022
|
setupResponseHandlers(this, this);
|
|
1599
|
-
// Setup @
|
|
1600
|
-
|
|
1601
|
-
setupObservers(this, this);
|
|
1602
|
-
}
|
|
1603
|
-
catch (error) {
|
|
1604
|
-
console.error(`Error setting up observers for ${this.tagName}:`, error);
|
|
1605
|
-
}
|
|
3023
|
+
// Setup @context handler for elements
|
|
3024
|
+
setupContextHandler(this);
|
|
1606
3025
|
// Mark as initialized
|
|
1607
3026
|
this[INITIALIZED] = true;
|
|
1608
3027
|
// NOW call the original user-defined connectedCallback after shadow DOM is set up
|
|
@@ -1611,11 +3030,8 @@ var Snice = (function (exports) {
|
|
|
1611
3030
|
}
|
|
1612
3031
|
}
|
|
1613
3032
|
finally {
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1616
|
-
this[READY_RESOLVE]();
|
|
1617
|
-
this[READY_RESOLVE] = null; // Clear the resolver
|
|
1618
|
-
}
|
|
3033
|
+
// Ready is now resolved inside the render microtask (or immediately if no render)
|
|
3034
|
+
// This ensures ready waits for initial render to complete
|
|
1619
3035
|
}
|
|
1620
3036
|
// Call @ready handlers after everything is set up and ready promise is resolved
|
|
1621
3037
|
const readyHandlers = constructor[READY_HANDLERS];
|
|
@@ -1652,10 +3068,12 @@ var Snice = (function (exports) {
|
|
|
1652
3068
|
console.error(`Failed to detach controller:`, error);
|
|
1653
3069
|
});
|
|
1654
3070
|
}
|
|
1655
|
-
// Cleanup @on event handlers
|
|
3071
|
+
// Cleanup @on event handlers (v2.5.4 compatibility restored!)
|
|
1656
3072
|
cleanupEventHandlers(this);
|
|
1657
3073
|
// Cleanup @respond handlers
|
|
1658
3074
|
cleanupResponseHandlers(this);
|
|
3075
|
+
// Cleanup @context handler
|
|
3076
|
+
cleanupContextHandler(this);
|
|
1659
3077
|
// Cleanup @observe observers
|
|
1660
3078
|
cleanupObservers(this);
|
|
1661
3079
|
};
|
|
@@ -1718,6 +3136,10 @@ var Snice = (function (exports) {
|
|
|
1718
3136
|
}
|
|
1719
3137
|
}
|
|
1720
3138
|
}
|
|
3139
|
+
// Trigger auto-render on attribute change (same as property setter)
|
|
3140
|
+
if (this[RENDER_METHOD] && this[INITIALIZED]) {
|
|
3141
|
+
requestRender(this);
|
|
3142
|
+
}
|
|
1721
3143
|
}
|
|
1722
3144
|
}
|
|
1723
3145
|
break;
|
|
@@ -1757,7 +3179,7 @@ var Snice = (function (exports) {
|
|
|
1757
3179
|
}
|
|
1758
3180
|
};
|
|
1759
3181
|
}
|
|
1760
|
-
function element(tagName) {
|
|
3182
|
+
function element(tagName, options) {
|
|
1761
3183
|
return function (constructor, context) {
|
|
1762
3184
|
// Transfer metadata from context to constructor
|
|
1763
3185
|
if (context.metadata && context.metadata[PROPERTIES]) {
|
|
@@ -1768,6 +3190,11 @@ var Snice = (function (exports) {
|
|
|
1768
3190
|
constructor[PROPERTIES].set(key, value);
|
|
1769
3191
|
}
|
|
1770
3192
|
}
|
|
3193
|
+
// Set up form association if requested via options
|
|
3194
|
+
// MUST be done BEFORE applyElementFunctionality and customElements.define
|
|
3195
|
+
if (options?.formAssociated === true) {
|
|
3196
|
+
constructor.formAssociated = true;
|
|
3197
|
+
}
|
|
1771
3198
|
applyElementFunctionality(constructor);
|
|
1772
3199
|
customElements.define(tagName, constructor);
|
|
1773
3200
|
return constructor;
|
|
@@ -1823,29 +3250,43 @@ var Snice = (function (exports) {
|
|
|
1823
3250
|
if (attrValue !== null) {
|
|
1824
3251
|
return parseAttributeValue(attrValue, finalOptions || {}, undefined, initialValue);
|
|
1825
3252
|
}
|
|
1826
|
-
// For Boolean properties that have been explicitly set via attribute,
|
|
3253
|
+
// For Boolean properties that have been explicitly set via attribute (and then removed),
|
|
1827
3254
|
// follow HTML boolean attribute semantics (absence = false)
|
|
1828
3255
|
const inferredType = finalOptions?.type || detectType(initialValue);
|
|
1829
3256
|
if (inferredType === Boolean && this[EXPLICITLY_SET_PROPERTIES]?.has(propertyKey)) {
|
|
1830
3257
|
return false;
|
|
1831
3258
|
}
|
|
1832
|
-
//
|
|
3259
|
+
// Check for pre-init property values (set before element was connected)
|
|
3260
|
+
if (this[PRE_INIT_PROPERTY_VALUES]?.has(propertyKey)) {
|
|
3261
|
+
return this[PRE_INIT_PROPERTY_VALUES].get(propertyKey);
|
|
3262
|
+
}
|
|
3263
|
+
// Otherwise return initial value (respects default values like showRememberMe = true)
|
|
1833
3264
|
return initialValue;
|
|
1834
3265
|
},
|
|
1835
3266
|
set(newValue) {
|
|
1836
|
-
// Get old value
|
|
1837
|
-
|
|
1838
|
-
this[PROPERTY_VALUES] = {};
|
|
1839
|
-
}
|
|
1840
|
-
const oldValue = this[PROPERTY_VALUES][propertyKey];
|
|
3267
|
+
// Get old value by calling the getter (which reads from attribute)
|
|
3268
|
+
const oldValue = this[propertyKey];
|
|
1841
3269
|
// Check if value actually changed
|
|
1842
3270
|
if (oldValue === newValue)
|
|
1843
3271
|
return;
|
|
1844
|
-
//
|
|
1845
|
-
|
|
1846
|
-
|
|
3272
|
+
// Don't reflect to DOM until connectedCallback has started
|
|
3273
|
+
// This prevents field initializers from overwriting HTML attributes
|
|
3274
|
+
if (!this[PROPERTIES_INITIALIZED]) {
|
|
3275
|
+
// Store value for later application when element is connected
|
|
3276
|
+
if (!this[PRE_INIT_PROPERTY_VALUES]) {
|
|
3277
|
+
this[PRE_INIT_PROPERTY_VALUES] = new Map();
|
|
3278
|
+
}
|
|
3279
|
+
this[PRE_INIT_PROPERTY_VALUES].set(propertyKey, newValue);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
// Reflect to DOM - properties are backed by attributes
|
|
1847
3283
|
const attributeName = typeof finalOptions.attribute === 'string' ? finalOptions.attribute : propertyKey.toLowerCase();
|
|
1848
3284
|
const attributeValue = valueToAttribute(newValue, finalOptions, initialValue);
|
|
3285
|
+
// Mark as explicitly set for boolean handling
|
|
3286
|
+
if (!this[EXPLICITLY_SET_PROPERTIES]) {
|
|
3287
|
+
this[EXPLICITLY_SET_PROPERTIES] = new Set();
|
|
3288
|
+
}
|
|
3289
|
+
this[EXPLICITLY_SET_PROPERTIES].add(propertyKey);
|
|
1849
3290
|
// Flag to prevent attributeChangedCallback from triggering watchers for this change
|
|
1850
3291
|
if (!this._settingFromProperty)
|
|
1851
3292
|
this._settingFromProperty = new Set();
|
|
@@ -1887,8 +3328,12 @@ var Snice = (function (exports) {
|
|
|
1887
3328
|
}
|
|
1888
3329
|
}
|
|
1889
3330
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
3331
|
+
// v3.0.0: Trigger auto-render on property change
|
|
3332
|
+
// This respects @render options (debounce, throttle, once, sync)
|
|
3333
|
+
// Only trigger renders after element is fully initialized to avoid
|
|
3334
|
+
// infinite loops during initial setup
|
|
3335
|
+
if (this[RENDER_METHOD] && this[INITIALIZED]) {
|
|
3336
|
+
requestRender(this);
|
|
1892
3337
|
}
|
|
1893
3338
|
},
|
|
1894
3339
|
configurable: true,
|
|
@@ -1983,12 +3428,15 @@ var Snice = (function (exports) {
|
|
|
1983
3428
|
function watch(...propertyNames) {
|
|
1984
3429
|
return function (target, context) {
|
|
1985
3430
|
const methodName = context.name;
|
|
3431
|
+
const initKey = `__watch_init_${methodName}`;
|
|
1986
3432
|
context.addInitializer(function () {
|
|
1987
3433
|
const constructor = this.constructor;
|
|
3434
|
+
if (constructor[initKey])
|
|
3435
|
+
return;
|
|
3436
|
+
constructor[initKey] = true;
|
|
1988
3437
|
if (!constructor[PROPERTY_WATCHERS]) {
|
|
1989
3438
|
constructor[PROPERTY_WATCHERS] = new Map();
|
|
1990
3439
|
}
|
|
1991
|
-
// Store the watcher method for each property
|
|
1992
3440
|
for (const propertyName of propertyNames) {
|
|
1993
3441
|
if (!constructor[PROPERTY_WATCHERS].has(propertyName)) {
|
|
1994
3442
|
constructor[PROPERTY_WATCHERS].set(propertyName, []);
|
|
@@ -2067,8 +3515,12 @@ var Snice = (function (exports) {
|
|
|
2067
3515
|
function ready() {
|
|
2068
3516
|
return function (target, context) {
|
|
2069
3517
|
const methodName = context.name;
|
|
3518
|
+
const initKey = `__ready_init_${methodName}`;
|
|
2070
3519
|
context.addInitializer(function () {
|
|
2071
3520
|
const constructor = this.constructor;
|
|
3521
|
+
if (constructor[initKey])
|
|
3522
|
+
return;
|
|
3523
|
+
constructor[initKey] = true;
|
|
2072
3524
|
if (!constructor[READY_HANDLERS]) {
|
|
2073
3525
|
constructor[READY_HANDLERS] = [];
|
|
2074
3526
|
}
|
|
@@ -2086,8 +3538,12 @@ var Snice = (function (exports) {
|
|
|
2086
3538
|
function dispose() {
|
|
2087
3539
|
return function (target, context) {
|
|
2088
3540
|
const methodName = context.name;
|
|
3541
|
+
const initKey = `__dispose_init_${methodName}`;
|
|
2089
3542
|
context.addInitializer(function () {
|
|
2090
3543
|
const constructor = this.constructor;
|
|
3544
|
+
if (constructor[initKey])
|
|
3545
|
+
return;
|
|
3546
|
+
constructor[initKey] = true;
|
|
2091
3547
|
if (!constructor[DISPOSE_HANDLERS]) {
|
|
2092
3548
|
constructor[DISPOSE_HANDLERS] = [];
|
|
2093
3549
|
}
|
|
@@ -2105,8 +3561,12 @@ var Snice = (function (exports) {
|
|
|
2105
3561
|
function moved(options = {}) {
|
|
2106
3562
|
return function (originalMethod, context) {
|
|
2107
3563
|
const methodName = context.name;
|
|
3564
|
+
const initKey = `__moved_init_${methodName}`;
|
|
2108
3565
|
context.addInitializer(function () {
|
|
2109
3566
|
const constructor = this.constructor;
|
|
3567
|
+
if (constructor[initKey])
|
|
3568
|
+
return;
|
|
3569
|
+
constructor[initKey] = true;
|
|
2110
3570
|
if (!constructor[MOVED_HANDLERS]) {
|
|
2111
3571
|
constructor[MOVED_HANDLERS] = [];
|
|
2112
3572
|
}
|
|
@@ -2191,102 +3651,20 @@ var Snice = (function (exports) {
|
|
|
2191
3651
|
return function (...args) {
|
|
2192
3652
|
// Initialize timers storage if not present
|
|
2193
3653
|
if (!this[ADOPTED_TIMERS]) {
|
|
2194
|
-
this[ADOPTED_TIMERS] = new Map();
|
|
2195
|
-
}
|
|
2196
|
-
// Get or create timers for this specific method
|
|
2197
|
-
if (!this[ADOPTED_TIMERS].has(methodName)) {
|
|
2198
|
-
this[ADOPTED_TIMERS].set(methodName, {
|
|
2199
|
-
throttleTimer: null,
|
|
2200
|
-
debounceTimer: null,
|
|
2201
|
-
lastThrottleCall: 0
|
|
2202
|
-
});
|
|
2203
|
-
}
|
|
2204
|
-
const timers = this[ADOPTED_TIMERS].get(methodName);
|
|
2205
|
-
// Helper function to execute method
|
|
2206
|
-
const executeMethod = (...methodArgs) => {
|
|
2207
|
-
return originalMethod.apply(this, methodArgs);
|
|
2208
|
-
};
|
|
2209
|
-
const hasDebounce = options.debounce !== undefined && options.debounce > 0;
|
|
2210
|
-
const hasThrottle = options.throttle !== undefined && options.throttle > 0;
|
|
2211
|
-
// Handle timing based on priority: debounce > throttle > immediate
|
|
2212
|
-
switch (true) {
|
|
2213
|
-
case hasDebounce: {
|
|
2214
|
-
clearTimeout(timers.debounceTimer);
|
|
2215
|
-
timers.debounceTimer = setTimeout(() => executeMethod(...args), options.debounce);
|
|
2216
|
-
return undefined;
|
|
2217
|
-
}
|
|
2218
|
-
case hasThrottle: {
|
|
2219
|
-
const throttleMs = options.throttle;
|
|
2220
|
-
const now = Date.now();
|
|
2221
|
-
const canExecuteImmediately = timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= throttleMs;
|
|
2222
|
-
if (canExecuteImmediately) {
|
|
2223
|
-
timers.lastThrottleCall = now;
|
|
2224
|
-
return executeMethod(...args);
|
|
2225
|
-
}
|
|
2226
|
-
const hasScheduledTimer = !!timers.throttleTimer;
|
|
2227
|
-
if (!hasScheduledTimer) {
|
|
2228
|
-
const remainingTime = throttleMs - (now - timers.lastThrottleCall);
|
|
2229
|
-
timers.throttleTimer = setTimeout(() => {
|
|
2230
|
-
timers.throttleTimer = null;
|
|
2231
|
-
timers.lastThrottleCall = Date.now();
|
|
2232
|
-
executeMethod(...args);
|
|
2233
|
-
}, remainingTime);
|
|
2234
|
-
}
|
|
2235
|
-
return undefined;
|
|
2236
|
-
}
|
|
2237
|
-
default:
|
|
2238
|
-
return executeMethod(...args);
|
|
2239
|
-
}
|
|
2240
|
-
};
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
|
-
/**
|
|
2244
|
-
* Decorator for methods that render specific parts of the template
|
|
2245
|
-
* Parts are identified by the 'part' attribute in the HTML template
|
|
2246
|
-
* When the decorated method is called, it automatically re-renders its part
|
|
2247
|
-
*/
|
|
2248
|
-
function part(partName, options = {}) {
|
|
2249
|
-
return function (originalMethod, context) {
|
|
2250
|
-
const methodName = context.name;
|
|
2251
|
-
context.addInitializer(function () {
|
|
2252
|
-
const constructor = this.constructor;
|
|
2253
|
-
if (!constructor[PARTS]) {
|
|
2254
|
-
constructor[PARTS] = new Map();
|
|
2255
|
-
}
|
|
2256
|
-
constructor[PARTS].set(partName, {
|
|
2257
|
-
methodName,
|
|
2258
|
-
method: originalMethod
|
|
2259
|
-
});
|
|
2260
|
-
});
|
|
2261
|
-
// Return wrapped method that automatically re-renders the part when called
|
|
2262
|
-
return function (...args) {
|
|
2263
|
-
// Initialize timers storage if not present
|
|
2264
|
-
if (!this[PART_TIMERS]) {
|
|
2265
|
-
this[PART_TIMERS] = new Map();
|
|
3654
|
+
this[ADOPTED_TIMERS] = new Map();
|
|
2266
3655
|
}
|
|
2267
|
-
// Get or create timers for this specific
|
|
2268
|
-
if (!this[
|
|
2269
|
-
this[
|
|
3656
|
+
// Get or create timers for this specific method
|
|
3657
|
+
if (!this[ADOPTED_TIMERS].has(methodName)) {
|
|
3658
|
+
this[ADOPTED_TIMERS].set(methodName, {
|
|
2270
3659
|
throttleTimer: null,
|
|
2271
3660
|
debounceTimer: null,
|
|
2272
3661
|
lastThrottleCall: 0
|
|
2273
3662
|
});
|
|
2274
3663
|
}
|
|
2275
|
-
const timers = this[
|
|
2276
|
-
// Helper function to execute method
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
const updateDOM = (content) => {
|
|
2280
|
-
const hasContent = content !== undefined;
|
|
2281
|
-
const hasElement = this.shadowRoot?.querySelector(`[part="${partName}"]`);
|
|
2282
|
-
if (hasContent && hasElement) {
|
|
2283
|
-
hasElement.innerHTML = content;
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2286
|
-
const isPromise = result instanceof Promise;
|
|
2287
|
-
return isPromise
|
|
2288
|
-
? result.then(content => { updateDOM(content); return content; })
|
|
2289
|
-
: (updateDOM(result), result);
|
|
3664
|
+
const timers = this[ADOPTED_TIMERS].get(methodName);
|
|
3665
|
+
// Helper function to execute method
|
|
3666
|
+
const executeMethod = (...methodArgs) => {
|
|
3667
|
+
return originalMethod.apply(this, methodArgs);
|
|
2290
3668
|
};
|
|
2291
3669
|
const hasDebounce = options.debounce !== undefined && options.debounce > 0;
|
|
2292
3670
|
const hasThrottle = options.throttle !== undefined && options.throttle > 0;
|
|
@@ -2294,7 +3672,7 @@ var Snice = (function (exports) {
|
|
|
2294
3672
|
switch (true) {
|
|
2295
3673
|
case hasDebounce: {
|
|
2296
3674
|
clearTimeout(timers.debounceTimer);
|
|
2297
|
-
timers.debounceTimer = setTimeout(() =>
|
|
3675
|
+
timers.debounceTimer = setTimeout(() => executeMethod(...args), options.debounce);
|
|
2298
3676
|
return undefined;
|
|
2299
3677
|
}
|
|
2300
3678
|
case hasThrottle: {
|
|
@@ -2303,7 +3681,7 @@ var Snice = (function (exports) {
|
|
|
2303
3681
|
const canExecuteImmediately = timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= throttleMs;
|
|
2304
3682
|
if (canExecuteImmediately) {
|
|
2305
3683
|
timers.lastThrottleCall = now;
|
|
2306
|
-
return
|
|
3684
|
+
return executeMethod(...args);
|
|
2307
3685
|
}
|
|
2308
3686
|
const hasScheduledTimer = !!timers.throttleTimer;
|
|
2309
3687
|
if (!hasScheduledTimer) {
|
|
@@ -2311,17 +3689,19 @@ var Snice = (function (exports) {
|
|
|
2311
3689
|
timers.throttleTimer = setTimeout(() => {
|
|
2312
3690
|
timers.throttleTimer = null;
|
|
2313
3691
|
timers.lastThrottleCall = Date.now();
|
|
2314
|
-
|
|
3692
|
+
executeMethod(...args);
|
|
2315
3693
|
}, remainingTime);
|
|
2316
3694
|
}
|
|
2317
3695
|
return undefined;
|
|
2318
3696
|
}
|
|
2319
3697
|
default:
|
|
2320
|
-
return
|
|
3698
|
+
return executeMethod(...args);
|
|
2321
3699
|
}
|
|
2322
3700
|
};
|
|
2323
3701
|
};
|
|
2324
3702
|
}
|
|
3703
|
+
// @part decorator removed in v3.0.0
|
|
3704
|
+
// Use @render with differential rendering instead
|
|
2325
3705
|
|
|
2326
3706
|
/*!
|
|
2327
3707
|
* pica-route v1.1.2
|
|
@@ -3072,6 +4452,8 @@ var Snice = (function (exports) {
|
|
|
3072
4452
|
let currentLayoutName = null; // Track current layout name
|
|
3073
4453
|
let currentLayoutTimestamp = null; // Track current layout timestamp
|
|
3074
4454
|
const context = options.context || {}; // Store context for guards
|
|
4455
|
+
// Create Context instance for managing router state
|
|
4456
|
+
const navigationContext = new Context(context, [], '', {});
|
|
3075
4457
|
function getCurrentLayoutElement(target) {
|
|
3076
4458
|
const noCurrentLayout = !currentLayoutName || !currentLayoutTimestamp;
|
|
3077
4459
|
if (noCurrentLayout) {
|
|
@@ -3109,33 +4491,19 @@ var Snice = (function (exports) {
|
|
|
3109
4491
|
// Extend the connectedCallback to add router-specific functionality
|
|
3110
4492
|
const elementConnectedCallback = constructor.prototype.connectedCallback;
|
|
3111
4493
|
constructor.prototype.connectedCallback = function () {
|
|
4494
|
+
// Store the Context instance for @context decorated methods to access
|
|
4495
|
+
this[CONTEXT_HANDLER] = navigationContext;
|
|
3112
4496
|
// Call the element's connectedCallback first
|
|
3113
4497
|
elementConnectedCallback?.call(this);
|
|
3114
|
-
// Setup context request handler for nested elements
|
|
3115
|
-
const contextRequestHandler = (event) => {
|
|
3116
|
-
// Only respond if this element has context
|
|
3117
|
-
if (this[ROUTER_CONTEXT] !== undefined) {
|
|
3118
|
-
event.detail.context = this[ROUTER_CONTEXT];
|
|
3119
|
-
event.stopPropagation(); // Stop bubbling once context is provided
|
|
3120
|
-
}
|
|
3121
|
-
};
|
|
3122
|
-
this.addEventListener('@context/request', contextRequestHandler);
|
|
3123
|
-
// Store handler for cleanup
|
|
3124
|
-
this[CONTEXT_REQUEST_HANDLER] = contextRequestHandler;
|
|
3125
4498
|
};
|
|
3126
4499
|
// Extend the disconnectedCallback to clean up router-specific stuff
|
|
3127
4500
|
const elementDisconnectedCallback = constructor.prototype.disconnectedCallback;
|
|
3128
4501
|
constructor.prototype.disconnectedCallback = function () {
|
|
3129
4502
|
// Call element's disconnectedCallback first
|
|
3130
4503
|
elementDisconnectedCallback?.call(this);
|
|
3131
|
-
// Clean up context
|
|
3132
|
-
const handler = this[CONTEXT_REQUEST_HANDLER];
|
|
3133
|
-
if (handler) {
|
|
3134
|
-
this.removeEventListener('@context/request', handler);
|
|
3135
|
-
delete this[CONTEXT_REQUEST_HANDLER];
|
|
3136
|
-
}
|
|
3137
|
-
// Clean up context reference
|
|
4504
|
+
// Clean up context references
|
|
3138
4505
|
delete this[ROUTER_CONTEXT];
|
|
4506
|
+
delete this[CONTEXT_HANDLER];
|
|
3139
4507
|
};
|
|
3140
4508
|
// Define the custom element
|
|
3141
4509
|
customElements.define(pageOptions.tag, constructor);
|
|
@@ -3194,8 +4562,8 @@ var Snice = (function (exports) {
|
|
|
3194
4562
|
* initialize();
|
|
3195
4563
|
*/
|
|
3196
4564
|
function initialize() {
|
|
3197
|
-
const
|
|
3198
|
-
if (!
|
|
4565
|
+
const target = document.querySelector(options.target);
|
|
4566
|
+
if (!target) {
|
|
3199
4567
|
throw new Error(`Target element not found: ${options.target}`);
|
|
3200
4568
|
}
|
|
3201
4569
|
const needsSorting = !is_sorted;
|
|
@@ -3219,6 +4587,10 @@ var Snice = (function (exports) {
|
|
|
3219
4587
|
: placard;
|
|
3220
4588
|
});
|
|
3221
4589
|
}
|
|
4590
|
+
function emitContextUpdate(target, currentPath, routeParams) {
|
|
4591
|
+
// Update the navigation context and notify all registered elements
|
|
4592
|
+
navigationContext.update(context, placards, currentPath, routeParams);
|
|
4593
|
+
}
|
|
3222
4594
|
function updateLayout(layoutElement, currentPath, routeParams) {
|
|
3223
4595
|
// Check if layout implements the update method
|
|
3224
4596
|
if (typeof layoutElement.update === 'function') {
|
|
@@ -3301,6 +4673,7 @@ var Snice = (function (exports) {
|
|
|
3301
4673
|
}
|
|
3302
4674
|
const newPageElement = document.createElement(route.tag);
|
|
3303
4675
|
newPageElement[ROUTER_CONTEXT] = context;
|
|
4676
|
+
newPageElement[CONTEXT_HANDLER] = navigationContext;
|
|
3304
4677
|
const routeParams = params;
|
|
3305
4678
|
Object.keys(routeParams).forEach(key => newPageElement.setAttribute(key, routeParams[key]));
|
|
3306
4679
|
return { result: RouteResult.SUCCESS, element: newPageElement, transition: route.transition, layout: route.layout, routeParams };
|
|
@@ -3391,7 +4764,7 @@ var Snice = (function (exports) {
|
|
|
3391
4764
|
// Collect fresh placards before navigation
|
|
3392
4765
|
collectPlacards();
|
|
3393
4766
|
window.scrollTo(0, 0);
|
|
3394
|
-
const isHomePath = (path
|
|
4767
|
+
const isHomePath = (path?.trim() === '' || path === '/') && !!home;
|
|
3395
4768
|
if (isHomePath) {
|
|
3396
4769
|
const homeRoute = routes.find(r => r.route.match('/'));
|
|
3397
4770
|
const guardsAllowed = checkGuards(homeRoute?.guards, {}, target);
|
|
@@ -3405,11 +4778,15 @@ var Snice = (function (exports) {
|
|
|
3405
4778
|
const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
|
|
3406
4779
|
if (hasLayout) {
|
|
3407
4780
|
await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, {});
|
|
4781
|
+
emitContextUpdate(target, path, {});
|
|
3408
4782
|
return;
|
|
3409
4783
|
}
|
|
3410
4784
|
await renderDirect(target, element, finalTransition);
|
|
4785
|
+
emitContextUpdate(target, path, {});
|
|
3411
4786
|
return;
|
|
3412
4787
|
}
|
|
4788
|
+
if (!path)
|
|
4789
|
+
return;
|
|
3413
4790
|
const routeResult = resolveRoute(path, target);
|
|
3414
4791
|
const isGuardsFailed = routeResult.result === RouteResult.GUARDS_FAILED;
|
|
3415
4792
|
if (isGuardsFailed) {
|
|
@@ -3424,9 +4801,11 @@ var Snice = (function (exports) {
|
|
|
3424
4801
|
const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
|
|
3425
4802
|
if (hasLayout) {
|
|
3426
4803
|
await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, routeParams);
|
|
4804
|
+
emitContextUpdate(target, path, routeParams);
|
|
3427
4805
|
return;
|
|
3428
4806
|
}
|
|
3429
4807
|
await renderDirect(target, element, finalTransition);
|
|
4808
|
+
emitContextUpdate(target, path, routeParams);
|
|
3430
4809
|
return;
|
|
3431
4810
|
}
|
|
3432
4811
|
const { element, transition, layout } = create404Element();
|
|
@@ -3436,9 +4815,11 @@ var Snice = (function (exports) {
|
|
|
3436
4815
|
const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
|
|
3437
4816
|
if (hasLayout) {
|
|
3438
4817
|
await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, {});
|
|
4818
|
+
emitContextUpdate(target, path, {});
|
|
3439
4819
|
return;
|
|
3440
4820
|
}
|
|
3441
4821
|
await renderDirect(target, element, finalTransition);
|
|
4822
|
+
emitContextUpdate(target, path, {});
|
|
3442
4823
|
}
|
|
3443
4824
|
async function performTransition$1(container, oldElement, newElement, transition) {
|
|
3444
4825
|
return performTransition(container, oldElement, newElement, transition);
|
|
@@ -3451,29 +4832,599 @@ var Snice = (function (exports) {
|
|
|
3451
4832
|
};
|
|
3452
4833
|
}
|
|
3453
4834
|
|
|
4835
|
+
// @on decorator removed in v3.0.0 - use template event syntax instead: @click=${handler}
|
|
4836
|
+
/**
|
|
4837
|
+
* Decorator that automatically dispatches a custom event after a method is called.
|
|
4838
|
+
* The return value of the method becomes the event detail.
|
|
4839
|
+
*
|
|
4840
|
+
* @param eventName The name of the event to dispatch
|
|
4841
|
+
* @param options Optional configuration extending EventInit
|
|
4842
|
+
*/
|
|
4843
|
+
function dispatch(eventName, options) {
|
|
4844
|
+
return function (originalMethod, _context) {
|
|
4845
|
+
return function (...args) {
|
|
4846
|
+
// Create timing wrappers for dispatch (per-instance)
|
|
4847
|
+
if (!this[DISPATCH_TIMERS]) {
|
|
4848
|
+
this[DISPATCH_TIMERS] = new Map();
|
|
4849
|
+
}
|
|
4850
|
+
const timerKey = `${eventName}_${_context.name}`;
|
|
4851
|
+
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
4852
|
+
this[DISPATCH_TIMERS].set(timerKey, {
|
|
4853
|
+
debounceTimeout: null,
|
|
4854
|
+
throttleLastCall: 0,
|
|
4855
|
+
throttleTimeout: null
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
4859
|
+
// Call the original method with preserved this context
|
|
4860
|
+
const result = originalMethod.apply(this, args);
|
|
4861
|
+
// Helper to dispatch the event
|
|
4862
|
+
const doDispatch = (detail) => {
|
|
4863
|
+
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
4864
|
+
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
4865
|
+
return;
|
|
4866
|
+
}
|
|
4867
|
+
// Create event with spread operator for options
|
|
4868
|
+
const event = new CustomEvent(eventName, {
|
|
4869
|
+
bubbles: true, // Default to true for component events
|
|
4870
|
+
composed: true, // Allow crossing shadow DOM boundaries
|
|
4871
|
+
...options, // Spread all EventInit options
|
|
4872
|
+
detail
|
|
4873
|
+
});
|
|
4874
|
+
this.dispatchEvent(event);
|
|
4875
|
+
};
|
|
4876
|
+
// Helper to handle timed dispatch
|
|
4877
|
+
const timedDispatch = (detail) => {
|
|
4878
|
+
if (options?.debounce) {
|
|
4879
|
+
clearTimeout(timers.debounceTimeout);
|
|
4880
|
+
timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
|
|
4881
|
+
}
|
|
4882
|
+
else if (options?.throttle) {
|
|
4883
|
+
const now = Date.now();
|
|
4884
|
+
const remaining = options.throttle - (now - timers.throttleLastCall);
|
|
4885
|
+
if (remaining <= 0) {
|
|
4886
|
+
clearTimeout(timers.throttleTimeout);
|
|
4887
|
+
timers.throttleLastCall = now;
|
|
4888
|
+
doDispatch(detail);
|
|
4889
|
+
}
|
|
4890
|
+
else if (!timers.throttleTimeout) {
|
|
4891
|
+
timers.throttleTimeout = setTimeout(() => {
|
|
4892
|
+
timers.throttleLastCall = Date.now();
|
|
4893
|
+
timers.throttleTimeout = null;
|
|
4894
|
+
doDispatch(detail);
|
|
4895
|
+
}, remaining);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
else {
|
|
4899
|
+
doDispatch(detail);
|
|
4900
|
+
}
|
|
4901
|
+
};
|
|
4902
|
+
// Handle async methods
|
|
4903
|
+
if (result instanceof Promise) {
|
|
4904
|
+
return result.then((resolvedResult) => {
|
|
4905
|
+
timedDispatch(resolvedResult);
|
|
4906
|
+
return resolvedResult;
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
// Sync method
|
|
4910
|
+
timedDispatch(result);
|
|
4911
|
+
return result;
|
|
4912
|
+
};
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
/**
|
|
4917
|
+
* Custom element readiness utilities
|
|
4918
|
+
* Handles waiting for custom elements to be defined with timeout warnings
|
|
4919
|
+
*/
|
|
4920
|
+
/**
|
|
4921
|
+
* Global flag to disable custom element readiness timeout warnings
|
|
4922
|
+
* Set this to true in environments where slow element registration is expected
|
|
4923
|
+
*/
|
|
4924
|
+
let DISABLE_ELEMENT_READY_WARNINGS = false;
|
|
4925
|
+
/**
|
|
4926
|
+
* Set whether to disable custom element readiness timeout warnings
|
|
4927
|
+
*/
|
|
4928
|
+
function setDisableElementReadyWarnings(value) {
|
|
4929
|
+
DISABLE_ELEMENT_READY_WARNINGS = value;
|
|
4930
|
+
}
|
|
4931
|
+
/**
|
|
4932
|
+
* Default timeout for custom element registration warning (500ms)
|
|
4933
|
+
*/
|
|
4934
|
+
const DEFAULT_WARNING_TIMEOUT = 500;
|
|
4935
|
+
/**
|
|
4936
|
+
* Wait for a custom element to be defined
|
|
4937
|
+
* Logs a warning if it takes longer than the warning timeout
|
|
4938
|
+
*
|
|
4939
|
+
* @param tagName - The custom element tag name
|
|
4940
|
+
* @param warningTimeout - Time in ms before warning (default 500ms)
|
|
4941
|
+
* @returns Promise that resolves when element is defined
|
|
4942
|
+
*/
|
|
4943
|
+
async function waitForElementDefined(tagName, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
|
|
4944
|
+
// If already defined, return immediately
|
|
4945
|
+
if (customElements.get(tagName)) {
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
// Set up warning timer if not disabled
|
|
4949
|
+
let warningTimer = null;
|
|
4950
|
+
if (!DISABLE_ELEMENT_READY_WARNINGS) {
|
|
4951
|
+
warningTimer = setTimeout(() => {
|
|
4952
|
+
console.warn(`Custom element <${tagName}> is taking longer than ${warningTimeout}ms to register. ` +
|
|
4953
|
+
`This may indicate a missing import or circular dependency. ` +
|
|
4954
|
+
`Set DISABLE_ELEMENT_READY_WARNINGS=true to disable this warning.`);
|
|
4955
|
+
}, warningTimeout);
|
|
4956
|
+
}
|
|
4957
|
+
try {
|
|
4958
|
+
// Wait for element to be defined
|
|
4959
|
+
await customElements.whenDefined(tagName);
|
|
4960
|
+
}
|
|
4961
|
+
finally {
|
|
4962
|
+
// Clear warning timer
|
|
4963
|
+
if (warningTimer) {
|
|
4964
|
+
clearTimeout(warningTimer);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
/**
|
|
4969
|
+
* Wait for a custom element to be defined and ready
|
|
4970
|
+
* First waits for the element to be registered, then waits for its ready promise
|
|
4971
|
+
*
|
|
4972
|
+
* @param element - The custom element instance
|
|
4973
|
+
* @param warningTimeout - Time in ms before warning about registration (default 500ms)
|
|
4974
|
+
* @returns Promise that resolves when element is defined and ready
|
|
4975
|
+
*/
|
|
4976
|
+
async function waitForElementReady(element, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
|
|
4977
|
+
const tagName = element.tagName.toLowerCase();
|
|
4978
|
+
// Wait for element to be defined
|
|
4979
|
+
await waitForElementDefined(tagName, warningTimeout);
|
|
4980
|
+
// Wait for element's ready promise if it exists
|
|
4981
|
+
if ('ready' in element && typeof element.ready?.then === 'function') {
|
|
4982
|
+
await element.ready;
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
/**
|
|
4986
|
+
* Process all custom elements in a node tree and wait for them to be ready
|
|
4987
|
+
* This is useful after inserting a template with custom elements
|
|
4988
|
+
*
|
|
4989
|
+
* @param node - The root node to scan for custom elements
|
|
4990
|
+
* @param warningTimeout - Time in ms before warning about registration (default 500ms)
|
|
4991
|
+
* @returns Promise that resolves when all custom elements are ready
|
|
4992
|
+
*/
|
|
4993
|
+
async function waitForAllCustomElements(node, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
|
|
4994
|
+
const customElements = [];
|
|
4995
|
+
// Find all custom elements (tag names with hyphens)
|
|
4996
|
+
if (node instanceof Element) {
|
|
4997
|
+
if (node.tagName.includes('-')) {
|
|
4998
|
+
customElements.push(node);
|
|
4999
|
+
}
|
|
5000
|
+
// Also check in shadow DOM
|
|
5001
|
+
if (node.shadowRoot) {
|
|
5002
|
+
const shadowCustomElements = node.shadowRoot.querySelectorAll('*');
|
|
5003
|
+
shadowCustomElements.forEach(el => {
|
|
5004
|
+
if (el.tagName.includes('-')) {
|
|
5005
|
+
customElements.push(el);
|
|
5006
|
+
}
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
// If it's a DocumentFragment, check all children
|
|
5011
|
+
if (node instanceof DocumentFragment) {
|
|
5012
|
+
const elements = node.querySelectorAll('*');
|
|
5013
|
+
elements.forEach(el => {
|
|
5014
|
+
if (el.tagName.includes('-')) {
|
|
5015
|
+
customElements.push(el);
|
|
5016
|
+
}
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
// Wait for all custom elements to be ready
|
|
5020
|
+
await Promise.all(customElements.map(el => waitForElementReady(el, warningTimeout)));
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
/**
|
|
5024
|
+
* Render debugging utilities for Snice v3.0.0
|
|
5025
|
+
* For testing and debugging only - not recommended for production use
|
|
5026
|
+
*/
|
|
5027
|
+
/**
|
|
5028
|
+
* Track renders of an element using an async generator
|
|
5029
|
+
* Each call to tracker.next() waits for the next render to complete
|
|
5030
|
+
*
|
|
5031
|
+
* @example
|
|
5032
|
+
* ```typescript
|
|
5033
|
+
* const tracker = trackRenders(element);
|
|
5034
|
+
*
|
|
5035
|
+
* element.someProp = 'new value';
|
|
5036
|
+
* await tracker.next(); // Waits for render
|
|
5037
|
+
* // DOM is now updated
|
|
5038
|
+
*
|
|
5039
|
+
* element.someProp = 'another value';
|
|
5040
|
+
* await tracker.next(); // Waits for next render
|
|
5041
|
+
* // DOM is updated again
|
|
5042
|
+
* ```
|
|
5043
|
+
*
|
|
5044
|
+
* WARNING: For testing/debugging only!
|
|
5045
|
+
* - Do not use in production code
|
|
5046
|
+
* - The generator yields indefinitely - use it only in controlled test environments
|
|
5047
|
+
* - Each yield waits for the next render event
|
|
5048
|
+
*/
|
|
5049
|
+
async function* trackRenders(element) {
|
|
5050
|
+
while (true) {
|
|
5051
|
+
await new Promise(resolve => {
|
|
5052
|
+
if (!element[RENDER_CALLBACKS]) {
|
|
5053
|
+
element[RENDER_CALLBACKS] = [];
|
|
5054
|
+
}
|
|
5055
|
+
element[RENDER_CALLBACKS].push(resolve);
|
|
5056
|
+
});
|
|
5057
|
+
yield;
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
|
|
5061
|
+
/**
|
|
5062
|
+
* Method decorators for common patterns
|
|
5063
|
+
* @debounce, @throttle, @once, @memoize
|
|
5064
|
+
*/
|
|
5065
|
+
const DEBOUNCE_TIMERS = getSymbol('debounce-timers');
|
|
5066
|
+
const THROTTLE_TIMERS = getSymbol('throttle-timers');
|
|
5067
|
+
const ONCE_CALLED = getSymbol('once-called');
|
|
5068
|
+
const MEMOIZE_CACHE = getSymbol('memoize-cache');
|
|
5069
|
+
/**
|
|
5070
|
+
* @debounce decorator - delays function execution until after wait time has elapsed
|
|
5071
|
+
* since the last invocation
|
|
5072
|
+
*
|
|
5073
|
+
* @param wait - Time to wait in milliseconds (default: 300)
|
|
5074
|
+
* @param options - Debounce options
|
|
5075
|
+
* @param options.leading - Invoke on the leading edge (default: false)
|
|
5076
|
+
* @param options.trailing - Invoke on the trailing edge (default: true)
|
|
5077
|
+
* @param options.maxWait - Maximum time to wait before invoking (default: undefined)
|
|
5078
|
+
*
|
|
5079
|
+
* @example
|
|
5080
|
+
* ```typescript
|
|
5081
|
+
* @element('search-input')
|
|
5082
|
+
* class SearchInput extends HTMLElement {
|
|
5083
|
+
* @debounce(500)
|
|
5084
|
+
* handleSearch(query: string) {
|
|
5085
|
+
* // Only called 500ms after last keystroke
|
|
5086
|
+
* fetch(`/api/search?q=${query}`);
|
|
5087
|
+
* }
|
|
5088
|
+
* }
|
|
5089
|
+
* ```
|
|
5090
|
+
*/
|
|
5091
|
+
function debounce(wait = 300, options = {}) {
|
|
5092
|
+
const { leading = false, trailing = true, maxWait } = options;
|
|
5093
|
+
return function (originalMethod, context) {
|
|
5094
|
+
const methodName = context.name;
|
|
5095
|
+
return function (...args) {
|
|
5096
|
+
if (!this[DEBOUNCE_TIMERS]) {
|
|
5097
|
+
this[DEBOUNCE_TIMERS] = {};
|
|
5098
|
+
}
|
|
5099
|
+
const timers = this[DEBOUNCE_TIMERS];
|
|
5100
|
+
const timerKey = methodName;
|
|
5101
|
+
// Clear existing timer
|
|
5102
|
+
if (timers[timerKey]) {
|
|
5103
|
+
clearTimeout(timers[timerKey].timeout);
|
|
5104
|
+
}
|
|
5105
|
+
// Track when debounce started for maxWait
|
|
5106
|
+
const now = Date.now();
|
|
5107
|
+
const isFirstCall = !timers[timerKey];
|
|
5108
|
+
const startTime = isFirstCall ? now : timers[timerKey].startTime;
|
|
5109
|
+
// Check if maxWait exceeded
|
|
5110
|
+
const shouldInvokeFromMaxWait = maxWait !== undefined && now - startTime >= maxWait;
|
|
5111
|
+
// Leading edge invocation
|
|
5112
|
+
if (leading && isFirstCall) {
|
|
5113
|
+
const result = originalMethod.apply(this, args);
|
|
5114
|
+
timers[timerKey] = {
|
|
5115
|
+
timeout: null,
|
|
5116
|
+
startTime,
|
|
5117
|
+
lastArgs: args,
|
|
5118
|
+
};
|
|
5119
|
+
return result;
|
|
5120
|
+
}
|
|
5121
|
+
// Set new timer for trailing edge
|
|
5122
|
+
const timeout = setTimeout(() => {
|
|
5123
|
+
if (trailing || shouldInvokeFromMaxWait) {
|
|
5124
|
+
originalMethod.apply(this, timers[timerKey].lastArgs);
|
|
5125
|
+
}
|
|
5126
|
+
delete timers[timerKey];
|
|
5127
|
+
}, shouldInvokeFromMaxWait ? 0 : wait);
|
|
5128
|
+
timers[timerKey] = {
|
|
5129
|
+
timeout,
|
|
5130
|
+
startTime,
|
|
5131
|
+
lastArgs: args,
|
|
5132
|
+
};
|
|
5133
|
+
};
|
|
5134
|
+
};
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* @throttle decorator - ensures function is called at most once per specified time period
|
|
5138
|
+
*
|
|
5139
|
+
* @param wait - Time to wait in milliseconds (default: 300)
|
|
5140
|
+
* @param options - Throttle options
|
|
5141
|
+
* @param options.leading - Invoke on the leading edge (default: true)
|
|
5142
|
+
* @param options.trailing - Invoke on the trailing edge (default: true)
|
|
5143
|
+
*
|
|
5144
|
+
* @example
|
|
5145
|
+
* ```typescript
|
|
5146
|
+
* @element('scroll-tracker')
|
|
5147
|
+
* class ScrollTracker extends HTMLElement {
|
|
5148
|
+
* @throttle(100)
|
|
5149
|
+
* handleScroll(e: Event) {
|
|
5150
|
+
* // Called at most once every 100ms
|
|
5151
|
+
* this.updateScrollPosition();
|
|
5152
|
+
* }
|
|
5153
|
+
* }
|
|
5154
|
+
* ```
|
|
5155
|
+
*/
|
|
5156
|
+
function throttle(wait = 300, options = {}) {
|
|
5157
|
+
const { leading = true, trailing = true } = options;
|
|
5158
|
+
return function (originalMethod, context) {
|
|
5159
|
+
const methodName = context.name;
|
|
5160
|
+
return function (...args) {
|
|
5161
|
+
if (!this[THROTTLE_TIMERS]) {
|
|
5162
|
+
this[THROTTLE_TIMERS] = {};
|
|
5163
|
+
}
|
|
5164
|
+
const timers = this[THROTTLE_TIMERS];
|
|
5165
|
+
const timerKey = methodName;
|
|
5166
|
+
const now = Date.now();
|
|
5167
|
+
if (!timers[timerKey]) {
|
|
5168
|
+
// First call
|
|
5169
|
+
if (leading) {
|
|
5170
|
+
originalMethod.apply(this, args);
|
|
5171
|
+
}
|
|
5172
|
+
timers[timerKey] = {
|
|
5173
|
+
lastInvoke: now,
|
|
5174
|
+
timeout: null,
|
|
5175
|
+
lastArgs: args,
|
|
5176
|
+
};
|
|
5177
|
+
if (trailing && !leading) {
|
|
5178
|
+
// If no leading edge, set up trailing
|
|
5179
|
+
timers[timerKey].timeout = setTimeout(() => {
|
|
5180
|
+
originalMethod.apply(this, timers[timerKey].lastArgs);
|
|
5181
|
+
delete timers[timerKey];
|
|
5182
|
+
}, wait);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
else {
|
|
5186
|
+
// Subsequent calls
|
|
5187
|
+
const timeSinceLastInvoke = now - timers[timerKey].lastInvoke;
|
|
5188
|
+
// Update last args
|
|
5189
|
+
timers[timerKey].lastArgs = args;
|
|
5190
|
+
// Clear any pending trailing call
|
|
5191
|
+
if (timers[timerKey].timeout) {
|
|
5192
|
+
clearTimeout(timers[timerKey].timeout);
|
|
5193
|
+
}
|
|
5194
|
+
if (timeSinceLastInvoke >= wait) {
|
|
5195
|
+
// Enough time has passed, invoke immediately
|
|
5196
|
+
originalMethod.apply(this, args);
|
|
5197
|
+
timers[timerKey].lastInvoke = now;
|
|
5198
|
+
}
|
|
5199
|
+
else if (trailing) {
|
|
5200
|
+
// Set up trailing call
|
|
5201
|
+
const remaining = wait - timeSinceLastInvoke;
|
|
5202
|
+
timers[timerKey].timeout = setTimeout(() => {
|
|
5203
|
+
originalMethod.apply(this, timers[timerKey].lastArgs);
|
|
5204
|
+
timers[timerKey].lastInvoke = Date.now();
|
|
5205
|
+
timers[timerKey].timeout = null;
|
|
5206
|
+
}, remaining);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
};
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
/**
|
|
5213
|
+
* @once decorator - ensures function is only called once
|
|
5214
|
+
* Subsequent calls return the result of the first call
|
|
5215
|
+
*
|
|
5216
|
+
* @param perInstance - If true, function can be called once per instance (default: true)
|
|
5217
|
+
* If false, function can only be called once globally across all instances
|
|
5218
|
+
*
|
|
5219
|
+
* @example
|
|
5220
|
+
* ```typescript
|
|
5221
|
+
* @element('data-loader')
|
|
5222
|
+
* class DataLoader extends HTMLElement {
|
|
5223
|
+
* @once()
|
|
5224
|
+
* async loadData() {
|
|
5225
|
+
* // Only loads data once, even if called multiple times
|
|
5226
|
+
* const data = await fetch('/api/data');
|
|
5227
|
+
* return data.json();
|
|
5228
|
+
* }
|
|
5229
|
+
* }
|
|
5230
|
+
* ```
|
|
5231
|
+
*/
|
|
5232
|
+
function once(perInstance = true) {
|
|
5233
|
+
let globalCalled = false;
|
|
5234
|
+
let globalResult;
|
|
5235
|
+
return function (originalMethod, context) {
|
|
5236
|
+
const methodName = context.name;
|
|
5237
|
+
return function (...args) {
|
|
5238
|
+
if (perInstance) {
|
|
5239
|
+
// Per-instance tracking
|
|
5240
|
+
if (!this[ONCE_CALLED]) {
|
|
5241
|
+
this[ONCE_CALLED] = {};
|
|
5242
|
+
}
|
|
5243
|
+
if (!this[ONCE_CALLED][methodName]) {
|
|
5244
|
+
this[ONCE_CALLED][methodName] = {
|
|
5245
|
+
called: true,
|
|
5246
|
+
result: originalMethod.apply(this, args),
|
|
5247
|
+
};
|
|
5248
|
+
}
|
|
5249
|
+
return this[ONCE_CALLED][methodName].result;
|
|
5250
|
+
}
|
|
5251
|
+
else {
|
|
5252
|
+
// Global tracking
|
|
5253
|
+
if (!globalCalled) {
|
|
5254
|
+
globalCalled = true;
|
|
5255
|
+
globalResult = originalMethod.apply(this, args);
|
|
5256
|
+
}
|
|
5257
|
+
return globalResult;
|
|
5258
|
+
}
|
|
5259
|
+
};
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
5262
|
+
/**
|
|
5263
|
+
* @memoize decorator - caches function results based on arguments
|
|
5264
|
+
* Uses JSON.stringify for argument comparison by default
|
|
5265
|
+
*
|
|
5266
|
+
* @param options - Memoization options
|
|
5267
|
+
* @param options.keyGenerator - Custom function to generate cache key from arguments
|
|
5268
|
+
* @param options.maxSize - Maximum cache size (default: 100)
|
|
5269
|
+
* @param options.ttl - Time to live in milliseconds (default: undefined - no expiration)
|
|
5270
|
+
*
|
|
5271
|
+
* @example
|
|
5272
|
+
* ```typescript
|
|
5273
|
+
* @element('calculator')
|
|
5274
|
+
* class Calculator extends HTMLElement {
|
|
5275
|
+
* @memoize({ maxSize: 50 })
|
|
5276
|
+
* fibonacci(n: number): number {
|
|
5277
|
+
* // Results are cached, subsequent calls with same n are instant
|
|
5278
|
+
* if (n <= 1) return n;
|
|
5279
|
+
* return this.fibonacci(n - 1) + this.fibonacci(n - 2);
|
|
5280
|
+
* }
|
|
5281
|
+
* }
|
|
5282
|
+
* ```
|
|
5283
|
+
*/
|
|
5284
|
+
function memoize(options = {}) {
|
|
5285
|
+
const { keyGenerator = (...args) => JSON.stringify(args), maxSize = 100, ttl } = options;
|
|
5286
|
+
return function (originalMethod, context) {
|
|
5287
|
+
const methodName = context.name;
|
|
5288
|
+
return function (...args) {
|
|
5289
|
+
if (!this[MEMOIZE_CACHE]) {
|
|
5290
|
+
this[MEMOIZE_CACHE] = {};
|
|
5291
|
+
}
|
|
5292
|
+
if (!this[MEMOIZE_CACHE][methodName]) {
|
|
5293
|
+
this[MEMOIZE_CACHE][methodName] = new Map();
|
|
5294
|
+
}
|
|
5295
|
+
const cache = this[MEMOIZE_CACHE][methodName];
|
|
5296
|
+
const key = keyGenerator(...args);
|
|
5297
|
+
// Check if cached
|
|
5298
|
+
if (cache.has(key)) {
|
|
5299
|
+
const cached = cache.get(key);
|
|
5300
|
+
// Check TTL
|
|
5301
|
+
if (ttl !== undefined) {
|
|
5302
|
+
const age = Date.now() - cached.timestamp;
|
|
5303
|
+
if (age > ttl) {
|
|
5304
|
+
cache.delete(key);
|
|
5305
|
+
}
|
|
5306
|
+
else {
|
|
5307
|
+
return cached.value;
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
else {
|
|
5311
|
+
return cached.value;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
// Compute result
|
|
5315
|
+
const result = originalMethod.apply(this, args);
|
|
5316
|
+
// Store in cache
|
|
5317
|
+
cache.set(key, {
|
|
5318
|
+
value: result,
|
|
5319
|
+
timestamp: Date.now(),
|
|
5320
|
+
});
|
|
5321
|
+
// Enforce max size (LRU - delete oldest)
|
|
5322
|
+
if (cache.size > maxSize) {
|
|
5323
|
+
const firstKey = cache.keys().next().value;
|
|
5324
|
+
cache.delete(firstKey);
|
|
5325
|
+
}
|
|
5326
|
+
return result;
|
|
5327
|
+
};
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* Clear all debounce timers for an instance
|
|
5332
|
+
* Useful in cleanup/disconnectedCallback
|
|
5333
|
+
*/
|
|
5334
|
+
function clearDebounceTimers(instance) {
|
|
5335
|
+
if (instance[DEBOUNCE_TIMERS]) {
|
|
5336
|
+
for (const timerKey in instance[DEBOUNCE_TIMERS]) {
|
|
5337
|
+
if (instance[DEBOUNCE_TIMERS][timerKey]?.timeout) {
|
|
5338
|
+
clearTimeout(instance[DEBOUNCE_TIMERS][timerKey].timeout);
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
instance[DEBOUNCE_TIMERS] = {};
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Clear all throttle timers for an instance
|
|
5346
|
+
* Useful in cleanup/disconnectedCallback
|
|
5347
|
+
*/
|
|
5348
|
+
function clearThrottleTimers(instance) {
|
|
5349
|
+
if (instance[THROTTLE_TIMERS]) {
|
|
5350
|
+
for (const timerKey in instance[THROTTLE_TIMERS]) {
|
|
5351
|
+
if (instance[THROTTLE_TIMERS][timerKey]?.timeout) {
|
|
5352
|
+
clearTimeout(instance[THROTTLE_TIMERS][timerKey].timeout);
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
instance[THROTTLE_TIMERS] = {};
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
/**
|
|
5359
|
+
* Clear memoize cache for an instance
|
|
5360
|
+
*/
|
|
5361
|
+
function clearMemoizeCache(instance, methodName) {
|
|
5362
|
+
if (instance[MEMOIZE_CACHE]) {
|
|
5363
|
+
if (methodName) {
|
|
5364
|
+
delete instance[MEMOIZE_CACHE][methodName];
|
|
5365
|
+
}
|
|
5366
|
+
else {
|
|
5367
|
+
instance[MEMOIZE_CACHE] = {};
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
/**
|
|
5372
|
+
* Reset once-called state for an instance
|
|
5373
|
+
*/
|
|
5374
|
+
function resetOnce(instance, methodName) {
|
|
5375
|
+
if (instance[ONCE_CALLED]) {
|
|
5376
|
+
if (methodName) {
|
|
5377
|
+
delete instance[ONCE_CALLED][methodName];
|
|
5378
|
+
}
|
|
5379
|
+
else {
|
|
5380
|
+
instance[ONCE_CALLED] = {};
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5385
|
+
exports.Context = Context;
|
|
3454
5386
|
exports.IS_CONTROLLER_INSTANCE = IS_CONTROLLER_INSTANCE;
|
|
3455
5387
|
exports.Router = Router;
|
|
3456
5388
|
exports.SimpleArray = SimpleArray;
|
|
3457
5389
|
exports.adopted = adopted;
|
|
3458
5390
|
exports.applyElementFunctionality = applyElementFunctionality;
|
|
3459
|
-
exports.
|
|
5391
|
+
exports.clearDebounceTimers = clearDebounceTimers;
|
|
5392
|
+
exports.clearMemoizeCache = clearMemoizeCache;
|
|
5393
|
+
exports.clearThrottleTimers = clearThrottleTimers;
|
|
5394
|
+
exports.context = context$1;
|
|
5395
|
+
exports.contextProperty = context;
|
|
3460
5396
|
exports.controller = controller;
|
|
5397
|
+
exports.css = css;
|
|
5398
|
+
exports.debounce = debounce;
|
|
3461
5399
|
exports.dispatch = dispatch;
|
|
3462
5400
|
exports.dispose = dispose;
|
|
3463
5401
|
exports.element = element;
|
|
3464
5402
|
exports.getSymbol = getSymbol;
|
|
5403
|
+
exports.html = html;
|
|
3465
5404
|
exports.layout = layout;
|
|
5405
|
+
exports.memoize = memoize;
|
|
3466
5406
|
exports.moved = moved;
|
|
5407
|
+
exports.nothing = nothing;
|
|
3467
5408
|
exports.observe = observe;
|
|
3468
5409
|
exports.on = on;
|
|
3469
|
-
exports.
|
|
5410
|
+
exports.once = once;
|
|
3470
5411
|
exports.property = property;
|
|
3471
5412
|
exports.query = query;
|
|
3472
5413
|
exports.queryAll = queryAll;
|
|
3473
5414
|
exports.ready = ready;
|
|
5415
|
+
exports.render = render;
|
|
3474
5416
|
exports.request = request;
|
|
5417
|
+
exports.resetOnce = resetOnce;
|
|
3475
5418
|
exports.respond = respond;
|
|
5419
|
+
exports.setDisableElementReadyWarnings = setDisableElementReadyWarnings;
|
|
5420
|
+
exports.styles = styles;
|
|
5421
|
+
exports.throttle = throttle;
|
|
5422
|
+
exports.trackRenders = trackRenders;
|
|
5423
|
+
exports.unsafeHTML = unsafeHTML;
|
|
3476
5424
|
exports.useNativeElementControllers = useNativeElementControllers;
|
|
5425
|
+
exports.waitForAllCustomElements = waitForAllCustomElements;
|
|
5426
|
+
exports.waitForElementDefined = waitForElementDefined;
|
|
5427
|
+
exports.waitForElementReady = waitForElementReady;
|
|
3477
5428
|
exports.watch = watch;
|
|
3478
5429
|
|
|
3479
5430
|
return exports;
|