snice 7.5.0 → 7.7.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/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/avatar.d.ts +1 -0
- package/adapters/react/avatar.d.ts.map +1 -1
- package/adapters/react/avatar.js +1 -1
- package/adapters/react/avatar.js.map +1 -1
- package/adapters/react/avatar.tsx +2 -1
- package/adapters/react/calendar.d.ts +4 -0
- package/adapters/react/calendar.d.ts.map +1 -1
- package/adapters/react/calendar.js +1 -1
- package/adapters/react/calendar.js.map +1 -1
- package/adapters/react/calendar.tsx +5 -1
- package/adapters/react/matchRoute.js +68 -2
- package/adapters/react/matchRoute.js.map +1 -1
- package/adapters/react/route-match.d.ts +7 -0
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +60 -2
- package/bin/project-analyzer.js +1262 -118
- package/bin/snice.js +95 -5
- package/custom-elements.json +112 -0
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/README.md +1 -1
- package/dist/cdn/avatar/snice-avatar.js +9 -3
- package/dist/cdn/avatar/snice-avatar.js.map +1 -1
- package/dist/cdn/avatar/snice-avatar.min.js +2 -2
- package/dist/cdn/avatar/snice-avatar.min.js.map +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +2 -2
- package/dist/cdn/button/snice-button.js.map +1 -1
- package/dist/cdn/button/snice-button.min.js +1 -1
- package/dist/cdn/button/snice-button.min.js.map +1 -1
- package/dist/cdn/calendar/README.md +2 -2
- package/dist/cdn/calendar/snice-calendar.js +562 -40
- package/dist/cdn/calendar/snice-calendar.js.map +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +28 -1
- package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/README.md +1 -1
- package/dist/cdn/login/snice-login.js +2 -2
- package/dist/cdn/login/snice-login.js.map +1 -1
- package/dist/cdn/login/snice-login.min.js +1 -1
- package/dist/cdn/login/snice-login.min.js.map +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +706 -267
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +706 -267
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/snice-select.js +3 -1
- package/dist/cdn/select/snice-select.js.map +1 -1
- package/dist/cdn/select/snice-select.min.js +2 -2
- package/dist/cdn/select/snice-select.min.js.map +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/README.md +1 -1
- package/dist/cdn/switch/snice-switch.js +8 -4
- package/dist/cdn/switch/snice-switch.js.map +1 -1
- package/dist/cdn/switch/snice-switch.min.js +8 -4
- package/dist/cdn/switch/snice-switch.min.js.map +1 -1
- package/dist/cdn/table/README.md +2 -2
- package/dist/cdn/table/snice-table.esm.js +77 -5
- package/dist/cdn/table/snice-table.esm.js.map +1 -1
- package/dist/cdn/table/snice-table.esm.min.js +68 -47
- package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
- package/dist/cdn/table/snice-table.js +76 -4
- package/dist/cdn/table/snice-table.js.map +1 -1
- package/dist/cdn/table/snice-table.min.js +53 -32
- package/dist/cdn/table/snice-table.min.js.map +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/avatar/snice-avatar.d.ts +1 -0
- package/dist/components/avatar/snice-avatar.js +8 -2
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/avatar/snice-avatar.types.d.ts +2 -0
- package/dist/components/button/snice-button.js +1 -1
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/calendar/snice-calendar.d.ts +38 -1
- package/dist/components/calendar/snice-calendar.js +390 -40
- package/dist/components/calendar/snice-calendar.js.map +1 -1
- package/dist/components/calendar/snice-calendar.types.d.ts +63 -0
- package/dist/components/select/snice-select.js +2 -0
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/switch/snice-switch.js +7 -3
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +9 -0
- package/dist/components/table/snice-table.js +72 -2
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/table-keyboard.js +1 -1
- package/dist/components/table/table-keyboard.js.map +1 -1
- package/dist/index.cjs +703 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +703 -264
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +703 -264
- package/dist/index.iife.js.map +1 -1
- package/dist/parts.d.ts +14 -4
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.js +68 -2
- package/dist/react/matchRoute.js.map +1 -1
- package/dist/react/route-match.d.ts +7 -0
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/render-errors.d.ts +9 -0
- package/dist/route-match.d.ts +7 -0
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/template-marker.d.ts +5 -0
- package/dist/testing-dom.cjs +1 -1
- package/dist/testing-dom.esm.js +1 -1
- package/dist/testing.d.ts +3 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/dispatch-options.d.ts +5 -4
- package/dist/types/event-timing.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/on-options.d.ts +21 -6
- package/dist/utils.d.ts +3 -0
- package/docs/ai/DEVELOPMENT.md +1 -1
- package/docs/ai/api.md +7 -2
- package/docs/ai/cli.md +14 -0
- package/docs/ai/components/avatar.md +1 -0
- package/docs/ai/components/calendar.md +24 -1
- package/docs/ai/components/switch.md +3 -0
- package/docs/ai/components/table.md +1 -1
- package/docs/ai/decorators.md +3 -2
- package/docs/ai/events.md +45 -8
- package/docs/ai/rendering.md +7 -0
- package/docs/ai/routing.md +6 -1
- package/docs/ai/testing.md +17 -0
- package/docs/cli.md +19 -0
- package/docs/components/avatar.md +1 -0
- package/docs/components/calendar.md +112 -2
- package/docs/components/switch.md +7 -1
- package/docs/components/table.md +2 -1
- package/docs/events.md +90 -10
- package/docs/rendering.md +10 -0
- package/docs/routing.md +22 -1
- package/docs/testing.md +19 -0
- package/package.json +2 -2
- package/vscode.html-custom-data.json +28 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* snice v7.
|
|
2
|
+
* snice v7.6.0
|
|
3
3
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
4
4
|
* (c) 2024
|
|
5
5
|
* Released under the MIT License.
|
|
6
6
|
*/
|
|
7
7
|
/*!
|
|
8
|
-
* snice v7.
|
|
8
|
+
* snice v7.6.0
|
|
9
9
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
10
10
|
* (c) 2024
|
|
11
11
|
* Released under the MIT License.
|
|
@@ -720,6 +720,18 @@ function createThrottled(fn, delay) {
|
|
|
720
720
|
}
|
|
721
721
|
};
|
|
722
722
|
}
|
|
723
|
+
/** Resolve and validate an @on/@dispatch debounce or throttle interval. */
|
|
724
|
+
function resolveEventTiming(instance, timing, decorator, option) {
|
|
725
|
+
if (timing === undefined)
|
|
726
|
+
return undefined;
|
|
727
|
+
const value = typeof timing === 'function'
|
|
728
|
+
? timing.call(instance)
|
|
729
|
+
: timing;
|
|
730
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
731
|
+
throw new TypeError(`${decorator} ${option} must resolve to a finite, non-negative number of milliseconds.`);
|
|
732
|
+
}
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
723
735
|
/**
|
|
724
736
|
* Resolve a scope option to a concrete EventTarget.
|
|
725
737
|
*
|
|
@@ -1853,6 +1865,11 @@ function on(eventName, selectorOrOptions, options) {
|
|
|
1853
1865
|
else if (selectorOrOptions === null && options) {
|
|
1854
1866
|
opts = options;
|
|
1855
1867
|
}
|
|
1868
|
+
// `target` is the options-object spelling of the positional selector; fold it
|
|
1869
|
+
// into the same delegation path. The positional argument wins when both are given.
|
|
1870
|
+
if (!selector && opts.target) {
|
|
1871
|
+
selector = opts.target;
|
|
1872
|
+
}
|
|
1856
1873
|
return function (originalMethod, context) {
|
|
1857
1874
|
const methodName = context.name;
|
|
1858
1875
|
context.addInitializer(function () {
|
|
@@ -1864,7 +1881,7 @@ function on(eventName, selectorOrOptions, options) {
|
|
|
1864
1881
|
// and child registrations pollute parent (and vice versa) via the
|
|
1865
1882
|
// prototype chain.
|
|
1866
1883
|
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
1867
|
-
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}`;
|
|
1884
|
+
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}::light:${opts.light ?? ''}::shadow:${opts.shadow ?? ''}`;
|
|
1868
1885
|
if (!Object.prototype.hasOwnProperty.call(constructor, ON_METHODS)) {
|
|
1869
1886
|
constructor[ON_METHODS] = new Set();
|
|
1870
1887
|
}
|
|
@@ -1944,6 +1961,10 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1944
1961
|
const currentMethod = instance[handler.methodName];
|
|
1945
1962
|
let boundMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
|
|
1946
1963
|
const handlerOptions = handler.options || {};
|
|
1964
|
+
const debounceDelay = resolveEventTiming(instance, handlerOptions.debounce, '@on', 'debounce');
|
|
1965
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
1966
|
+
? undefined
|
|
1967
|
+
: resolveEventTiming(instance, handlerOptions.throttle, '@on', 'throttle');
|
|
1947
1968
|
// Parse event name for key modifiers
|
|
1948
1969
|
// Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
|
|
1949
1970
|
// Only parse colons for keyboard events, not custom events
|
|
@@ -1964,13 +1985,13 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1964
1985
|
if (delimiterIndex <= 0)
|
|
1965
1986
|
warnIfModifierMisuse(handler.eventName);
|
|
1966
1987
|
// Apply debounce (takes precedence over throttle)
|
|
1967
|
-
if (
|
|
1968
|
-
const debounced = createDebounced(boundMethod,
|
|
1988
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
1989
|
+
const debounced = createDebounced(boundMethod, debounceDelay);
|
|
1969
1990
|
instance[CLEANUP].eventCancels.push(() => debounced.cancel());
|
|
1970
1991
|
boundMethod = debounced;
|
|
1971
1992
|
}
|
|
1972
|
-
else if (
|
|
1973
|
-
boundMethod = throttle$1(boundMethod,
|
|
1993
|
+
else if (throttleDelay && throttleDelay > 0) {
|
|
1994
|
+
boundMethod = throttle$1(boundMethod, throttleDelay);
|
|
1974
1995
|
}
|
|
1975
1996
|
// Create event handler with key modifier support
|
|
1976
1997
|
// Uses shared keyboard filter implementation from parts.ts
|
|
@@ -1978,15 +1999,21 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1978
1999
|
if (keyModifier && ['keydown', 'keyup', 'keypress'].includes(baseEventName)) {
|
|
1979
2000
|
keyFilter = parseKeyboardFilter(keyModifier);
|
|
1980
2001
|
}
|
|
2002
|
+
// Returns whether the method actually ran, so `once` can be managed
|
|
2003
|
+
// manually: native `once` consumes the listener on ANY invocation — a
|
|
2004
|
+
// non-matching key or (with multiple tree listeners) a different root
|
|
2005
|
+
// would either eat the single firing or allow a second one.
|
|
1981
2006
|
const createKeyModifierHandler = (method) => {
|
|
1982
2007
|
if (!keyFilter) {
|
|
1983
|
-
return method;
|
|
2008
|
+
return (event) => { method(event); return true; };
|
|
1984
2009
|
}
|
|
1985
2010
|
return (event) => {
|
|
1986
2011
|
const keyEvent = event;
|
|
1987
2012
|
if (matchesKeyboardFilter(keyEvent, keyFilter)) {
|
|
1988
2013
|
method(event);
|
|
2014
|
+
return true;
|
|
1989
2015
|
}
|
|
2016
|
+
return false;
|
|
1990
2017
|
};
|
|
1991
2018
|
};
|
|
1992
2019
|
// Apply key modifier wrapper
|
|
@@ -2004,6 +2031,20 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2004
2031
|
continue;
|
|
2005
2032
|
}
|
|
2006
2033
|
const hasExplicitTarget = hasExplicitScope || hasDaemon;
|
|
2034
|
+
// Tree toggles — the same light/shadow pair @query uses. Both default to
|
|
2035
|
+
// true: direct listeners attach in both trees, delegated listeners match
|
|
2036
|
+
// in both. An explicit scope/daemon owns the listener target outright, so
|
|
2037
|
+
// the flags are meaningless there and ignored with a warning.
|
|
2038
|
+
const inShadow = handlerOptions.shadow !== false;
|
|
2039
|
+
const inLight = handlerOptions.light !== false;
|
|
2040
|
+
if (hasExplicitTarget
|
|
2041
|
+
&& (handlerOptions.light !== undefined || handlerOptions.shadow !== undefined)) {
|
|
2042
|
+
console.warn(`[snice/@on] light/shadow are ignored for "${handler.eventName}" — an explicit scope/daemon owns the listener target.`);
|
|
2043
|
+
}
|
|
2044
|
+
if (!hasExplicitTarget && !inShadow && !inLight) {
|
|
2045
|
+
console.warn(`[snice/@on] "${handler.eventName}" disables both light and shadow — listener skipped.`);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2007
2048
|
let scopedTarget = null;
|
|
2008
2049
|
if (hasDaemon) {
|
|
2009
2050
|
try {
|
|
@@ -2025,56 +2066,125 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2025
2066
|
}
|
|
2026
2067
|
// Main event handler with error handling and event delegation
|
|
2027
2068
|
if (handler.selector) {
|
|
2028
|
-
// Delegated event handling.
|
|
2029
|
-
// -
|
|
2030
|
-
// -
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
(
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
keyModifierMethod(event);
|
|
2049
|
-
}
|
|
2050
|
-
catch (error) {
|
|
2051
|
-
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2069
|
+
// Delegated event handling. Listener roots by tree toggle:
|
|
2070
|
+
// - shadow → the shadow root (when present)
|
|
2071
|
+
// - light → the host element (hears light-DOM bubbles)
|
|
2072
|
+
// - explicit scope: the scoped target only
|
|
2073
|
+
const componentShadowRoot = targetElement.shadowRoot;
|
|
2074
|
+
const delegationRoots = hasExplicitTarget
|
|
2075
|
+
? [scopedTarget]
|
|
2076
|
+
: [
|
|
2077
|
+
...(inShadow && componentShadowRoot ? [componentShadowRoot] : []),
|
|
2078
|
+
...(inLight ? [targetElement] : []),
|
|
2079
|
+
];
|
|
2080
|
+
if (delegationRoots.length === 0) {
|
|
2081
|
+
console.warn(`[snice/@on] "${handler.eventName}" cannot delegate "${handler.selector}" — no shadow root and light disabled; listener skipped.`);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
// One event can reach more than one root (e.g. a slotted click seen by
|
|
2085
|
+
// both the shadow root and the host); a per-handler Symbol keeps the
|
|
2086
|
+
// handler to one invocation per event. The symbol is only stamped on a
|
|
2087
|
+
// match so a non-matching root never blocks the other.
|
|
2088
|
+
const handledSymbol = Symbol();
|
|
2054
2089
|
// Auto-enable capture for non-bubbling events when using delegation
|
|
2055
2090
|
const needsCapture = NON_BUBBLING_EVENTS.has(baseEventName);
|
|
2056
2091
|
const useCapture = handlerOptions.capture !== undefined
|
|
2057
2092
|
? handlerOptions.capture
|
|
2058
2093
|
: needsCapture;
|
|
2094
|
+
// `once` is managed manually (never native): the handler fires exactly
|
|
2095
|
+
// once — a non-matching event or key must not consume it, and a match
|
|
2096
|
+
// must retire the listeners on EVERY root, not just the one that fired.
|
|
2059
2097
|
const listenerOptions = {
|
|
2060
2098
|
capture: useCapture,
|
|
2061
|
-
once:
|
|
2099
|
+
once: false,
|
|
2062
2100
|
passive: handlerOptions.passive || false,
|
|
2063
2101
|
};
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2102
|
+
const listenerGroup = [];
|
|
2103
|
+
const removeGroup = () => {
|
|
2104
|
+
for (const entry of listenerGroup) {
|
|
2105
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
for (const eventRoot of delegationRoots) {
|
|
2109
|
+
// Delegation matches only elements visible in the listener's own tree
|
|
2110
|
+
// scope: a shadow-root listener matches its shadow tree, anything else
|
|
2111
|
+
// matches its containing document/root. This mirrors native retargeting
|
|
2112
|
+
// (child-component internals never match) while still matching a shadow
|
|
2113
|
+
// wrapper when the click lands on light-DOM content slotted into it —
|
|
2114
|
+
// `target.closest()` alone cannot cross that slot boundary.
|
|
2115
|
+
const treeScope = eventRoot instanceof ShadowRoot
|
|
2116
|
+
? eventRoot
|
|
2117
|
+
: (eventRoot.getRootNode?.() ?? document);
|
|
2118
|
+
const findDelegateMatch = (event) => {
|
|
2119
|
+
if (typeof event.composedPath !== 'function') {
|
|
2120
|
+
// Fallback for environments without composedPath: ancestor walk.
|
|
2121
|
+
const target = event.target;
|
|
2122
|
+
return (target.matches && target.matches(handler.selector) && target)
|
|
2123
|
+
|| (target.closest && target.closest(handler.selector))
|
|
2124
|
+
|| null;
|
|
2125
|
+
}
|
|
2126
|
+
for (const node of event.composedPath()) {
|
|
2127
|
+
const el = node;
|
|
2128
|
+
if (el === eventRoot)
|
|
2129
|
+
break;
|
|
2130
|
+
if (el.nodeType !== 1)
|
|
2131
|
+
continue;
|
|
2132
|
+
if (el.getRootNode() !== treeScope)
|
|
2133
|
+
continue;
|
|
2134
|
+
if (el.matches(handler.selector))
|
|
2135
|
+
return el;
|
|
2136
|
+
}
|
|
2137
|
+
return null;
|
|
2138
|
+
};
|
|
2139
|
+
const delegatedHandler = (event) => {
|
|
2140
|
+
if (event[handledSymbol])
|
|
2141
|
+
return;
|
|
2142
|
+
const matchingElement = findDelegateMatch(event);
|
|
2143
|
+
if (!matchingElement)
|
|
2144
|
+
return;
|
|
2145
|
+
event[handledSymbol] = true;
|
|
2146
|
+
if (handlerOptions.preventDefault)
|
|
2147
|
+
event.preventDefault();
|
|
2148
|
+
if (handlerOptions.stopPropagation) {
|
|
2149
|
+
event.stopPropagation();
|
|
2150
|
+
event.stopImmediatePropagation();
|
|
2151
|
+
}
|
|
2152
|
+
let fired = false;
|
|
2153
|
+
try {
|
|
2154
|
+
fired = keyModifierMethod(event);
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
2157
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2158
|
+
fired = true;
|
|
2159
|
+
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2160
|
+
}
|
|
2161
|
+
if (fired && handlerOptions.once)
|
|
2162
|
+
removeGroup();
|
|
2163
|
+
};
|
|
2164
|
+
eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
2165
|
+
listenerGroup.push({ target: eventRoot, handler: delegatedHandler });
|
|
2166
|
+
instance[CLEANUP].events.push({
|
|
2167
|
+
target: eventRoot,
|
|
2168
|
+
eventName: baseEventName,
|
|
2169
|
+
handler: delegatedHandler,
|
|
2170
|
+
options: listenerOptions,
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2071
2173
|
}
|
|
2072
2174
|
else {
|
|
2073
2175
|
// Direct event handling.
|
|
2074
2176
|
// - Default: shadow root + host element (so events inside shadow and on
|
|
2075
2177
|
// the host itself both fire). A per-handler Symbol dedupes.
|
|
2178
|
+
// - light/shadow narrow the attachment: shadow → the shadow root
|
|
2179
|
+
// listener, light → the host listener.
|
|
2076
2180
|
// - With explicit scope: attach to the scoped target ONLY (no duplication).
|
|
2077
|
-
const shadowRoot = hasExplicitTarget
|
|
2181
|
+
const shadowRoot = (hasExplicitTarget || !inShadow)
|
|
2182
|
+
? null
|
|
2183
|
+
: targetElement.shadowRoot;
|
|
2184
|
+
if (!hasExplicitTarget && !shadowRoot && !inLight) {
|
|
2185
|
+
console.warn(`[snice/@on] "${handler.eventName}" requests shadow only but the element has no shadow root — listener skipped.`);
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2078
2188
|
// Per-handler private Symbol so dedup is scoped to THIS handler's two
|
|
2079
2189
|
// listeners (shadowRoot + host). Using Symbol.for() with the method name
|
|
2080
2190
|
// would collide across components that share a method name (e.g. a parent
|
|
@@ -2082,6 +2192,20 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2082
2192
|
// handler to be silently swallowed when events bubble up through a
|
|
2083
2193
|
// shadow boundary.
|
|
2084
2194
|
const handledSymbol = Symbol();
|
|
2195
|
+
// `once` is managed manually (never native): a key-filtered handler must
|
|
2196
|
+
// not be consumed by a non-matching key, and with listeners in two trees
|
|
2197
|
+
// the first real firing must retire BOTH, not just the one that fired.
|
|
2198
|
+
const listenerOptions = {
|
|
2199
|
+
capture: handlerOptions.capture || false,
|
|
2200
|
+
once: false,
|
|
2201
|
+
passive: handlerOptions.passive || false,
|
|
2202
|
+
};
|
|
2203
|
+
const listenerGroup = [];
|
|
2204
|
+
const removeGroup = () => {
|
|
2205
|
+
for (const entry of listenerGroup) {
|
|
2206
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2085
2209
|
const wrappedMethod = (event) => {
|
|
2086
2210
|
if (event[handledSymbol])
|
|
2087
2211
|
return;
|
|
@@ -2090,46 +2214,39 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2090
2214
|
event.preventDefault();
|
|
2091
2215
|
if (handlerOptions.stopPropagation)
|
|
2092
2216
|
event.stopPropagation();
|
|
2217
|
+
let fired = false;
|
|
2093
2218
|
try {
|
|
2094
|
-
keyModifierMethod(event);
|
|
2219
|
+
fired = keyModifierMethod(event);
|
|
2095
2220
|
}
|
|
2096
2221
|
catch (error) {
|
|
2222
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2223
|
+
fired = true;
|
|
2097
2224
|
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2098
2225
|
}
|
|
2226
|
+
if (fired && handlerOptions.once)
|
|
2227
|
+
removeGroup();
|
|
2099
2228
|
};
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
passive: handlerOptions.passive || false,
|
|
2104
|
-
};
|
|
2105
|
-
if (hasExplicitTarget) {
|
|
2106
|
-
scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2229
|
+
const attach = (target) => {
|
|
2230
|
+
target.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2231
|
+
listenerGroup.push({ target, handler: wrappedMethod });
|
|
2107
2232
|
instance[CLEANUP].events.push({
|
|
2108
|
-
target
|
|
2233
|
+
target,
|
|
2109
2234
|
eventName: baseEventName,
|
|
2110
2235
|
handler: wrappedMethod,
|
|
2111
2236
|
options: listenerOptions,
|
|
2112
2237
|
});
|
|
2238
|
+
};
|
|
2239
|
+
if (hasExplicitTarget) {
|
|
2240
|
+
attach(scopedTarget);
|
|
2113
2241
|
continue;
|
|
2114
2242
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
shadowRoot
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
options: listenerOptions,
|
|
2123
|
-
});
|
|
2124
|
-
}
|
|
2125
|
-
// Also listen on host element (for clicks on host itself or when no shadow root)
|
|
2126
|
-
targetElement.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2127
|
-
instance[CLEANUP].events.push({
|
|
2128
|
-
target: targetElement,
|
|
2129
|
-
eventName: baseEventName,
|
|
2130
|
-
handler: wrappedMethod,
|
|
2131
|
-
options: listenerOptions,
|
|
2132
|
-
});
|
|
2243
|
+
// Listen on shadow root for events inside shadow DOM
|
|
2244
|
+
if (shadowRoot)
|
|
2245
|
+
attach(shadowRoot);
|
|
2246
|
+
// Also listen on host element (for clicks on host itself, light-DOM
|
|
2247
|
+
// bubbles, or when no shadow root) — unless light is disabled.
|
|
2248
|
+
if (inLight)
|
|
2249
|
+
attach(targetElement);
|
|
2133
2250
|
}
|
|
2134
2251
|
}
|
|
2135
2252
|
}
|
|
@@ -2149,6 +2266,185 @@ function cleanupEventHandlers(instance) {
|
|
|
2149
2266
|
instance[CLEANUP].events = [];
|
|
2150
2267
|
}
|
|
2151
2268
|
|
|
2269
|
+
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
2270
|
+
const DISPATCH_GENERATION = getSymbol('dispatch-generation');
|
|
2271
|
+
function isDispatchGenerationActive(instance, generation) {
|
|
2272
|
+
return (instance[DISPATCH_GENERATION] ?? 0) === generation;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Decorator that automatically dispatches a custom event after a method is called.
|
|
2276
|
+
* The return value of the method becomes the event detail.
|
|
2277
|
+
*
|
|
2278
|
+
* @param eventName The name of the event to dispatch
|
|
2279
|
+
* @param options Optional configuration extending EventInit
|
|
2280
|
+
*/
|
|
2281
|
+
function dispatch(eventName, options) {
|
|
2282
|
+
return function (originalMethod, _context) {
|
|
2283
|
+
// Decorator-instance identity avoids collisions such as event `a_b` on
|
|
2284
|
+
// method `c` versus event `a` on method `b_c`.
|
|
2285
|
+
const timerKey = Symbol(`@dispatch:${eventName}`);
|
|
2286
|
+
return function (...args) {
|
|
2287
|
+
// Resolve against the actual decorated instance for every invocation.
|
|
2288
|
+
// Capture the generation before calling an async method so teardown can
|
|
2289
|
+
// invalidate work that has not reached the scheduling step yet.
|
|
2290
|
+
const generation = this[DISPATCH_GENERATION] ?? 0;
|
|
2291
|
+
// Create timing wrappers for dispatch (per-instance)
|
|
2292
|
+
if (!this[DISPATCH_TIMERS]) {
|
|
2293
|
+
this[DISPATCH_TIMERS] = new Map();
|
|
2294
|
+
}
|
|
2295
|
+
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
2296
|
+
this[DISPATCH_TIMERS].set(timerKey, {
|
|
2297
|
+
debounceTimeout: null,
|
|
2298
|
+
throttleLastCall: 0,
|
|
2299
|
+
throttleTimeout: null,
|
|
2300
|
+
invocation: 0,
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
2304
|
+
const debounceDelay = resolveEventTiming(this, options?.debounce, '@dispatch', 'debounce');
|
|
2305
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
2306
|
+
? undefined
|
|
2307
|
+
: resolveEventTiming(this, options?.throttle, '@dispatch', 'throttle');
|
|
2308
|
+
const invocation = ++timers.invocation;
|
|
2309
|
+
// A new invocation supersedes deferred work for this decorated method.
|
|
2310
|
+
// In particular, a resolver changing debounce to 0 must not leave the
|
|
2311
|
+
// previous positive-delay timer armed. Throttle trailing work is also
|
|
2312
|
+
// rebuilt below using this invocation's freshly resolved interval.
|
|
2313
|
+
if (timers.debounceTimeout)
|
|
2314
|
+
clearTimeout(timers.debounceTimeout);
|
|
2315
|
+
timers.debounceTimeout = null;
|
|
2316
|
+
if (timers.throttleTimeout)
|
|
2317
|
+
clearTimeout(timers.throttleTimeout);
|
|
2318
|
+
timers.throttleTimeout = null;
|
|
2319
|
+
timers.latestDetail = undefined;
|
|
2320
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2321
|
+
// Switching from throttle to debounce starts a new timing regime.
|
|
2322
|
+
timers.throttleLastCall = 0;
|
|
2323
|
+
}
|
|
2324
|
+
else if (!throttleDelay || throttleDelay <= 0) {
|
|
2325
|
+
// An un-timed invocation cancels both old regimes completely.
|
|
2326
|
+
timers.throttleLastCall = 0;
|
|
2327
|
+
}
|
|
2328
|
+
// Call the original method with preserved this context
|
|
2329
|
+
const result = originalMethod.apply(this, args);
|
|
2330
|
+
// Helper to dispatch the event
|
|
2331
|
+
const doDispatch = (detail) => {
|
|
2332
|
+
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
2333
|
+
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
2337
|
+
// not belong in the CustomEvent constructor dictionary.
|
|
2338
|
+
const event = new CustomEvent(eventName, {
|
|
2339
|
+
bubbles: options?.bubbles ?? true,
|
|
2340
|
+
cancelable: options?.cancelable ?? false,
|
|
2341
|
+
composed: options?.composed ?? true,
|
|
2342
|
+
detail
|
|
2343
|
+
});
|
|
2344
|
+
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
2345
|
+
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
2346
|
+
}
|
|
2347
|
+
if (options?.daemon !== undefined) {
|
|
2348
|
+
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (options?.scope !== undefined) {
|
|
2352
|
+
const target = resolveScope(this, options.scope);
|
|
2353
|
+
if (!target) {
|
|
2354
|
+
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
target.dispatchEvent(event);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
const target = defaultCommunicationTarget(this);
|
|
2361
|
+
if (!target) {
|
|
2362
|
+
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
2363
|
+
}
|
|
2364
|
+
target.dispatchEvent(event);
|
|
2365
|
+
};
|
|
2366
|
+
// Helper to handle timed dispatch
|
|
2367
|
+
const timedDispatch = (detail) => {
|
|
2368
|
+
if (!isDispatchGenerationActive(this, generation))
|
|
2369
|
+
return;
|
|
2370
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2371
|
+
timers.debounceTimeout = setTimeout(() => {
|
|
2372
|
+
timers.debounceTimeout = null;
|
|
2373
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2374
|
+
doDispatch(detail);
|
|
2375
|
+
}
|
|
2376
|
+
}, debounceDelay);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (!throttleDelay || throttleDelay <= 0) {
|
|
2380
|
+
doDispatch(detail);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const now = Date.now();
|
|
2384
|
+
const remaining = throttleDelay - (now - timers.throttleLastCall);
|
|
2385
|
+
if (remaining <= 0) {
|
|
2386
|
+
clearTimeout(timers.throttleTimeout);
|
|
2387
|
+
timers.throttleLastCall = now;
|
|
2388
|
+
doDispatch(detail);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
2392
|
+
// data, not the first-suppressed-call detail captured by closure.
|
|
2393
|
+
timers.latestDetail = detail;
|
|
2394
|
+
// Always schedule from the last actual dispatch using the interval
|
|
2395
|
+
// resolved for THIS invocation. A later invocation can therefore
|
|
2396
|
+
// lengthen or shorten the pending trailing window coherently.
|
|
2397
|
+
timers.throttleTimeout = setTimeout(() => {
|
|
2398
|
+
timers.throttleLastCall = Date.now();
|
|
2399
|
+
timers.throttleTimeout = null;
|
|
2400
|
+
const d = timers.latestDetail;
|
|
2401
|
+
timers.latestDetail = undefined;
|
|
2402
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2403
|
+
doDispatch(d);
|
|
2404
|
+
}
|
|
2405
|
+
}, remaining);
|
|
2406
|
+
};
|
|
2407
|
+
// Handle async methods
|
|
2408
|
+
if (result instanceof Promise) {
|
|
2409
|
+
return result.then((resolvedResult) => {
|
|
2410
|
+
const usesDeferredTiming = (debounceDelay ?? 0) > 0 || (throttleDelay ?? 0) > 0;
|
|
2411
|
+
if (isDispatchGenerationActive(this, generation)
|
|
2412
|
+
&& (!usesDeferredTiming || timers.invocation === invocation)) {
|
|
2413
|
+
timedDispatch(resolvedResult);
|
|
2414
|
+
}
|
|
2415
|
+
return resolvedResult;
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
// Sync method
|
|
2419
|
+
timedDispatch(result);
|
|
2420
|
+
return result;
|
|
2421
|
+
};
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
2426
|
+
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
2427
|
+
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
2428
|
+
*/
|
|
2429
|
+
function clearDispatchTimers(instance) {
|
|
2430
|
+
const generation = (instance[DISPATCH_GENERATION] ?? 0) + 1;
|
|
2431
|
+
instance[DISPATCH_GENERATION] = generation;
|
|
2432
|
+
const timers = instance[DISPATCH_TIMERS];
|
|
2433
|
+
if (!timers)
|
|
2434
|
+
return;
|
|
2435
|
+
for (const t of timers.values()) {
|
|
2436
|
+
if (t.debounceTimeout)
|
|
2437
|
+
clearTimeout(t.debounceTimeout);
|
|
2438
|
+
if (t.throttleTimeout)
|
|
2439
|
+
clearTimeout(t.throttleTimeout);
|
|
2440
|
+
t.debounceTimeout = null;
|
|
2441
|
+
t.throttleTimeout = null;
|
|
2442
|
+
t.throttleLastCall = 0;
|
|
2443
|
+
t.latestDetail = undefined;
|
|
2444
|
+
t.invocation = (t.invocation ?? 0) + 1;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2152
2448
|
/**
|
|
2153
2449
|
* @context decorator for receiving router context updates
|
|
2154
2450
|
*/
|
|
@@ -2687,6 +2983,9 @@ async function detachController(element) {
|
|
|
2687
2983
|
if (!controllerInstance) {
|
|
2688
2984
|
return;
|
|
2689
2985
|
}
|
|
2986
|
+
// Invalidate dispatch work before every teardown path, including an attach
|
|
2987
|
+
// that is still waiting for host readiness and will return early below.
|
|
2988
|
+
clearDispatchTimers(controllerInstance);
|
|
2690
2989
|
// Claim the controller immediately — before any await — so a second detach
|
|
2691
2990
|
// that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
|
|
2692
2991
|
// controller reassignment) finds no instance and returns, instead of running
|
|
@@ -2719,6 +3018,8 @@ async function detachController(element) {
|
|
|
2719
3018
|
else {
|
|
2720
3019
|
await controllerInstance.detach(element);
|
|
2721
3020
|
}
|
|
3021
|
+
// detach() itself may have invoked a decorated dispatch method.
|
|
3022
|
+
clearDispatchTimers(controllerInstance);
|
|
2722
3023
|
controllerInstance.element = null;
|
|
2723
3024
|
// Cleanup @observe observers for controller
|
|
2724
3025
|
cleanupObservers(controllerInstance);
|
|
@@ -3078,36 +3379,184 @@ function findRenderHost(node) {
|
|
|
3078
3379
|
return null;
|
|
3079
3380
|
}
|
|
3080
3381
|
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3382
|
+
const SAFE_TAG = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
3383
|
+
const SAFE_CLASS = /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/;
|
|
3384
|
+
const AMBIGUOUS_RENDER_HOST_IDENTITY = Symbol('ambiguous-render-host-identity');
|
|
3385
|
+
// Keys are weak; values contain only a sanitized label or the ambiguity
|
|
3386
|
+
// sentinel. The same frozen identity is recorded for the exact constructor
|
|
3387
|
+
// and its exact prototype only after the caller verifies registration.
|
|
3388
|
+
const registeredRenderHosts = new WeakMap();
|
|
3389
|
+
function registerRenderHostIdentity(constructor, tagName) {
|
|
3390
|
+
try {
|
|
3391
|
+
if (typeof constructor !== 'function' || !SAFE_TAG.test(tagName))
|
|
3392
|
+
return;
|
|
3393
|
+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(constructor, 'prototype');
|
|
3394
|
+
const prototype = prototypeDescriptor && 'value' in prototypeDescriptor
|
|
3395
|
+
? prototypeDescriptor.value
|
|
3396
|
+
: undefined;
|
|
3397
|
+
if ((typeof prototype !== 'object' && typeof prototype !== 'function') || prototype === null)
|
|
3398
|
+
return;
|
|
3399
|
+
const nameDescriptor = Object.getOwnPropertyDescriptor(constructor, 'name');
|
|
3400
|
+
const name = nameDescriptor && 'value' in nameDescriptor && typeof nameDescriptor.value === 'string'
|
|
3401
|
+
? nameDescriptor.value
|
|
3402
|
+
: '';
|
|
3403
|
+
const className = SAFE_CLASS.test(name) ? name : '';
|
|
3404
|
+
const identity = Object.freeze({
|
|
3405
|
+
label: className ? `<${tagName}> (${className})` : `<${tagName}>`,
|
|
3406
|
+
});
|
|
3407
|
+
const constructorState = registeredRenderHosts.get(constructor);
|
|
3408
|
+
const prototypeState = registeredRenderHosts.get(prototype);
|
|
3409
|
+
const existingStates = [constructorState, prototypeState].filter((state) => state !== undefined);
|
|
3410
|
+
const ambiguous = existingStates.some(state => state === AMBIGUOUS_RENDER_HOST_IDENTITY || state.label !== identity.label);
|
|
3411
|
+
const nextState = ambiguous
|
|
3412
|
+
? AMBIGUOUS_RENDER_HOST_IDENTITY
|
|
3413
|
+
: existingStates[0] ?? identity;
|
|
3414
|
+
// Update both exact keys in the same synchronous registration call. Once
|
|
3415
|
+
// either key is ambiguous, no later registration can restore attribution.
|
|
3416
|
+
registeredRenderHosts.set(constructor, nextState);
|
|
3417
|
+
registeredRenderHosts.set(prototype, nextState);
|
|
3418
|
+
}
|
|
3419
|
+
catch {
|
|
3420
|
+
// Registration diagnostics are best-effort and must not affect the
|
|
3421
|
+
// successful custom-elements registration that already occurred.
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
function registeredIdentity(host) {
|
|
3425
|
+
const prototype = Object.getPrototypeOf(host);
|
|
3426
|
+
if (!prototype)
|
|
3427
|
+
return undefined;
|
|
3428
|
+
const constructorDescriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor');
|
|
3429
|
+
const constructor = constructorDescriptor && 'value' in constructorDescriptor
|
|
3430
|
+
? constructorDescriptor.value
|
|
3431
|
+
: undefined;
|
|
3432
|
+
if (typeof constructor !== 'function')
|
|
3433
|
+
return undefined;
|
|
3434
|
+
const prototypeIdentity = registeredRenderHosts.get(prototype);
|
|
3435
|
+
const constructorIdentity = registeredRenderHosts.get(constructor);
|
|
3436
|
+
return prototypeIdentity !== AMBIGUOUS_RENDER_HOST_IDENTITY &&
|
|
3437
|
+
prototypeIdentity === constructorIdentity
|
|
3438
|
+
? prototypeIdentity
|
|
3439
|
+
: undefined;
|
|
3440
|
+
}
|
|
3441
|
+
/** Capture once at the render boundary; failures fall back without masking the render error. */
|
|
3442
|
+
function captureRenderHostIdentity(host) {
|
|
3443
|
+
let label = '<element>';
|
|
3444
|
+
try {
|
|
3445
|
+
const registration = registeredIdentity(host);
|
|
3446
|
+
if (registration)
|
|
3447
|
+
label = registration.label;
|
|
3448
|
+
}
|
|
3449
|
+
catch {
|
|
3450
|
+
// Host/prototype objects can be proxied. Attribution is best-effort
|
|
3451
|
+
// and must never replace the original render or template error.
|
|
3452
|
+
}
|
|
3453
|
+
return Object.freeze({ label, marker: Symbol('render-host-identity') });
|
|
3454
|
+
}
|
|
3455
|
+
class ContextualRenderError extends Error {
|
|
3456
|
+
#identityMarker;
|
|
3457
|
+
constructor(identity, message, cause) {
|
|
3458
|
+
super(message, { cause });
|
|
3459
|
+
this.#identityMarker = identity.marker;
|
|
3460
|
+
}
|
|
3461
|
+
belongsTo(identity) {
|
|
3462
|
+
return this.#identityMarker === identity.marker;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
function contextualizeRenderError(identity, error) {
|
|
3466
|
+
if (error instanceof ContextualRenderError && error.belongsTo(identity))
|
|
3467
|
+
return error;
|
|
3468
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3469
|
+
const contextual = new ContextualRenderError(identity, `snice: render failed for ${identity.label}: ${message}`, error);
|
|
3470
|
+
if (error instanceof Error)
|
|
3471
|
+
contextual.name = error.name;
|
|
3472
|
+
return contextual;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// Unique marker for dynamic template parts. It parses as a comment node but
|
|
3476
|
+
// does not get escaped in attributes.
|
|
3477
|
+
const templateMarker = `snice$${Math.random().toFixed(9).slice(2)}$`;
|
|
3478
|
+
const templateMarkerMatch = '?' + templateMarker;
|
|
3085
3479
|
// A true comment, not a processing instruction (<?...>): in HTML content both
|
|
3086
3480
|
// parse to the same comment node, but PIs are dropped inside foreign content
|
|
3087
3481
|
// (<svg>), which would silently kill any node binding inside an svg block.
|
|
3088
|
-
const
|
|
3482
|
+
const templateNodeMarker = `<!--${templateMarkerMatch}-->`;
|
|
3089
3483
|
// Escape the `$` chars — as a bare RegExp they'd be end anchors and the
|
|
3090
3484
|
// pattern would never match, so marker-bearing text/comments never split.
|
|
3091
|
-
const
|
|
3485
|
+
const templateMarkerRegex = new RegExp(templateMarker.replace(/\$/g, '\\$'), 'g');
|
|
3486
|
+
function hasTemplateMarker(value) {
|
|
3487
|
+
return value?.includes(templateMarker) ?? false;
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3092
3490
|
// Template cache - templates with same string array can be reused
|
|
3093
3491
|
const templateCache = new WeakMap();
|
|
3094
3492
|
// Sentinel for "not yet set" - distinct from undefined/null
|
|
3095
3493
|
const NOT_COMMITTED = Symbol('not-committed');
|
|
3096
|
-
|
|
3494
|
+
const pendingDefinitionRegistries = new WeakMap();
|
|
3495
|
+
function isUpgradedBy(element, constructor) {
|
|
3496
|
+
return Object.prototype.isPrototypeOf.call(constructor.prototype, element);
|
|
3497
|
+
}
|
|
3498
|
+
function flushPreUpgradePropertyBindings(element, registry) {
|
|
3499
|
+
const tagName = element.localName;
|
|
3500
|
+
const constructor = tagName && registry?.get(tagName);
|
|
3501
|
+
if (!constructor || !isUpgradedBy(element, constructor))
|
|
3502
|
+
return;
|
|
3503
|
+
const target = element;
|
|
3504
|
+
const markedBindings = target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3505
|
+
if (!markedBindings?.size)
|
|
3506
|
+
return;
|
|
3507
|
+
for (const propertyName of Array.from(markedBindings)) {
|
|
3508
|
+
// A Snice @property initializer consumes its own marker and expando while
|
|
3509
|
+
// the element upgrades. Anything left belongs to a class that retained
|
|
3510
|
+
// the native/custom property channel, so replay the latest parked value
|
|
3511
|
+
// through that now-upgraded channel.
|
|
3512
|
+
if (Object.prototype.hasOwnProperty.call(target, propertyName)) {
|
|
3513
|
+
const value = target[propertyName];
|
|
3514
|
+
delete target[propertyName];
|
|
3515
|
+
target[propertyName] = value;
|
|
3516
|
+
}
|
|
3517
|
+
markedBindings.delete(propertyName);
|
|
3518
|
+
}
|
|
3519
|
+
delete target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3520
|
+
}
|
|
3521
|
+
function schedulePreUpgradePropertyFlush(element, registry) {
|
|
3522
|
+
if (!registry?.whenDefined || pendingDefinitionRegistries.get(element) === registry)
|
|
3523
|
+
return;
|
|
3524
|
+
pendingDefinitionRegistries.set(element, registry);
|
|
3525
|
+
const reference = new WeakRef(element);
|
|
3526
|
+
void registry.whenDefined(element.localName).then(() => {
|
|
3527
|
+
const target = reference.deref();
|
|
3528
|
+
if (target)
|
|
3529
|
+
flushPreUpgradePropertyBindings(target, registry);
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
function markPreUpgradePropertyBinding(element, propertyName, registry) {
|
|
3097
3533
|
const tagName = element.localName;
|
|
3098
3534
|
if (!tagName?.includes('-'))
|
|
3099
3535
|
return false;
|
|
3100
|
-
const
|
|
3101
|
-
if (
|
|
3102
|
-
|
|
3536
|
+
const registeredConstructor = registry?.get(tagName);
|
|
3537
|
+
if (registeredConstructor) {
|
|
3538
|
+
// Template contents can still contain an unupgraded HTMLElement after the
|
|
3539
|
+
// definition has been registered. Registry presence alone therefore does
|
|
3540
|
+
// not prove that the reactive accessor exists on this clone yet.
|
|
3541
|
+
const isUpgraded = isUpgradedBy(element, registeredConstructor);
|
|
3542
|
+
if (isUpgraded)
|
|
3543
|
+
return false;
|
|
3544
|
+
// A registered non-Snice element (or a Snice element that did not
|
|
3545
|
+
// redeclare this member) must retain the native IDL channel. Parking its
|
|
3546
|
+
// role/title/etc. would replace native reflection with an own expando.
|
|
3547
|
+
const reactiveProperties = registeredConstructor[PROPERTIES];
|
|
3548
|
+
if (!reactiveProperties?.has(propertyName))
|
|
3549
|
+
return false;
|
|
3550
|
+
}
|
|
3103
3551
|
const target = element;
|
|
3104
3552
|
if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
|
|
3105
3553
|
target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
|
|
3106
3554
|
target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
|
|
3555
|
+
schedulePreUpgradePropertyFlush(element, registry);
|
|
3107
3556
|
return true;
|
|
3108
3557
|
}
|
|
3109
|
-
function commitPropertyValue(element, propertyName, value) {
|
|
3110
|
-
if (!markPreUpgradePropertyBinding(element, propertyName)) {
|
|
3558
|
+
function commitPropertyValue(element, propertyName, value, registry) {
|
|
3559
|
+
if (!markPreUpgradePropertyBinding(element, propertyName, registry)) {
|
|
3111
3560
|
element[propertyName] = value;
|
|
3112
3561
|
return;
|
|
3113
3562
|
}
|
|
@@ -3207,7 +3656,7 @@ class Template {
|
|
|
3207
3656
|
if (tagName === 'if') {
|
|
3208
3657
|
// <if value="${condition}">children</if>
|
|
3209
3658
|
const valueAttr = element.getAttribute('value');
|
|
3210
|
-
if (valueAttr && valueAttr.includes(
|
|
3659
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3211
3660
|
// Remove the value attribute
|
|
3212
3661
|
element.removeAttribute('value');
|
|
3213
3662
|
this.parts.push({
|
|
@@ -3226,7 +3675,7 @@ class Template {
|
|
|
3226
3675
|
if (tagName === 'case') {
|
|
3227
3676
|
// <case value="${value}">children</case>
|
|
3228
3677
|
const valueAttr = element.getAttribute('value');
|
|
3229
|
-
if (valueAttr && valueAttr.includes(
|
|
3678
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3230
3679
|
for (const child of Array.from(element.childNodes)) {
|
|
3231
3680
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
3232
3681
|
const branchTag = child.tagName.toLowerCase();
|
|
@@ -3235,7 +3684,7 @@ class Template {
|
|
|
3235
3684
|
}
|
|
3236
3685
|
}
|
|
3237
3686
|
else if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim() ||
|
|
3238
|
-
child.nodeType === Node.COMMENT_NODE && child.data.includes(
|
|
3687
|
+
child.nodeType === Node.COMMENT_NODE && child.data.includes(templateMarker)) {
|
|
3239
3688
|
throw new Error('snice: content inside <case> must be nested in a <when> or <default> branch.');
|
|
3240
3689
|
}
|
|
3241
3690
|
}
|
|
@@ -3255,7 +3704,7 @@ class Template {
|
|
|
3255
3704
|
}
|
|
3256
3705
|
if (tagName === 'else-if') {
|
|
3257
3706
|
const valueAttr = element.getAttribute('value');
|
|
3258
|
-
if (valueAttr && valueAttr.includes(
|
|
3707
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3259
3708
|
element.removeAttribute('value');
|
|
3260
3709
|
this.parts.push({
|
|
3261
3710
|
type: 'conditional-else-if',
|
|
@@ -3270,7 +3719,7 @@ class Template {
|
|
|
3270
3719
|
}
|
|
3271
3720
|
if (tagName === 'when') {
|
|
3272
3721
|
const valueAttr = element.getAttribute('value');
|
|
3273
|
-
if (valueAttr && valueAttr.includes(
|
|
3722
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3274
3723
|
element.removeAttribute('value');
|
|
3275
3724
|
this.parts.push({
|
|
3276
3725
|
type: 'conditional-when',
|
|
@@ -3287,12 +3736,12 @@ class Template {
|
|
|
3287
3736
|
const attr = attributes[i];
|
|
3288
3737
|
const value = attr.value;
|
|
3289
3738
|
// Check for attribute bindings
|
|
3290
|
-
if (value.includes(
|
|
3739
|
+
if (value.includes(templateMarker)) {
|
|
3291
3740
|
attrsToRemove.push(attr);
|
|
3292
3741
|
// Get original attribute name with preserved case
|
|
3293
3742
|
const originalName = attrNamesForParts[partIndex] || attr.name;
|
|
3294
3743
|
// Extract static string segments by splitting on marker
|
|
3295
|
-
const attrStrings = value.split(
|
|
3744
|
+
const attrStrings = value.split(templateMarker);
|
|
3296
3745
|
// Number of expressions = number of markers = attrStrings.length - 1
|
|
3297
3746
|
const expressionCount = attrStrings.length - 1;
|
|
3298
3747
|
// Property/boolean/event bindings take a SINGLE expression — they
|
|
@@ -3433,7 +3882,7 @@ class Template {
|
|
|
3433
3882
|
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
3434
3883
|
const comment = node;
|
|
3435
3884
|
// Check for marker match (processing instruction becomes comment)
|
|
3436
|
-
if (comment.data ===
|
|
3885
|
+
if (comment.data === templateMarkerMatch) {
|
|
3437
3886
|
// Node part
|
|
3438
3887
|
const parent = comment.parentNode;
|
|
3439
3888
|
const endNode = document.createComment('');
|
|
@@ -3445,11 +3894,11 @@ class Template {
|
|
|
3445
3894
|
endNode
|
|
3446
3895
|
});
|
|
3447
3896
|
}
|
|
3448
|
-
else if (comment.data.includes(
|
|
3897
|
+
else if (comment.data.includes(templateMarker)) {
|
|
3449
3898
|
// Binding(s) inside an authored HTML comment (<!-- ${x} -->).
|
|
3450
3899
|
// Render values into the comment text; critically, consume the
|
|
3451
3900
|
// right number of value indices so later parts stay aligned.
|
|
3452
|
-
const commentStrings = comment.data.split(
|
|
3901
|
+
const commentStrings = comment.data.split(templateMarkerRegex);
|
|
3453
3902
|
this.parts.push({
|
|
3454
3903
|
type: 'comment',
|
|
3455
3904
|
index: partIndex,
|
|
@@ -3462,10 +3911,10 @@ class Template {
|
|
|
3462
3911
|
else if (node.nodeType === Node.TEXT_NODE) {
|
|
3463
3912
|
const text = node;
|
|
3464
3913
|
const data = text.data;
|
|
3465
|
-
if (data.includes(
|
|
3914
|
+
if (data.includes(templateMarker)) {
|
|
3466
3915
|
// Split text node at markers
|
|
3467
3916
|
const parent = text.parentNode;
|
|
3468
|
-
const parts = data.split(
|
|
3917
|
+
const parts = data.split(templateMarkerRegex);
|
|
3469
3918
|
const lastIndex = parts.length - 1;
|
|
3470
3919
|
for (let i = 0; i < lastIndex; i++) {
|
|
3471
3920
|
parent.insertBefore(document.createTextNode(parts[i]), text);
|
|
@@ -3587,13 +4036,13 @@ function prepareTemplate(result) {
|
|
|
3587
4036
|
// the value index stays aligned (a swallowed marker would shift every
|
|
3588
4037
|
// binding after the comment).
|
|
3589
4038
|
attrNamesForParts.push('');
|
|
3590
|
-
htmlParts.push(
|
|
4039
|
+
htmlParts.push(templateMarker);
|
|
3591
4040
|
}
|
|
3592
4041
|
else if (inAttrValue) {
|
|
3593
4042
|
// We're inside a quoted attribute value - this is an attribute binding
|
|
3594
4043
|
// For subsequent interpolations in same attribute, keep using same attr name
|
|
3595
4044
|
attrNamesForParts.push(currentAttrName);
|
|
3596
|
-
htmlParts.push(
|
|
4045
|
+
htmlParts.push(templateMarker);
|
|
3597
4046
|
}
|
|
3598
4047
|
else if (inTag) {
|
|
3599
4048
|
// Inside tag but not in attr value - check for special cases
|
|
@@ -3608,7 +4057,7 @@ function prepareTemplate(result) {
|
|
|
3608
4057
|
}
|
|
3609
4058
|
currentAttrName = trimmed.substring(attrStart + 1, trimmed.length - 1).trim();
|
|
3610
4059
|
attrNamesForParts.push(currentAttrName);
|
|
3611
|
-
htmlParts.push(
|
|
4060
|
+
htmlParts.push(templateMarker);
|
|
3612
4061
|
}
|
|
3613
4062
|
else {
|
|
3614
4063
|
// Check if this is a meta element (<if> or <case>)
|
|
@@ -3616,7 +4065,7 @@ function prepareTemplate(result) {
|
|
|
3616
4065
|
if (metaElementMatch) {
|
|
3617
4066
|
currentAttrName = 'value';
|
|
3618
4067
|
attrNamesForParts.push('value');
|
|
3619
|
-
htmlParts.push(`value="${
|
|
4068
|
+
htmlParts.push(`value="${templateMarker}"`);
|
|
3620
4069
|
}
|
|
3621
4070
|
else {
|
|
3622
4071
|
const nearbyTemplate = (str.slice(-40) + '${…}' + (strings[i + 1] ?? '').slice(0, 40)).replace(/\s+/g, ' ').trim();
|
|
@@ -3629,7 +4078,7 @@ function prepareTemplate(result) {
|
|
|
3629
4078
|
else {
|
|
3630
4079
|
// Outside any tag - this is node content
|
|
3631
4080
|
attrNamesForParts.push('');
|
|
3632
|
-
htmlParts.push(
|
|
4081
|
+
htmlParts.push(templateNodeMarker);
|
|
3633
4082
|
}
|
|
3634
4083
|
}
|
|
3635
4084
|
}
|
|
@@ -3663,22 +4112,37 @@ function prepareTemplate(result) {
|
|
|
3663
4112
|
* Extract the key of a list item rendered from a template with a
|
|
3664
4113
|
* `key=${...}` binding. Returns undefined for unkeyed items.
|
|
3665
4114
|
*/
|
|
3666
|
-
function getItemKey(item) {
|
|
4115
|
+
function getItemKey(item, renderIdentity) {
|
|
3667
4116
|
if (!isTemplateResult(item))
|
|
3668
4117
|
return undefined;
|
|
3669
|
-
|
|
4118
|
+
let tmpl;
|
|
4119
|
+
try {
|
|
4120
|
+
tmpl = prepareTemplate(item);
|
|
4121
|
+
}
|
|
4122
|
+
catch (error) {
|
|
4123
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4124
|
+
}
|
|
3670
4125
|
return tmpl.keyIndex === -1 ? undefined : item.values[tmpl.keyIndex];
|
|
3671
4126
|
}
|
|
3672
4127
|
/**
|
|
3673
4128
|
* Instance of a rendered template
|
|
3674
4129
|
*/
|
|
3675
4130
|
class TemplateInstance {
|
|
3676
|
-
|
|
4131
|
+
#renderIdentity;
|
|
4132
|
+
#customElementRegistry;
|
|
4133
|
+
constructor(result, renderIdentity = null, customElementRegistry = document.defaultView?.customElements ?? globalThis.customElements ?? null) {
|
|
3677
4134
|
this.parts = [];
|
|
3678
4135
|
this.fragment = null;
|
|
3679
4136
|
this.conditionalParts = []; // if/case parts with their indices
|
|
3680
4137
|
this.regularParts = []; // all other parts with their indices
|
|
3681
|
-
this
|
|
4138
|
+
this.#renderIdentity = renderIdentity;
|
|
4139
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4140
|
+
try {
|
|
4141
|
+
this.template = prepareTemplate(result);
|
|
4142
|
+
}
|
|
4143
|
+
catch (error) {
|
|
4144
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4145
|
+
}
|
|
3682
4146
|
this.strings = result.strings;
|
|
3683
4147
|
}
|
|
3684
4148
|
/**
|
|
@@ -3688,6 +4152,10 @@ class TemplateInstance {
|
|
|
3688
4152
|
isSameTemplate(strings) {
|
|
3689
4153
|
return this.strings === strings;
|
|
3690
4154
|
}
|
|
4155
|
+
/** Internal attribution value; intentionally contains no live renderer host. */
|
|
4156
|
+
getRenderHostIdentity() {
|
|
4157
|
+
return this.#renderIdentity;
|
|
4158
|
+
}
|
|
3691
4159
|
renderFragment() {
|
|
3692
4160
|
if (!this.fragment) {
|
|
3693
4161
|
// First render - clone template and create parts
|
|
@@ -3716,7 +4184,7 @@ class TemplateInstance {
|
|
|
3716
4184
|
case 'node':
|
|
3717
4185
|
const startNode = nodeMap.get(partDef.startNode);
|
|
3718
4186
|
const endNode = nodeMap.get(partDef.endNode);
|
|
3719
|
-
part = new NodePart(startNode, endNode);
|
|
4187
|
+
part = new NodePart(startNode, endNode, this.#renderIdentity, this.#customElementRegistry);
|
|
3720
4188
|
break;
|
|
3721
4189
|
case 'attribute':
|
|
3722
4190
|
const attrElement = nodeMap.get(partDef.element);
|
|
@@ -3724,7 +4192,7 @@ class TemplateInstance {
|
|
|
3724
4192
|
break;
|
|
3725
4193
|
case 'property':
|
|
3726
4194
|
const propElement = nodeMap.get(partDef.element);
|
|
3727
|
-
part = new PropertyPart(propElement, partDef.name);
|
|
4195
|
+
part = new PropertyPart(propElement, partDef.name, this.#customElementRegistry);
|
|
3728
4196
|
break;
|
|
3729
4197
|
case 'boolean-attribute':
|
|
3730
4198
|
const boolElement = nodeMap.get(partDef.element);
|
|
@@ -3744,7 +4212,7 @@ class TemplateInstance {
|
|
|
3744
4212
|
break;
|
|
3745
4213
|
case 'spread':
|
|
3746
4214
|
const spreadElement = nodeMap.get(partDef.element);
|
|
3747
|
-
part = new SpreadPart(spreadElement, partDef.name);
|
|
4215
|
+
part = new SpreadPart(spreadElement, partDef.name, this.#customElementRegistry);
|
|
3748
4216
|
break;
|
|
3749
4217
|
case 'controller':
|
|
3750
4218
|
const controllerElement = nodeMap.get(partDef.element);
|
|
@@ -3923,7 +4391,9 @@ class Part {
|
|
|
3923
4391
|
* - Reuses child NodeParts when rendering arrays
|
|
3924
4392
|
*/
|
|
3925
4393
|
class NodePart extends Part {
|
|
3926
|
-
|
|
4394
|
+
#renderIdentity;
|
|
4395
|
+
#customElementRegistry;
|
|
4396
|
+
constructor(startNode, endNode, renderIdentity = null, customElementRegistry = null) {
|
|
3927
4397
|
super();
|
|
3928
4398
|
this.type = 'node';
|
|
3929
4399
|
this._committedValue = NOT_COMMITTED;
|
|
@@ -3938,6 +4408,11 @@ class NodePart extends Part {
|
|
|
3938
4408
|
this._committingAsyncValue = false;
|
|
3939
4409
|
this.startNode = startNode;
|
|
3940
4410
|
this.endNode = endNode;
|
|
4411
|
+
this.#renderIdentity = renderIdentity;
|
|
4412
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4413
|
+
}
|
|
4414
|
+
_withRenderContext(error) {
|
|
4415
|
+
return this.#renderIdentity ? contextualizeRenderError(this.#renderIdentity, error) : error;
|
|
3941
4416
|
}
|
|
3942
4417
|
commit(value) {
|
|
3943
4418
|
// Handle noChange sentinel
|
|
@@ -4046,7 +4521,7 @@ class NodePart extends Part {
|
|
|
4046
4521
|
// Different template or first render: prepare and validate the replacement
|
|
4047
4522
|
// while it is detached. If a binding throws, the currently
|
|
4048
4523
|
// committed range remains intact and its lifecycle stays connected.
|
|
4049
|
-
const instance = new TemplateInstance(result);
|
|
4524
|
+
const instance = new TemplateInstance(result, this.#renderIdentity, this.#customElementRegistry);
|
|
4050
4525
|
const fragment = instance.renderFragment();
|
|
4051
4526
|
try {
|
|
4052
4527
|
instance.update(result.values);
|
|
@@ -4097,7 +4572,9 @@ class NodePart extends Part {
|
|
|
4097
4572
|
*/
|
|
4098
4573
|
_commitIterable(value, explicitKeys) {
|
|
4099
4574
|
const items = Array.isArray(value) ? value : Array.from(value);
|
|
4100
|
-
const newKeys = explicitKeys
|
|
4575
|
+
const newKeys = explicitKeys
|
|
4576
|
+
? Array.from(explicitKeys)
|
|
4577
|
+
: items.map(item => getItemKey(item, this.#renderIdentity));
|
|
4101
4578
|
if (newKeys.length !== items.length) {
|
|
4102
4579
|
throw new Error('snice: keyed iterable produced a different number of keys and values.');
|
|
4103
4580
|
}
|
|
@@ -4143,7 +4620,7 @@ class NodePart extends Part {
|
|
|
4143
4620
|
const endMarker = document.createComment('');
|
|
4144
4621
|
this._insertBefore(startMarker);
|
|
4145
4622
|
this._insertBefore(endMarker);
|
|
4146
|
-
itemPart = new NodePart(startMarker, endMarker);
|
|
4623
|
+
itemPart = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4147
4624
|
itemParts.push(itemPart);
|
|
4148
4625
|
}
|
|
4149
4626
|
else {
|
|
@@ -4247,7 +4724,7 @@ class NodePart extends Part {
|
|
|
4247
4724
|
const endMarker = document.createComment('');
|
|
4248
4725
|
parent.insertBefore(startMarker, ref);
|
|
4249
4726
|
parent.insertBefore(endMarker, ref);
|
|
4250
|
-
part = new NodePart(startMarker, endMarker);
|
|
4727
|
+
part = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4251
4728
|
newParts[i] = part;
|
|
4252
4729
|
}
|
|
4253
4730
|
part.commit(items[i]);
|
|
@@ -4319,7 +4796,7 @@ class NodePart extends Part {
|
|
|
4319
4796
|
catch (error) {
|
|
4320
4797
|
this._asyncRunning = false;
|
|
4321
4798
|
this._asyncCompleted = true;
|
|
4322
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4799
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4323
4800
|
return;
|
|
4324
4801
|
}
|
|
4325
4802
|
void (async () => {
|
|
@@ -4332,8 +4809,9 @@ class NodePart extends Part {
|
|
|
4332
4809
|
}
|
|
4333
4810
|
}
|
|
4334
4811
|
catch (error) {
|
|
4335
|
-
if (version === this._asyncVersion)
|
|
4336
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4812
|
+
if (version === this._asyncVersion) {
|
|
4813
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4814
|
+
}
|
|
4337
4815
|
}
|
|
4338
4816
|
finally {
|
|
4339
4817
|
if (version === this._asyncVersion) {
|
|
@@ -4354,14 +4832,14 @@ class NodePart extends Part {
|
|
|
4354
4832
|
this._commitAsyncValue(value);
|
|
4355
4833
|
}
|
|
4356
4834
|
catch (error) {
|
|
4357
|
-
console.error('snice: promise template value failed:', error);
|
|
4835
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4358
4836
|
}
|
|
4359
4837
|
}, error => {
|
|
4360
4838
|
if (version !== this._asyncVersion)
|
|
4361
4839
|
return;
|
|
4362
4840
|
this._asyncRunning = false;
|
|
4363
4841
|
this._asyncCompleted = true;
|
|
4364
|
-
console.error('snice: promise template value failed:', error);
|
|
4842
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4365
4843
|
});
|
|
4366
4844
|
}
|
|
4367
4845
|
_commitAsyncValue(value) {
|
|
@@ -4729,7 +5207,8 @@ function validateSpreadEvents(next, label) {
|
|
|
4729
5207
|
}
|
|
4730
5208
|
/** Named spreads: ...props, ...attrs, and ...events. */
|
|
4731
5209
|
class SpreadPart extends Part {
|
|
4732
|
-
|
|
5210
|
+
#customElementRegistry;
|
|
5211
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4733
5212
|
super();
|
|
4734
5213
|
this.type = 'spread';
|
|
4735
5214
|
this.committed = {};
|
|
@@ -4737,6 +5216,7 @@ class SpreadPart extends Part {
|
|
|
4737
5216
|
this.consumedOnce = new Map();
|
|
4738
5217
|
this.element = element;
|
|
4739
5218
|
this.name = name;
|
|
5219
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4740
5220
|
}
|
|
4741
5221
|
commit(value) {
|
|
4742
5222
|
if (value === noChange)
|
|
@@ -4758,13 +5238,13 @@ class SpreadPart extends Part {
|
|
|
4758
5238
|
commitProperties(next) {
|
|
4759
5239
|
for (const key of Object.keys(this.committed)) {
|
|
4760
5240
|
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
4761
|
-
commitPropertyValue(this.element, key, undefined);
|
|
5241
|
+
commitPropertyValue(this.element, key, undefined, this.#customElementRegistry);
|
|
4762
5242
|
}
|
|
4763
5243
|
}
|
|
4764
5244
|
for (const [key, value] of Object.entries(next)) {
|
|
4765
5245
|
if (Object.is(this.committed[key], value))
|
|
4766
5246
|
continue;
|
|
4767
|
-
commitPropertyValue(this.element, key, value === nothing ? undefined : value);
|
|
5247
|
+
commitPropertyValue(this.element, key, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
4768
5248
|
}
|
|
4769
5249
|
}
|
|
4770
5250
|
commitAttributes(next) {
|
|
@@ -4848,6 +5328,9 @@ class SpreadPart extends Part {
|
|
|
4848
5328
|
this.detachListeners();
|
|
4849
5329
|
}
|
|
4850
5330
|
reconnected() {
|
|
5331
|
+
if (this.name === 'props' || this.name === 'properties') {
|
|
5332
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
5333
|
+
}
|
|
4851
5334
|
if (this.name === 'events' && this.listeners.size === 0)
|
|
4852
5335
|
this.commitEvents(this.committed);
|
|
4853
5336
|
}
|
|
@@ -4862,12 +5345,14 @@ class SpreadPart extends Part {
|
|
|
4862
5345
|
* PropertyPart handles property bindings (.property=${value})
|
|
4863
5346
|
*/
|
|
4864
5347
|
class PropertyPart extends Part {
|
|
4865
|
-
|
|
5348
|
+
#customElementRegistry;
|
|
5349
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4866
5350
|
super();
|
|
4867
5351
|
this.type = 'property';
|
|
4868
5352
|
this._committedValue = NOT_COMMITTED;
|
|
4869
5353
|
this.element = element;
|
|
4870
5354
|
this.name = name;
|
|
5355
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4871
5356
|
}
|
|
4872
5357
|
commit(value) {
|
|
4873
5358
|
if (value === noChange)
|
|
@@ -4891,7 +5376,10 @@ class PropertyPart extends Part {
|
|
|
4891
5376
|
return;
|
|
4892
5377
|
}
|
|
4893
5378
|
this._committedValue = value;
|
|
4894
|
-
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
|
|
5379
|
+
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
5380
|
+
}
|
|
5381
|
+
reconnected() {
|
|
5382
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
4895
5383
|
}
|
|
4896
5384
|
clear() {
|
|
4897
5385
|
// This part is being destroyed with its owning template. The element is
|
|
@@ -5611,16 +6099,6 @@ function resetHostAutofocus(host) {
|
|
|
5611
6099
|
* Production default is false (log and keep the previous DOM).
|
|
5612
6100
|
*/
|
|
5613
6101
|
let strictRenderErrors = false;
|
|
5614
|
-
function contextualizeRenderError(element, error) {
|
|
5615
|
-
const tag = element.tagName?.toLowerCase() || 'element';
|
|
5616
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5617
|
-
const contextual = new Error(`snice: render failed for <${tag}>: ${message}`, {
|
|
5618
|
-
cause: error
|
|
5619
|
-
});
|
|
5620
|
-
if (error instanceof Error)
|
|
5621
|
-
contextual.name = error.name;
|
|
5622
|
-
return contextual;
|
|
5623
|
-
}
|
|
5624
6102
|
function setStrictRenderErrors(value) {
|
|
5625
6103
|
strictRenderErrors = value;
|
|
5626
6104
|
}
|
|
@@ -5723,6 +6201,8 @@ const MAX_RENDER_DEPTH = 50;
|
|
|
5723
6201
|
* Perform the actual render of an element
|
|
5724
6202
|
*/
|
|
5725
6203
|
function performRender(element, options, precomputedResult) {
|
|
6204
|
+
const capturedIdentity = captureRenderHostIdentity(element);
|
|
6205
|
+
let renderIdentity = capturedIdentity;
|
|
5726
6206
|
const hasPrecomputedResult = arguments.length >= 3;
|
|
5727
6207
|
const renderMethod = element[RENDER_METHOD];
|
|
5728
6208
|
if (!renderMethod) {
|
|
@@ -5778,12 +6258,16 @@ function performRender(element, options, precomputedResult) {
|
|
|
5778
6258
|
}
|
|
5779
6259
|
let instance = element[RENDER_INSTANCE];
|
|
5780
6260
|
if (instance && instance.isSameTemplate(result.strings)) {
|
|
6261
|
+
renderIdentity = instance.getRenderHostIdentity() ?? capturedIdentity;
|
|
5781
6262
|
instance.update(result.values);
|
|
5782
6263
|
flushRenderCallbacks(element);
|
|
5783
6264
|
scheduleAutofocus(element);
|
|
5784
6265
|
return;
|
|
5785
6266
|
}
|
|
5786
|
-
const
|
|
6267
|
+
const customElementRegistry = renderRoot instanceof ShadowRoot
|
|
6268
|
+
? renderRoot.customElementRegistry ?? renderRoot.ownerDocument.defaultView?.customElements ?? null
|
|
6269
|
+
: renderRoot.ownerDocument.defaultView?.customElements ?? null;
|
|
6270
|
+
const nextInstance = new TemplateInstance(result, capturedIdentity, customElementRegistry);
|
|
5787
6271
|
const nextFragment = nextInstance.renderFragment();
|
|
5788
6272
|
// Commit while detached first. A malformed binding can then fail without
|
|
5789
6273
|
// exposing a half-updated tree. Connection-aware parts are paired with
|
|
@@ -5843,7 +6327,7 @@ function performRender(element, options, precomputedResult) {
|
|
|
5843
6327
|
throw cleanupError;
|
|
5844
6328
|
}
|
|
5845
6329
|
catch (error) {
|
|
5846
|
-
const contextual = contextualizeRenderError(
|
|
6330
|
+
const contextual = contextualizeRenderError(renderIdentity, error);
|
|
5847
6331
|
if (strictRenderErrors)
|
|
5848
6332
|
throw contextual;
|
|
5849
6333
|
console.error('Error rendering element:', contextual);
|
|
@@ -6131,134 +6615,6 @@ function applyStyles(element) {
|
|
|
6131
6615
|
}
|
|
6132
6616
|
}
|
|
6133
6617
|
|
|
6134
|
-
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
6135
|
-
/**
|
|
6136
|
-
* Decorator that automatically dispatches a custom event after a method is called.
|
|
6137
|
-
* The return value of the method becomes the event detail.
|
|
6138
|
-
*
|
|
6139
|
-
* @param eventName The name of the event to dispatch
|
|
6140
|
-
* @param options Optional configuration extending EventInit
|
|
6141
|
-
*/
|
|
6142
|
-
function dispatch(eventName, options) {
|
|
6143
|
-
return function (originalMethod, _context) {
|
|
6144
|
-
return function (...args) {
|
|
6145
|
-
// Create timing wrappers for dispatch (per-instance)
|
|
6146
|
-
if (!this[DISPATCH_TIMERS]) {
|
|
6147
|
-
this[DISPATCH_TIMERS] = new Map();
|
|
6148
|
-
}
|
|
6149
|
-
const timerKey = `${eventName}_${_context.name}`;
|
|
6150
|
-
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
6151
|
-
this[DISPATCH_TIMERS].set(timerKey, {
|
|
6152
|
-
debounceTimeout: null,
|
|
6153
|
-
throttleLastCall: 0,
|
|
6154
|
-
throttleTimeout: null
|
|
6155
|
-
});
|
|
6156
|
-
}
|
|
6157
|
-
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
6158
|
-
// Call the original method with preserved this context
|
|
6159
|
-
const result = originalMethod.apply(this, args);
|
|
6160
|
-
// Helper to dispatch the event
|
|
6161
|
-
const doDispatch = (detail) => {
|
|
6162
|
-
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
6163
|
-
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
6164
|
-
return;
|
|
6165
|
-
}
|
|
6166
|
-
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
6167
|
-
// not belong in the CustomEvent constructor dictionary.
|
|
6168
|
-
const event = new CustomEvent(eventName, {
|
|
6169
|
-
bubbles: options?.bubbles ?? true,
|
|
6170
|
-
cancelable: options?.cancelable ?? false,
|
|
6171
|
-
composed: options?.composed ?? true,
|
|
6172
|
-
detail
|
|
6173
|
-
});
|
|
6174
|
-
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
6175
|
-
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
6176
|
-
}
|
|
6177
|
-
if (options?.daemon !== undefined) {
|
|
6178
|
-
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
6179
|
-
return;
|
|
6180
|
-
}
|
|
6181
|
-
if (options?.scope !== undefined) {
|
|
6182
|
-
const target = resolveScope(this, options.scope);
|
|
6183
|
-
if (!target) {
|
|
6184
|
-
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
6185
|
-
return;
|
|
6186
|
-
}
|
|
6187
|
-
target.dispatchEvent(event);
|
|
6188
|
-
return;
|
|
6189
|
-
}
|
|
6190
|
-
const target = defaultCommunicationTarget(this);
|
|
6191
|
-
if (!target) {
|
|
6192
|
-
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
6193
|
-
}
|
|
6194
|
-
target.dispatchEvent(event);
|
|
6195
|
-
};
|
|
6196
|
-
// Helper to handle timed dispatch
|
|
6197
|
-
const timedDispatch = (detail) => {
|
|
6198
|
-
if (options?.debounce) {
|
|
6199
|
-
clearTimeout(timers.debounceTimeout);
|
|
6200
|
-
timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
|
|
6201
|
-
return;
|
|
6202
|
-
}
|
|
6203
|
-
if (!options?.throttle) {
|
|
6204
|
-
doDispatch(detail);
|
|
6205
|
-
return;
|
|
6206
|
-
}
|
|
6207
|
-
const now = Date.now();
|
|
6208
|
-
const remaining = options.throttle - (now - timers.throttleLastCall);
|
|
6209
|
-
if (remaining <= 0) {
|
|
6210
|
-
clearTimeout(timers.throttleTimeout);
|
|
6211
|
-
timers.throttleLastCall = now;
|
|
6212
|
-
doDispatch(detail);
|
|
6213
|
-
return;
|
|
6214
|
-
}
|
|
6215
|
-
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
6216
|
-
// data, not the first-suppressed-call detail captured by closure.
|
|
6217
|
-
timers.latestDetail = detail;
|
|
6218
|
-
if (!timers.throttleTimeout) {
|
|
6219
|
-
timers.throttleTimeout = setTimeout(() => {
|
|
6220
|
-
timers.throttleLastCall = Date.now();
|
|
6221
|
-
timers.throttleTimeout = null;
|
|
6222
|
-
const d = timers.latestDetail;
|
|
6223
|
-
timers.latestDetail = undefined;
|
|
6224
|
-
doDispatch(d);
|
|
6225
|
-
}, remaining);
|
|
6226
|
-
}
|
|
6227
|
-
};
|
|
6228
|
-
// Handle async methods
|
|
6229
|
-
if (result instanceof Promise) {
|
|
6230
|
-
return result.then((resolvedResult) => {
|
|
6231
|
-
timedDispatch(resolvedResult);
|
|
6232
|
-
return resolvedResult;
|
|
6233
|
-
});
|
|
6234
|
-
}
|
|
6235
|
-
// Sync method
|
|
6236
|
-
timedDispatch(result);
|
|
6237
|
-
return result;
|
|
6238
|
-
};
|
|
6239
|
-
};
|
|
6240
|
-
}
|
|
6241
|
-
/**
|
|
6242
|
-
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
6243
|
-
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
6244
|
-
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
6245
|
-
*/
|
|
6246
|
-
function clearDispatchTimers(instance) {
|
|
6247
|
-
const timers = instance[DISPATCH_TIMERS];
|
|
6248
|
-
if (!timers)
|
|
6249
|
-
return;
|
|
6250
|
-
for (const t of timers.values()) {
|
|
6251
|
-
if (t.debounceTimeout)
|
|
6252
|
-
clearTimeout(t.debounceTimeout);
|
|
6253
|
-
if (t.throttleTimeout)
|
|
6254
|
-
clearTimeout(t.throttleTimeout);
|
|
6255
|
-
t.debounceTimeout = null;
|
|
6256
|
-
t.throttleTimeout = null;
|
|
6257
|
-
t.throttleLastCall = 0;
|
|
6258
|
-
t.latestDetail = undefined;
|
|
6259
|
-
}
|
|
6260
|
-
}
|
|
6261
|
-
|
|
6262
6618
|
/**
|
|
6263
6619
|
* Method decorators for common patterns
|
|
6264
6620
|
* @debounce, @throttle, @once, @memoize
|
|
@@ -7141,6 +7497,10 @@ function applyElementFunctionality(constructor) {
|
|
|
7141
7497
|
scheduleAutofocus(this);
|
|
7142
7498
|
};
|
|
7143
7499
|
constructor.prototype.disconnectedCallback = async function () {
|
|
7500
|
+
// Invalidate @dispatch work queued or started before disconnect synchronously.
|
|
7501
|
+
// The platform does not await disconnectedCallback, and @dispose may be
|
|
7502
|
+
// async, so waiting until the end permits events from a detached host.
|
|
7503
|
+
clearDispatchTimers(this);
|
|
7144
7504
|
try {
|
|
7145
7505
|
disconnectRenderTree(this);
|
|
7146
7506
|
}
|
|
@@ -7185,7 +7545,6 @@ function applyElementFunctionality(constructor) {
|
|
|
7185
7545
|
if (clearRenderTimers(this)) {
|
|
7186
7546
|
this[PENDING_RECONNECT_RENDER] = true;
|
|
7187
7547
|
}
|
|
7188
|
-
clearDispatchTimers(this);
|
|
7189
7548
|
// @moved / @adopted debounce-throttle timers, likewise dropped so they
|
|
7190
7549
|
// don't fire on a dead element.
|
|
7191
7550
|
clearLifecycleTimers(this, MOVED_TIMERS);
|
|
@@ -7194,6 +7553,11 @@ function applyElementFunctionality(constructor) {
|
|
|
7194
7553
|
constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
|
|
7195
7554
|
originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
|
|
7196
7555
|
if (name === 'controller') {
|
|
7556
|
+
// Parsing a dynamic controller binding briefly creates this marker
|
|
7557
|
+
// attribute on the inert template element. It is renderer metadata,
|
|
7558
|
+
// not a registry name, so it must never start an attachment.
|
|
7559
|
+
if (hasTemplateMarker(newValue))
|
|
7560
|
+
return;
|
|
7197
7561
|
// A class attachment reflects its decorator name for DOM diagnostics,
|
|
7198
7562
|
// but the class reference remains the only attachment authority.
|
|
7199
7563
|
if (this[CONTROLLER_ATTRIBUTE_SYNC])
|
|
@@ -7371,12 +7735,16 @@ function defineElement(tagName, constructor, context, options) {
|
|
|
7371
7735
|
constructor.prototype[RENDER_OPTIONS] = constructor.renderOptions || {};
|
|
7372
7736
|
}
|
|
7373
7737
|
applyElementFunctionality(constructor);
|
|
7374
|
-
|
|
7738
|
+
const existing = customElements.get(tagName);
|
|
7739
|
+
if (existing) {
|
|
7740
|
+
if (existing === constructor)
|
|
7741
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7375
7742
|
if (globalThis.SNICE_DEBUG)
|
|
7376
7743
|
console.warn(`[snice] "${tagName}" is already registered. Skipping.`);
|
|
7377
7744
|
return constructor;
|
|
7378
7745
|
}
|
|
7379
7746
|
customElements.define(tagName, constructor);
|
|
7747
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7380
7748
|
return constructor;
|
|
7381
7749
|
}
|
|
7382
7750
|
function element(tagName, options) {
|
|
@@ -8785,6 +9153,72 @@ function provideRouterContext(root, navigation) {
|
|
|
8785
9153
|
}, navigation);
|
|
8786
9154
|
}
|
|
8787
9155
|
|
|
9156
|
+
const EMPTY_QUERY_MARKER = '__snice_empty_query_value__';
|
|
9157
|
+
const QUERY_PARAM_PATTERN = /=:[\w-]+\)?$/;
|
|
9158
|
+
function containsDecodedMarker(url, marker) {
|
|
9159
|
+
if (url.includes(marker))
|
|
9160
|
+
return true;
|
|
9161
|
+
const queryIndex = url.indexOf('?');
|
|
9162
|
+
if (queryIndex === -1)
|
|
9163
|
+
return false;
|
|
9164
|
+
return url.slice(queryIndex + 1).split('&').some(part => {
|
|
9165
|
+
try {
|
|
9166
|
+
return decodeURIComponent(part).includes(marker);
|
|
9167
|
+
}
|
|
9168
|
+
catch {
|
|
9169
|
+
return false;
|
|
9170
|
+
}
|
|
9171
|
+
});
|
|
9172
|
+
}
|
|
9173
|
+
function emptyQueryMarker(route, url) {
|
|
9174
|
+
let marker = EMPTY_QUERY_MARKER;
|
|
9175
|
+
while (route.spec.includes(marker) || containsDecodedMarker(url, marker)) {
|
|
9176
|
+
marker += '_';
|
|
9177
|
+
}
|
|
9178
|
+
return marker;
|
|
9179
|
+
}
|
|
9180
|
+
function fillEmptyQueryParams(route, url, marker) {
|
|
9181
|
+
const urlQueryIndex = url.indexOf('?');
|
|
9182
|
+
const routeQueryIndex = route.spec.indexOf('?');
|
|
9183
|
+
if (urlQueryIndex === -1 || routeQueryIndex === -1)
|
|
9184
|
+
return null;
|
|
9185
|
+
const urlParts = url.slice(urlQueryIndex + 1).split('&');
|
|
9186
|
+
const routeParts = route.spec.slice(routeQueryIndex + 1).split('&');
|
|
9187
|
+
let changed = false;
|
|
9188
|
+
const normalizedParts = urlParts.map((part, index) => {
|
|
9189
|
+
if (part.endsWith('=') && QUERY_PARAM_PATTERN.test(routeParts[index] ?? '')) {
|
|
9190
|
+
changed = true;
|
|
9191
|
+
return `${part}${marker}`;
|
|
9192
|
+
}
|
|
9193
|
+
return part;
|
|
9194
|
+
});
|
|
9195
|
+
if (!changed)
|
|
9196
|
+
return null;
|
|
9197
|
+
return `${url.slice(0, urlQueryIndex + 1)}${normalizedParts.join('&')}`;
|
|
9198
|
+
}
|
|
9199
|
+
/**
|
|
9200
|
+
* Match through pica-route while preserving legal empty query parameter values.
|
|
9201
|
+
* pica-route 1.1.2 rejects an empty param and therefore discards the whole route.
|
|
9202
|
+
* Keep this compatibility shim in sync with packages/react/src/route-match.ts.
|
|
9203
|
+
*/
|
|
9204
|
+
function matchRoute(route, url) {
|
|
9205
|
+
const directMatch = route.match(url);
|
|
9206
|
+
if (directMatch !== false)
|
|
9207
|
+
return directMatch;
|
|
9208
|
+
const marker = emptyQueryMarker(route, url);
|
|
9209
|
+
const normalizedUrl = fillEmptyQueryParams(route, url, marker);
|
|
9210
|
+
if (normalizedUrl === null)
|
|
9211
|
+
return false;
|
|
9212
|
+
const params = route.match(normalizedUrl);
|
|
9213
|
+
if (params === false)
|
|
9214
|
+
return false;
|
|
9215
|
+
for (const name of Object.keys(params)) {
|
|
9216
|
+
if (params[name] === marker)
|
|
9217
|
+
params[name] = '';
|
|
9218
|
+
}
|
|
9219
|
+
return params;
|
|
9220
|
+
}
|
|
9221
|
+
|
|
8788
9222
|
// Router historically allows a later Router instance to take over the same
|
|
8789
9223
|
// target. Track only Router-owned provider releases so that handoff remains
|
|
8790
9224
|
// explicit without weakening duplicate protection for public provideContext().
|
|
@@ -8900,8 +9334,13 @@ function Router(options) {
|
|
|
8900
9334
|
delete this[CONTEXT_HANDLER];
|
|
8901
9335
|
};
|
|
8902
9336
|
// Define the custom element (skip if already registered)
|
|
8903
|
-
|
|
9337
|
+
const existing = win.customElements.get(pageOptions.tag);
|
|
9338
|
+
if (!existing) {
|
|
8904
9339
|
win.customElements.define(pageOptions.tag, constructor);
|
|
9340
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
9341
|
+
}
|
|
9342
|
+
else if (existing === constructor) {
|
|
9343
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
8905
9344
|
}
|
|
8906
9345
|
// Register the routes with guards, layout, and placard
|
|
8907
9346
|
pageOptions.routes.forEach(route => {
|
|
@@ -9151,7 +9590,7 @@ function Router(options) {
|
|
|
9151
9590
|
}
|
|
9152
9591
|
async function resolveRoute(path, target, stale) {
|
|
9153
9592
|
for (const route of routes) {
|
|
9154
|
-
const params = route.route
|
|
9593
|
+
const params = matchRoute(route.route, path);
|
|
9155
9594
|
const isMatch = params !== false;
|
|
9156
9595
|
if (!isMatch) {
|
|
9157
9596
|
continue;
|
|
@@ -9728,7 +10167,7 @@ if (typeof document !== 'undefined') {
|
|
|
9728
10167
|
}
|
|
9729
10168
|
|
|
9730
10169
|
/*!
|
|
9731
|
-
* snice v7.
|
|
10170
|
+
* snice v7.6.0
|
|
9732
10171
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
9733
10172
|
* (c) 2024
|
|
9734
10173
|
* Released under the MIT License.
|
|
@@ -9750,7 +10189,7 @@ if (!globalThis.snice) {
|
|
|
9750
10189
|
globalThis.snice.pendingControllerAttachments ??= new Map();
|
|
9751
10190
|
|
|
9752
10191
|
/*!
|
|
9753
|
-
* snice v7.
|
|
10192
|
+
* snice v7.6.0
|
|
9754
10193
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
9755
10194
|
* (c) 2024
|
|
9756
10195
|
* Released under the MIT License.
|