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
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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.
|
|
@@ -714,6 +714,18 @@ function createThrottled(fn, delay) {
|
|
|
714
714
|
}
|
|
715
715
|
};
|
|
716
716
|
}
|
|
717
|
+
/** Resolve and validate an @on/@dispatch debounce or throttle interval. */
|
|
718
|
+
function resolveEventTiming(instance, timing, decorator, option) {
|
|
719
|
+
if (timing === undefined)
|
|
720
|
+
return undefined;
|
|
721
|
+
const value = typeof timing === 'function'
|
|
722
|
+
? timing.call(instance)
|
|
723
|
+
: timing;
|
|
724
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
725
|
+
throw new TypeError(`${decorator} ${option} must resolve to a finite, non-negative number of milliseconds.`);
|
|
726
|
+
}
|
|
727
|
+
return value;
|
|
728
|
+
}
|
|
717
729
|
/**
|
|
718
730
|
* Resolve a scope option to a concrete EventTarget.
|
|
719
731
|
*
|
|
@@ -1847,6 +1859,11 @@ function on(eventName, selectorOrOptions, options) {
|
|
|
1847
1859
|
else if (selectorOrOptions === null && options) {
|
|
1848
1860
|
opts = options;
|
|
1849
1861
|
}
|
|
1862
|
+
// `target` is the options-object spelling of the positional selector; fold it
|
|
1863
|
+
// into the same delegation path. The positional argument wins when both are given.
|
|
1864
|
+
if (!selector && opts.target) {
|
|
1865
|
+
selector = opts.target;
|
|
1866
|
+
}
|
|
1850
1867
|
return function (originalMethod, context) {
|
|
1851
1868
|
const methodName = context.name;
|
|
1852
1869
|
context.addInitializer(function () {
|
|
@@ -1858,7 +1875,7 @@ function on(eventName, selectorOrOptions, options) {
|
|
|
1858
1875
|
// and child registrations pollute parent (and vice versa) via the
|
|
1859
1876
|
// prototype chain.
|
|
1860
1877
|
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
1861
|
-
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}`;
|
|
1878
|
+
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}::light:${opts.light ?? ''}::shadow:${opts.shadow ?? ''}`;
|
|
1862
1879
|
if (!Object.prototype.hasOwnProperty.call(constructor, ON_METHODS)) {
|
|
1863
1880
|
constructor[ON_METHODS] = new Set();
|
|
1864
1881
|
}
|
|
@@ -1938,6 +1955,10 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1938
1955
|
const currentMethod = instance[handler.methodName];
|
|
1939
1956
|
let boundMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
|
|
1940
1957
|
const handlerOptions = handler.options || {};
|
|
1958
|
+
const debounceDelay = resolveEventTiming(instance, handlerOptions.debounce, '@on', 'debounce');
|
|
1959
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
1960
|
+
? undefined
|
|
1961
|
+
: resolveEventTiming(instance, handlerOptions.throttle, '@on', 'throttle');
|
|
1941
1962
|
// Parse event name for key modifiers
|
|
1942
1963
|
// Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
|
|
1943
1964
|
// Only parse colons for keyboard events, not custom events
|
|
@@ -1958,13 +1979,13 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1958
1979
|
if (delimiterIndex <= 0)
|
|
1959
1980
|
warnIfModifierMisuse(handler.eventName);
|
|
1960
1981
|
// Apply debounce (takes precedence over throttle)
|
|
1961
|
-
if (
|
|
1962
|
-
const debounced = createDebounced(boundMethod,
|
|
1982
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
1983
|
+
const debounced = createDebounced(boundMethod, debounceDelay);
|
|
1963
1984
|
instance[CLEANUP].eventCancels.push(() => debounced.cancel());
|
|
1964
1985
|
boundMethod = debounced;
|
|
1965
1986
|
}
|
|
1966
|
-
else if (
|
|
1967
|
-
boundMethod = throttle$1(boundMethod,
|
|
1987
|
+
else if (throttleDelay && throttleDelay > 0) {
|
|
1988
|
+
boundMethod = throttle$1(boundMethod, throttleDelay);
|
|
1968
1989
|
}
|
|
1969
1990
|
// Create event handler with key modifier support
|
|
1970
1991
|
// Uses shared keyboard filter implementation from parts.ts
|
|
@@ -1972,15 +1993,21 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1972
1993
|
if (keyModifier && ['keydown', 'keyup', 'keypress'].includes(baseEventName)) {
|
|
1973
1994
|
keyFilter = parseKeyboardFilter(keyModifier);
|
|
1974
1995
|
}
|
|
1996
|
+
// Returns whether the method actually ran, so `once` can be managed
|
|
1997
|
+
// manually: native `once` consumes the listener on ANY invocation — a
|
|
1998
|
+
// non-matching key or (with multiple tree listeners) a different root
|
|
1999
|
+
// would either eat the single firing or allow a second one.
|
|
1975
2000
|
const createKeyModifierHandler = (method) => {
|
|
1976
2001
|
if (!keyFilter) {
|
|
1977
|
-
return method;
|
|
2002
|
+
return (event) => { method(event); return true; };
|
|
1978
2003
|
}
|
|
1979
2004
|
return (event) => {
|
|
1980
2005
|
const keyEvent = event;
|
|
1981
2006
|
if (matchesKeyboardFilter(keyEvent, keyFilter)) {
|
|
1982
2007
|
method(event);
|
|
2008
|
+
return true;
|
|
1983
2009
|
}
|
|
2010
|
+
return false;
|
|
1984
2011
|
};
|
|
1985
2012
|
};
|
|
1986
2013
|
// Apply key modifier wrapper
|
|
@@ -1998,6 +2025,20 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
1998
2025
|
continue;
|
|
1999
2026
|
}
|
|
2000
2027
|
const hasExplicitTarget = hasExplicitScope || hasDaemon;
|
|
2028
|
+
// Tree toggles — the same light/shadow pair @query uses. Both default to
|
|
2029
|
+
// true: direct listeners attach in both trees, delegated listeners match
|
|
2030
|
+
// in both. An explicit scope/daemon owns the listener target outright, so
|
|
2031
|
+
// the flags are meaningless there and ignored with a warning.
|
|
2032
|
+
const inShadow = handlerOptions.shadow !== false;
|
|
2033
|
+
const inLight = handlerOptions.light !== false;
|
|
2034
|
+
if (hasExplicitTarget
|
|
2035
|
+
&& (handlerOptions.light !== undefined || handlerOptions.shadow !== undefined)) {
|
|
2036
|
+
console.warn(`[snice/@on] light/shadow are ignored for "${handler.eventName}" — an explicit scope/daemon owns the listener target.`);
|
|
2037
|
+
}
|
|
2038
|
+
if (!hasExplicitTarget && !inShadow && !inLight) {
|
|
2039
|
+
console.warn(`[snice/@on] "${handler.eventName}" disables both light and shadow — listener skipped.`);
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2001
2042
|
let scopedTarget = null;
|
|
2002
2043
|
if (hasDaemon) {
|
|
2003
2044
|
try {
|
|
@@ -2019,56 +2060,125 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2019
2060
|
}
|
|
2020
2061
|
// Main event handler with error handling and event delegation
|
|
2021
2062
|
if (handler.selector) {
|
|
2022
|
-
// Delegated event handling.
|
|
2023
|
-
// -
|
|
2024
|
-
// -
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
(
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
keyModifierMethod(event);
|
|
2043
|
-
}
|
|
2044
|
-
catch (error) {
|
|
2045
|
-
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2046
|
-
}
|
|
2047
|
-
};
|
|
2063
|
+
// Delegated event handling. Listener roots by tree toggle:
|
|
2064
|
+
// - shadow → the shadow root (when present)
|
|
2065
|
+
// - light → the host element (hears light-DOM bubbles)
|
|
2066
|
+
// - explicit scope: the scoped target only
|
|
2067
|
+
const componentShadowRoot = targetElement.shadowRoot;
|
|
2068
|
+
const delegationRoots = hasExplicitTarget
|
|
2069
|
+
? [scopedTarget]
|
|
2070
|
+
: [
|
|
2071
|
+
...(inShadow && componentShadowRoot ? [componentShadowRoot] : []),
|
|
2072
|
+
...(inLight ? [targetElement] : []),
|
|
2073
|
+
];
|
|
2074
|
+
if (delegationRoots.length === 0) {
|
|
2075
|
+
console.warn(`[snice/@on] "${handler.eventName}" cannot delegate "${handler.selector}" — no shadow root and light disabled; listener skipped.`);
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
// One event can reach more than one root (e.g. a slotted click seen by
|
|
2079
|
+
// both the shadow root and the host); a per-handler Symbol keeps the
|
|
2080
|
+
// handler to one invocation per event. The symbol is only stamped on a
|
|
2081
|
+
// match so a non-matching root never blocks the other.
|
|
2082
|
+
const handledSymbol = Symbol();
|
|
2048
2083
|
// Auto-enable capture for non-bubbling events when using delegation
|
|
2049
2084
|
const needsCapture = NON_BUBBLING_EVENTS.has(baseEventName);
|
|
2050
2085
|
const useCapture = handlerOptions.capture !== undefined
|
|
2051
2086
|
? handlerOptions.capture
|
|
2052
2087
|
: needsCapture;
|
|
2088
|
+
// `once` is managed manually (never native): the handler fires exactly
|
|
2089
|
+
// once — a non-matching event or key must not consume it, and a match
|
|
2090
|
+
// must retire the listeners on EVERY root, not just the one that fired.
|
|
2053
2091
|
const listenerOptions = {
|
|
2054
2092
|
capture: useCapture,
|
|
2055
|
-
once:
|
|
2093
|
+
once: false,
|
|
2056
2094
|
passive: handlerOptions.passive || false,
|
|
2057
2095
|
};
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2096
|
+
const listenerGroup = [];
|
|
2097
|
+
const removeGroup = () => {
|
|
2098
|
+
for (const entry of listenerGroup) {
|
|
2099
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
for (const eventRoot of delegationRoots) {
|
|
2103
|
+
// Delegation matches only elements visible in the listener's own tree
|
|
2104
|
+
// scope: a shadow-root listener matches its shadow tree, anything else
|
|
2105
|
+
// matches its containing document/root. This mirrors native retargeting
|
|
2106
|
+
// (child-component internals never match) while still matching a shadow
|
|
2107
|
+
// wrapper when the click lands on light-DOM content slotted into it —
|
|
2108
|
+
// `target.closest()` alone cannot cross that slot boundary.
|
|
2109
|
+
const treeScope = eventRoot instanceof ShadowRoot
|
|
2110
|
+
? eventRoot
|
|
2111
|
+
: (eventRoot.getRootNode?.() ?? document);
|
|
2112
|
+
const findDelegateMatch = (event) => {
|
|
2113
|
+
if (typeof event.composedPath !== 'function') {
|
|
2114
|
+
// Fallback for environments without composedPath: ancestor walk.
|
|
2115
|
+
const target = event.target;
|
|
2116
|
+
return (target.matches && target.matches(handler.selector) && target)
|
|
2117
|
+
|| (target.closest && target.closest(handler.selector))
|
|
2118
|
+
|| null;
|
|
2119
|
+
}
|
|
2120
|
+
for (const node of event.composedPath()) {
|
|
2121
|
+
const el = node;
|
|
2122
|
+
if (el === eventRoot)
|
|
2123
|
+
break;
|
|
2124
|
+
if (el.nodeType !== 1)
|
|
2125
|
+
continue;
|
|
2126
|
+
if (el.getRootNode() !== treeScope)
|
|
2127
|
+
continue;
|
|
2128
|
+
if (el.matches(handler.selector))
|
|
2129
|
+
return el;
|
|
2130
|
+
}
|
|
2131
|
+
return null;
|
|
2132
|
+
};
|
|
2133
|
+
const delegatedHandler = (event) => {
|
|
2134
|
+
if (event[handledSymbol])
|
|
2135
|
+
return;
|
|
2136
|
+
const matchingElement = findDelegateMatch(event);
|
|
2137
|
+
if (!matchingElement)
|
|
2138
|
+
return;
|
|
2139
|
+
event[handledSymbol] = true;
|
|
2140
|
+
if (handlerOptions.preventDefault)
|
|
2141
|
+
event.preventDefault();
|
|
2142
|
+
if (handlerOptions.stopPropagation) {
|
|
2143
|
+
event.stopPropagation();
|
|
2144
|
+
event.stopImmediatePropagation();
|
|
2145
|
+
}
|
|
2146
|
+
let fired = false;
|
|
2147
|
+
try {
|
|
2148
|
+
fired = keyModifierMethod(event);
|
|
2149
|
+
}
|
|
2150
|
+
catch (error) {
|
|
2151
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2152
|
+
fired = true;
|
|
2153
|
+
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2154
|
+
}
|
|
2155
|
+
if (fired && handlerOptions.once)
|
|
2156
|
+
removeGroup();
|
|
2157
|
+
};
|
|
2158
|
+
eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
2159
|
+
listenerGroup.push({ target: eventRoot, handler: delegatedHandler });
|
|
2160
|
+
instance[CLEANUP].events.push({
|
|
2161
|
+
target: eventRoot,
|
|
2162
|
+
eventName: baseEventName,
|
|
2163
|
+
handler: delegatedHandler,
|
|
2164
|
+
options: listenerOptions,
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2065
2167
|
}
|
|
2066
2168
|
else {
|
|
2067
2169
|
// Direct event handling.
|
|
2068
2170
|
// - Default: shadow root + host element (so events inside shadow and on
|
|
2069
2171
|
// the host itself both fire). A per-handler Symbol dedupes.
|
|
2172
|
+
// - light/shadow narrow the attachment: shadow → the shadow root
|
|
2173
|
+
// listener, light → the host listener.
|
|
2070
2174
|
// - With explicit scope: attach to the scoped target ONLY (no duplication).
|
|
2071
|
-
const shadowRoot = hasExplicitTarget
|
|
2175
|
+
const shadowRoot = (hasExplicitTarget || !inShadow)
|
|
2176
|
+
? null
|
|
2177
|
+
: targetElement.shadowRoot;
|
|
2178
|
+
if (!hasExplicitTarget && !shadowRoot && !inLight) {
|
|
2179
|
+
console.warn(`[snice/@on] "${handler.eventName}" requests shadow only but the element has no shadow root — listener skipped.`);
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2072
2182
|
// Per-handler private Symbol so dedup is scoped to THIS handler's two
|
|
2073
2183
|
// listeners (shadowRoot + host). Using Symbol.for() with the method name
|
|
2074
2184
|
// would collide across components that share a method name (e.g. a parent
|
|
@@ -2076,6 +2186,20 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2076
2186
|
// handler to be silently swallowed when events bubble up through a
|
|
2077
2187
|
// shadow boundary.
|
|
2078
2188
|
const handledSymbol = Symbol();
|
|
2189
|
+
// `once` is managed manually (never native): a key-filtered handler must
|
|
2190
|
+
// not be consumed by a non-matching key, and with listeners in two trees
|
|
2191
|
+
// the first real firing must retire BOTH, not just the one that fired.
|
|
2192
|
+
const listenerOptions = {
|
|
2193
|
+
capture: handlerOptions.capture || false,
|
|
2194
|
+
once: false,
|
|
2195
|
+
passive: handlerOptions.passive || false,
|
|
2196
|
+
};
|
|
2197
|
+
const listenerGroup = [];
|
|
2198
|
+
const removeGroup = () => {
|
|
2199
|
+
for (const entry of listenerGroup) {
|
|
2200
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2079
2203
|
const wrappedMethod = (event) => {
|
|
2080
2204
|
if (event[handledSymbol])
|
|
2081
2205
|
return;
|
|
@@ -2084,46 +2208,39 @@ function setupEventHandlers(instance, targetElement) {
|
|
|
2084
2208
|
event.preventDefault();
|
|
2085
2209
|
if (handlerOptions.stopPropagation)
|
|
2086
2210
|
event.stopPropagation();
|
|
2211
|
+
let fired = false;
|
|
2087
2212
|
try {
|
|
2088
|
-
keyModifierMethod(event);
|
|
2213
|
+
fired = keyModifierMethod(event);
|
|
2089
2214
|
}
|
|
2090
2215
|
catch (error) {
|
|
2216
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2217
|
+
fired = true;
|
|
2091
2218
|
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2092
2219
|
}
|
|
2220
|
+
if (fired && handlerOptions.once)
|
|
2221
|
+
removeGroup();
|
|
2093
2222
|
};
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
passive: handlerOptions.passive || false,
|
|
2098
|
-
};
|
|
2099
|
-
if (hasExplicitTarget) {
|
|
2100
|
-
scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2223
|
+
const attach = (target) => {
|
|
2224
|
+
target.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2225
|
+
listenerGroup.push({ target, handler: wrappedMethod });
|
|
2101
2226
|
instance[CLEANUP].events.push({
|
|
2102
|
-
target
|
|
2227
|
+
target,
|
|
2103
2228
|
eventName: baseEventName,
|
|
2104
2229
|
handler: wrappedMethod,
|
|
2105
2230
|
options: listenerOptions,
|
|
2106
2231
|
});
|
|
2232
|
+
};
|
|
2233
|
+
if (hasExplicitTarget) {
|
|
2234
|
+
attach(scopedTarget);
|
|
2107
2235
|
continue;
|
|
2108
2236
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
shadowRoot
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
options: listenerOptions,
|
|
2117
|
-
});
|
|
2118
|
-
}
|
|
2119
|
-
// Also listen on host element (for clicks on host itself or when no shadow root)
|
|
2120
|
-
targetElement.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2121
|
-
instance[CLEANUP].events.push({
|
|
2122
|
-
target: targetElement,
|
|
2123
|
-
eventName: baseEventName,
|
|
2124
|
-
handler: wrappedMethod,
|
|
2125
|
-
options: listenerOptions,
|
|
2126
|
-
});
|
|
2237
|
+
// Listen on shadow root for events inside shadow DOM
|
|
2238
|
+
if (shadowRoot)
|
|
2239
|
+
attach(shadowRoot);
|
|
2240
|
+
// Also listen on host element (for clicks on host itself, light-DOM
|
|
2241
|
+
// bubbles, or when no shadow root) — unless light is disabled.
|
|
2242
|
+
if (inLight)
|
|
2243
|
+
attach(targetElement);
|
|
2127
2244
|
}
|
|
2128
2245
|
}
|
|
2129
2246
|
}
|
|
@@ -2143,6 +2260,185 @@ function cleanupEventHandlers(instance) {
|
|
|
2143
2260
|
instance[CLEANUP].events = [];
|
|
2144
2261
|
}
|
|
2145
2262
|
|
|
2263
|
+
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
2264
|
+
const DISPATCH_GENERATION = getSymbol('dispatch-generation');
|
|
2265
|
+
function isDispatchGenerationActive(instance, generation) {
|
|
2266
|
+
return (instance[DISPATCH_GENERATION] ?? 0) === generation;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Decorator that automatically dispatches a custom event after a method is called.
|
|
2270
|
+
* The return value of the method becomes the event detail.
|
|
2271
|
+
*
|
|
2272
|
+
* @param eventName The name of the event to dispatch
|
|
2273
|
+
* @param options Optional configuration extending EventInit
|
|
2274
|
+
*/
|
|
2275
|
+
function dispatch(eventName, options) {
|
|
2276
|
+
return function (originalMethod, _context) {
|
|
2277
|
+
// Decorator-instance identity avoids collisions such as event `a_b` on
|
|
2278
|
+
// method `c` versus event `a` on method `b_c`.
|
|
2279
|
+
const timerKey = Symbol(`@dispatch:${eventName}`);
|
|
2280
|
+
return function (...args) {
|
|
2281
|
+
// Resolve against the actual decorated instance for every invocation.
|
|
2282
|
+
// Capture the generation before calling an async method so teardown can
|
|
2283
|
+
// invalidate work that has not reached the scheduling step yet.
|
|
2284
|
+
const generation = this[DISPATCH_GENERATION] ?? 0;
|
|
2285
|
+
// Create timing wrappers for dispatch (per-instance)
|
|
2286
|
+
if (!this[DISPATCH_TIMERS]) {
|
|
2287
|
+
this[DISPATCH_TIMERS] = new Map();
|
|
2288
|
+
}
|
|
2289
|
+
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
2290
|
+
this[DISPATCH_TIMERS].set(timerKey, {
|
|
2291
|
+
debounceTimeout: null,
|
|
2292
|
+
throttleLastCall: 0,
|
|
2293
|
+
throttleTimeout: null,
|
|
2294
|
+
invocation: 0,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
2298
|
+
const debounceDelay = resolveEventTiming(this, options?.debounce, '@dispatch', 'debounce');
|
|
2299
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
2300
|
+
? undefined
|
|
2301
|
+
: resolveEventTiming(this, options?.throttle, '@dispatch', 'throttle');
|
|
2302
|
+
const invocation = ++timers.invocation;
|
|
2303
|
+
// A new invocation supersedes deferred work for this decorated method.
|
|
2304
|
+
// In particular, a resolver changing debounce to 0 must not leave the
|
|
2305
|
+
// previous positive-delay timer armed. Throttle trailing work is also
|
|
2306
|
+
// rebuilt below using this invocation's freshly resolved interval.
|
|
2307
|
+
if (timers.debounceTimeout)
|
|
2308
|
+
clearTimeout(timers.debounceTimeout);
|
|
2309
|
+
timers.debounceTimeout = null;
|
|
2310
|
+
if (timers.throttleTimeout)
|
|
2311
|
+
clearTimeout(timers.throttleTimeout);
|
|
2312
|
+
timers.throttleTimeout = null;
|
|
2313
|
+
timers.latestDetail = undefined;
|
|
2314
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2315
|
+
// Switching from throttle to debounce starts a new timing regime.
|
|
2316
|
+
timers.throttleLastCall = 0;
|
|
2317
|
+
}
|
|
2318
|
+
else if (!throttleDelay || throttleDelay <= 0) {
|
|
2319
|
+
// An un-timed invocation cancels both old regimes completely.
|
|
2320
|
+
timers.throttleLastCall = 0;
|
|
2321
|
+
}
|
|
2322
|
+
// Call the original method with preserved this context
|
|
2323
|
+
const result = originalMethod.apply(this, args);
|
|
2324
|
+
// Helper to dispatch the event
|
|
2325
|
+
const doDispatch = (detail) => {
|
|
2326
|
+
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
2327
|
+
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
2331
|
+
// not belong in the CustomEvent constructor dictionary.
|
|
2332
|
+
const event = new CustomEvent(eventName, {
|
|
2333
|
+
bubbles: options?.bubbles ?? true,
|
|
2334
|
+
cancelable: options?.cancelable ?? false,
|
|
2335
|
+
composed: options?.composed ?? true,
|
|
2336
|
+
detail
|
|
2337
|
+
});
|
|
2338
|
+
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
2339
|
+
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
2340
|
+
}
|
|
2341
|
+
if (options?.daemon !== undefined) {
|
|
2342
|
+
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
if (options?.scope !== undefined) {
|
|
2346
|
+
const target = resolveScope(this, options.scope);
|
|
2347
|
+
if (!target) {
|
|
2348
|
+
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
target.dispatchEvent(event);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const target = defaultCommunicationTarget(this);
|
|
2355
|
+
if (!target) {
|
|
2356
|
+
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
2357
|
+
}
|
|
2358
|
+
target.dispatchEvent(event);
|
|
2359
|
+
};
|
|
2360
|
+
// Helper to handle timed dispatch
|
|
2361
|
+
const timedDispatch = (detail) => {
|
|
2362
|
+
if (!isDispatchGenerationActive(this, generation))
|
|
2363
|
+
return;
|
|
2364
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2365
|
+
timers.debounceTimeout = setTimeout(() => {
|
|
2366
|
+
timers.debounceTimeout = null;
|
|
2367
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2368
|
+
doDispatch(detail);
|
|
2369
|
+
}
|
|
2370
|
+
}, debounceDelay);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (!throttleDelay || throttleDelay <= 0) {
|
|
2374
|
+
doDispatch(detail);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const now = Date.now();
|
|
2378
|
+
const remaining = throttleDelay - (now - timers.throttleLastCall);
|
|
2379
|
+
if (remaining <= 0) {
|
|
2380
|
+
clearTimeout(timers.throttleTimeout);
|
|
2381
|
+
timers.throttleLastCall = now;
|
|
2382
|
+
doDispatch(detail);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
2386
|
+
// data, not the first-suppressed-call detail captured by closure.
|
|
2387
|
+
timers.latestDetail = detail;
|
|
2388
|
+
// Always schedule from the last actual dispatch using the interval
|
|
2389
|
+
// resolved for THIS invocation. A later invocation can therefore
|
|
2390
|
+
// lengthen or shorten the pending trailing window coherently.
|
|
2391
|
+
timers.throttleTimeout = setTimeout(() => {
|
|
2392
|
+
timers.throttleLastCall = Date.now();
|
|
2393
|
+
timers.throttleTimeout = null;
|
|
2394
|
+
const d = timers.latestDetail;
|
|
2395
|
+
timers.latestDetail = undefined;
|
|
2396
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2397
|
+
doDispatch(d);
|
|
2398
|
+
}
|
|
2399
|
+
}, remaining);
|
|
2400
|
+
};
|
|
2401
|
+
// Handle async methods
|
|
2402
|
+
if (result instanceof Promise) {
|
|
2403
|
+
return result.then((resolvedResult) => {
|
|
2404
|
+
const usesDeferredTiming = (debounceDelay ?? 0) > 0 || (throttleDelay ?? 0) > 0;
|
|
2405
|
+
if (isDispatchGenerationActive(this, generation)
|
|
2406
|
+
&& (!usesDeferredTiming || timers.invocation === invocation)) {
|
|
2407
|
+
timedDispatch(resolvedResult);
|
|
2408
|
+
}
|
|
2409
|
+
return resolvedResult;
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
// Sync method
|
|
2413
|
+
timedDispatch(result);
|
|
2414
|
+
return result;
|
|
2415
|
+
};
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
2420
|
+
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
2421
|
+
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
2422
|
+
*/
|
|
2423
|
+
function clearDispatchTimers(instance) {
|
|
2424
|
+
const generation = (instance[DISPATCH_GENERATION] ?? 0) + 1;
|
|
2425
|
+
instance[DISPATCH_GENERATION] = generation;
|
|
2426
|
+
const timers = instance[DISPATCH_TIMERS];
|
|
2427
|
+
if (!timers)
|
|
2428
|
+
return;
|
|
2429
|
+
for (const t of timers.values()) {
|
|
2430
|
+
if (t.debounceTimeout)
|
|
2431
|
+
clearTimeout(t.debounceTimeout);
|
|
2432
|
+
if (t.throttleTimeout)
|
|
2433
|
+
clearTimeout(t.throttleTimeout);
|
|
2434
|
+
t.debounceTimeout = null;
|
|
2435
|
+
t.throttleTimeout = null;
|
|
2436
|
+
t.throttleLastCall = 0;
|
|
2437
|
+
t.latestDetail = undefined;
|
|
2438
|
+
t.invocation = (t.invocation ?? 0) + 1;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2146
2442
|
/**
|
|
2147
2443
|
* @context decorator for receiving router context updates
|
|
2148
2444
|
*/
|
|
@@ -2681,6 +2977,9 @@ async function detachController(element) {
|
|
|
2681
2977
|
if (!controllerInstance) {
|
|
2682
2978
|
return;
|
|
2683
2979
|
}
|
|
2980
|
+
// Invalidate dispatch work before every teardown path, including an attach
|
|
2981
|
+
// that is still waiting for host readiness and will return early below.
|
|
2982
|
+
clearDispatchTimers(controllerInstance);
|
|
2684
2983
|
// Claim the controller immediately — before any await — so a second detach
|
|
2685
2984
|
// that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
|
|
2686
2985
|
// controller reassignment) finds no instance and returns, instead of running
|
|
@@ -2713,6 +3012,8 @@ async function detachController(element) {
|
|
|
2713
3012
|
else {
|
|
2714
3013
|
await controllerInstance.detach(element);
|
|
2715
3014
|
}
|
|
3015
|
+
// detach() itself may have invoked a decorated dispatch method.
|
|
3016
|
+
clearDispatchTimers(controllerInstance);
|
|
2716
3017
|
controllerInstance.element = null;
|
|
2717
3018
|
// Cleanup @observe observers for controller
|
|
2718
3019
|
cleanupObservers(controllerInstance);
|
|
@@ -3072,36 +3373,184 @@ function findRenderHost(node) {
|
|
|
3072
3373
|
return null;
|
|
3073
3374
|
}
|
|
3074
3375
|
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
const
|
|
3078
|
-
|
|
3376
|
+
const SAFE_TAG = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
3377
|
+
const SAFE_CLASS = /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/;
|
|
3378
|
+
const AMBIGUOUS_RENDER_HOST_IDENTITY = Symbol('ambiguous-render-host-identity');
|
|
3379
|
+
// Keys are weak; values contain only a sanitized label or the ambiguity
|
|
3380
|
+
// sentinel. The same frozen identity is recorded for the exact constructor
|
|
3381
|
+
// and its exact prototype only after the caller verifies registration.
|
|
3382
|
+
const registeredRenderHosts = new WeakMap();
|
|
3383
|
+
function registerRenderHostIdentity(constructor, tagName) {
|
|
3384
|
+
try {
|
|
3385
|
+
if (typeof constructor !== 'function' || !SAFE_TAG.test(tagName))
|
|
3386
|
+
return;
|
|
3387
|
+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(constructor, 'prototype');
|
|
3388
|
+
const prototype = prototypeDescriptor && 'value' in prototypeDescriptor
|
|
3389
|
+
? prototypeDescriptor.value
|
|
3390
|
+
: undefined;
|
|
3391
|
+
if ((typeof prototype !== 'object' && typeof prototype !== 'function') || prototype === null)
|
|
3392
|
+
return;
|
|
3393
|
+
const nameDescriptor = Object.getOwnPropertyDescriptor(constructor, 'name');
|
|
3394
|
+
const name = nameDescriptor && 'value' in nameDescriptor && typeof nameDescriptor.value === 'string'
|
|
3395
|
+
? nameDescriptor.value
|
|
3396
|
+
: '';
|
|
3397
|
+
const className = SAFE_CLASS.test(name) ? name : '';
|
|
3398
|
+
const identity = Object.freeze({
|
|
3399
|
+
label: className ? `<${tagName}> (${className})` : `<${tagName}>`,
|
|
3400
|
+
});
|
|
3401
|
+
const constructorState = registeredRenderHosts.get(constructor);
|
|
3402
|
+
const prototypeState = registeredRenderHosts.get(prototype);
|
|
3403
|
+
const existingStates = [constructorState, prototypeState].filter((state) => state !== undefined);
|
|
3404
|
+
const ambiguous = existingStates.some(state => state === AMBIGUOUS_RENDER_HOST_IDENTITY || state.label !== identity.label);
|
|
3405
|
+
const nextState = ambiguous
|
|
3406
|
+
? AMBIGUOUS_RENDER_HOST_IDENTITY
|
|
3407
|
+
: existingStates[0] ?? identity;
|
|
3408
|
+
// Update both exact keys in the same synchronous registration call. Once
|
|
3409
|
+
// either key is ambiguous, no later registration can restore attribution.
|
|
3410
|
+
registeredRenderHosts.set(constructor, nextState);
|
|
3411
|
+
registeredRenderHosts.set(prototype, nextState);
|
|
3412
|
+
}
|
|
3413
|
+
catch {
|
|
3414
|
+
// Registration diagnostics are best-effort and must not affect the
|
|
3415
|
+
// successful custom-elements registration that already occurred.
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
function registeredIdentity(host) {
|
|
3419
|
+
const prototype = Object.getPrototypeOf(host);
|
|
3420
|
+
if (!prototype)
|
|
3421
|
+
return undefined;
|
|
3422
|
+
const constructorDescriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor');
|
|
3423
|
+
const constructor = constructorDescriptor && 'value' in constructorDescriptor
|
|
3424
|
+
? constructorDescriptor.value
|
|
3425
|
+
: undefined;
|
|
3426
|
+
if (typeof constructor !== 'function')
|
|
3427
|
+
return undefined;
|
|
3428
|
+
const prototypeIdentity = registeredRenderHosts.get(prototype);
|
|
3429
|
+
const constructorIdentity = registeredRenderHosts.get(constructor);
|
|
3430
|
+
return prototypeIdentity !== AMBIGUOUS_RENDER_HOST_IDENTITY &&
|
|
3431
|
+
prototypeIdentity === constructorIdentity
|
|
3432
|
+
? prototypeIdentity
|
|
3433
|
+
: undefined;
|
|
3434
|
+
}
|
|
3435
|
+
/** Capture once at the render boundary; failures fall back without masking the render error. */
|
|
3436
|
+
function captureRenderHostIdentity(host) {
|
|
3437
|
+
let label = '<element>';
|
|
3438
|
+
try {
|
|
3439
|
+
const registration = registeredIdentity(host);
|
|
3440
|
+
if (registration)
|
|
3441
|
+
label = registration.label;
|
|
3442
|
+
}
|
|
3443
|
+
catch {
|
|
3444
|
+
// Host/prototype objects can be proxied. Attribution is best-effort
|
|
3445
|
+
// and must never replace the original render or template error.
|
|
3446
|
+
}
|
|
3447
|
+
return Object.freeze({ label, marker: Symbol('render-host-identity') });
|
|
3448
|
+
}
|
|
3449
|
+
class ContextualRenderError extends Error {
|
|
3450
|
+
#identityMarker;
|
|
3451
|
+
constructor(identity, message, cause) {
|
|
3452
|
+
super(message, { cause });
|
|
3453
|
+
this.#identityMarker = identity.marker;
|
|
3454
|
+
}
|
|
3455
|
+
belongsTo(identity) {
|
|
3456
|
+
return this.#identityMarker === identity.marker;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function contextualizeRenderError(identity, error) {
|
|
3460
|
+
if (error instanceof ContextualRenderError && error.belongsTo(identity))
|
|
3461
|
+
return error;
|
|
3462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3463
|
+
const contextual = new ContextualRenderError(identity, `snice: render failed for ${identity.label}: ${message}`, error);
|
|
3464
|
+
if (error instanceof Error)
|
|
3465
|
+
contextual.name = error.name;
|
|
3466
|
+
return contextual;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
// Unique marker for dynamic template parts. It parses as a comment node but
|
|
3470
|
+
// does not get escaped in attributes.
|
|
3471
|
+
const templateMarker = `snice$${Math.random().toFixed(9).slice(2)}$`;
|
|
3472
|
+
const templateMarkerMatch = '?' + templateMarker;
|
|
3079
3473
|
// A true comment, not a processing instruction (<?...>): in HTML content both
|
|
3080
3474
|
// parse to the same comment node, but PIs are dropped inside foreign content
|
|
3081
3475
|
// (<svg>), which would silently kill any node binding inside an svg block.
|
|
3082
|
-
const
|
|
3476
|
+
const templateNodeMarker = `<!--${templateMarkerMatch}-->`;
|
|
3083
3477
|
// Escape the `$` chars — as a bare RegExp they'd be end anchors and the
|
|
3084
3478
|
// pattern would never match, so marker-bearing text/comments never split.
|
|
3085
|
-
const
|
|
3479
|
+
const templateMarkerRegex = new RegExp(templateMarker.replace(/\$/g, '\\$'), 'g');
|
|
3480
|
+
function hasTemplateMarker(value) {
|
|
3481
|
+
return value?.includes(templateMarker) ?? false;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3086
3484
|
// Template cache - templates with same string array can be reused
|
|
3087
3485
|
const templateCache = new WeakMap();
|
|
3088
3486
|
// Sentinel for "not yet set" - distinct from undefined/null
|
|
3089
3487
|
const NOT_COMMITTED = Symbol('not-committed');
|
|
3090
|
-
|
|
3488
|
+
const pendingDefinitionRegistries = new WeakMap();
|
|
3489
|
+
function isUpgradedBy(element, constructor) {
|
|
3490
|
+
return Object.prototype.isPrototypeOf.call(constructor.prototype, element);
|
|
3491
|
+
}
|
|
3492
|
+
function flushPreUpgradePropertyBindings(element, registry) {
|
|
3493
|
+
const tagName = element.localName;
|
|
3494
|
+
const constructor = tagName && registry?.get(tagName);
|
|
3495
|
+
if (!constructor || !isUpgradedBy(element, constructor))
|
|
3496
|
+
return;
|
|
3497
|
+
const target = element;
|
|
3498
|
+
const markedBindings = target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3499
|
+
if (!markedBindings?.size)
|
|
3500
|
+
return;
|
|
3501
|
+
for (const propertyName of Array.from(markedBindings)) {
|
|
3502
|
+
// A Snice @property initializer consumes its own marker and expando while
|
|
3503
|
+
// the element upgrades. Anything left belongs to a class that retained
|
|
3504
|
+
// the native/custom property channel, so replay the latest parked value
|
|
3505
|
+
// through that now-upgraded channel.
|
|
3506
|
+
if (Object.prototype.hasOwnProperty.call(target, propertyName)) {
|
|
3507
|
+
const value = target[propertyName];
|
|
3508
|
+
delete target[propertyName];
|
|
3509
|
+
target[propertyName] = value;
|
|
3510
|
+
}
|
|
3511
|
+
markedBindings.delete(propertyName);
|
|
3512
|
+
}
|
|
3513
|
+
delete target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3514
|
+
}
|
|
3515
|
+
function schedulePreUpgradePropertyFlush(element, registry) {
|
|
3516
|
+
if (!registry?.whenDefined || pendingDefinitionRegistries.get(element) === registry)
|
|
3517
|
+
return;
|
|
3518
|
+
pendingDefinitionRegistries.set(element, registry);
|
|
3519
|
+
const reference = new WeakRef(element);
|
|
3520
|
+
void registry.whenDefined(element.localName).then(() => {
|
|
3521
|
+
const target = reference.deref();
|
|
3522
|
+
if (target)
|
|
3523
|
+
flushPreUpgradePropertyBindings(target, registry);
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
function markPreUpgradePropertyBinding(element, propertyName, registry) {
|
|
3091
3527
|
const tagName = element.localName;
|
|
3092
3528
|
if (!tagName?.includes('-'))
|
|
3093
3529
|
return false;
|
|
3094
|
-
const
|
|
3095
|
-
if (
|
|
3096
|
-
|
|
3530
|
+
const registeredConstructor = registry?.get(tagName);
|
|
3531
|
+
if (registeredConstructor) {
|
|
3532
|
+
// Template contents can still contain an unupgraded HTMLElement after the
|
|
3533
|
+
// definition has been registered. Registry presence alone therefore does
|
|
3534
|
+
// not prove that the reactive accessor exists on this clone yet.
|
|
3535
|
+
const isUpgraded = isUpgradedBy(element, registeredConstructor);
|
|
3536
|
+
if (isUpgraded)
|
|
3537
|
+
return false;
|
|
3538
|
+
// A registered non-Snice element (or a Snice element that did not
|
|
3539
|
+
// redeclare this member) must retain the native IDL channel. Parking its
|
|
3540
|
+
// role/title/etc. would replace native reflection with an own expando.
|
|
3541
|
+
const reactiveProperties = registeredConstructor[PROPERTIES];
|
|
3542
|
+
if (!reactiveProperties?.has(propertyName))
|
|
3543
|
+
return false;
|
|
3544
|
+
}
|
|
3097
3545
|
const target = element;
|
|
3098
3546
|
if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
|
|
3099
3547
|
target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
|
|
3100
3548
|
target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
|
|
3549
|
+
schedulePreUpgradePropertyFlush(element, registry);
|
|
3101
3550
|
return true;
|
|
3102
3551
|
}
|
|
3103
|
-
function commitPropertyValue(element, propertyName, value) {
|
|
3104
|
-
if (!markPreUpgradePropertyBinding(element, propertyName)) {
|
|
3552
|
+
function commitPropertyValue(element, propertyName, value, registry) {
|
|
3553
|
+
if (!markPreUpgradePropertyBinding(element, propertyName, registry)) {
|
|
3105
3554
|
element[propertyName] = value;
|
|
3106
3555
|
return;
|
|
3107
3556
|
}
|
|
@@ -3201,7 +3650,7 @@ class Template {
|
|
|
3201
3650
|
if (tagName === 'if') {
|
|
3202
3651
|
// <if value="${condition}">children</if>
|
|
3203
3652
|
const valueAttr = element.getAttribute('value');
|
|
3204
|
-
if (valueAttr && valueAttr.includes(
|
|
3653
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3205
3654
|
// Remove the value attribute
|
|
3206
3655
|
element.removeAttribute('value');
|
|
3207
3656
|
this.parts.push({
|
|
@@ -3220,7 +3669,7 @@ class Template {
|
|
|
3220
3669
|
if (tagName === 'case') {
|
|
3221
3670
|
// <case value="${value}">children</case>
|
|
3222
3671
|
const valueAttr = element.getAttribute('value');
|
|
3223
|
-
if (valueAttr && valueAttr.includes(
|
|
3672
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3224
3673
|
for (const child of Array.from(element.childNodes)) {
|
|
3225
3674
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
3226
3675
|
const branchTag = child.tagName.toLowerCase();
|
|
@@ -3229,7 +3678,7 @@ class Template {
|
|
|
3229
3678
|
}
|
|
3230
3679
|
}
|
|
3231
3680
|
else if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim() ||
|
|
3232
|
-
child.nodeType === Node.COMMENT_NODE && child.data.includes(
|
|
3681
|
+
child.nodeType === Node.COMMENT_NODE && child.data.includes(templateMarker)) {
|
|
3233
3682
|
throw new Error('snice: content inside <case> must be nested in a <when> or <default> branch.');
|
|
3234
3683
|
}
|
|
3235
3684
|
}
|
|
@@ -3249,7 +3698,7 @@ class Template {
|
|
|
3249
3698
|
}
|
|
3250
3699
|
if (tagName === 'else-if') {
|
|
3251
3700
|
const valueAttr = element.getAttribute('value');
|
|
3252
|
-
if (valueAttr && valueAttr.includes(
|
|
3701
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3253
3702
|
element.removeAttribute('value');
|
|
3254
3703
|
this.parts.push({
|
|
3255
3704
|
type: 'conditional-else-if',
|
|
@@ -3264,7 +3713,7 @@ class Template {
|
|
|
3264
3713
|
}
|
|
3265
3714
|
if (tagName === 'when') {
|
|
3266
3715
|
const valueAttr = element.getAttribute('value');
|
|
3267
|
-
if (valueAttr && valueAttr.includes(
|
|
3716
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3268
3717
|
element.removeAttribute('value');
|
|
3269
3718
|
this.parts.push({
|
|
3270
3719
|
type: 'conditional-when',
|
|
@@ -3281,12 +3730,12 @@ class Template {
|
|
|
3281
3730
|
const attr = attributes[i];
|
|
3282
3731
|
const value = attr.value;
|
|
3283
3732
|
// Check for attribute bindings
|
|
3284
|
-
if (value.includes(
|
|
3733
|
+
if (value.includes(templateMarker)) {
|
|
3285
3734
|
attrsToRemove.push(attr);
|
|
3286
3735
|
// Get original attribute name with preserved case
|
|
3287
3736
|
const originalName = attrNamesForParts[partIndex] || attr.name;
|
|
3288
3737
|
// Extract static string segments by splitting on marker
|
|
3289
|
-
const attrStrings = value.split(
|
|
3738
|
+
const attrStrings = value.split(templateMarker);
|
|
3290
3739
|
// Number of expressions = number of markers = attrStrings.length - 1
|
|
3291
3740
|
const expressionCount = attrStrings.length - 1;
|
|
3292
3741
|
// Property/boolean/event bindings take a SINGLE expression — they
|
|
@@ -3427,7 +3876,7 @@ class Template {
|
|
|
3427
3876
|
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
3428
3877
|
const comment = node;
|
|
3429
3878
|
// Check for marker match (processing instruction becomes comment)
|
|
3430
|
-
if (comment.data ===
|
|
3879
|
+
if (comment.data === templateMarkerMatch) {
|
|
3431
3880
|
// Node part
|
|
3432
3881
|
const parent = comment.parentNode;
|
|
3433
3882
|
const endNode = document.createComment('');
|
|
@@ -3439,11 +3888,11 @@ class Template {
|
|
|
3439
3888
|
endNode
|
|
3440
3889
|
});
|
|
3441
3890
|
}
|
|
3442
|
-
else if (comment.data.includes(
|
|
3891
|
+
else if (comment.data.includes(templateMarker)) {
|
|
3443
3892
|
// Binding(s) inside an authored HTML comment (<!-- ${x} -->).
|
|
3444
3893
|
// Render values into the comment text; critically, consume the
|
|
3445
3894
|
// right number of value indices so later parts stay aligned.
|
|
3446
|
-
const commentStrings = comment.data.split(
|
|
3895
|
+
const commentStrings = comment.data.split(templateMarkerRegex);
|
|
3447
3896
|
this.parts.push({
|
|
3448
3897
|
type: 'comment',
|
|
3449
3898
|
index: partIndex,
|
|
@@ -3456,10 +3905,10 @@ class Template {
|
|
|
3456
3905
|
else if (node.nodeType === Node.TEXT_NODE) {
|
|
3457
3906
|
const text = node;
|
|
3458
3907
|
const data = text.data;
|
|
3459
|
-
if (data.includes(
|
|
3908
|
+
if (data.includes(templateMarker)) {
|
|
3460
3909
|
// Split text node at markers
|
|
3461
3910
|
const parent = text.parentNode;
|
|
3462
|
-
const parts = data.split(
|
|
3911
|
+
const parts = data.split(templateMarkerRegex);
|
|
3463
3912
|
const lastIndex = parts.length - 1;
|
|
3464
3913
|
for (let i = 0; i < lastIndex; i++) {
|
|
3465
3914
|
parent.insertBefore(document.createTextNode(parts[i]), text);
|
|
@@ -3581,13 +4030,13 @@ function prepareTemplate(result) {
|
|
|
3581
4030
|
// the value index stays aligned (a swallowed marker would shift every
|
|
3582
4031
|
// binding after the comment).
|
|
3583
4032
|
attrNamesForParts.push('');
|
|
3584
|
-
htmlParts.push(
|
|
4033
|
+
htmlParts.push(templateMarker);
|
|
3585
4034
|
}
|
|
3586
4035
|
else if (inAttrValue) {
|
|
3587
4036
|
// We're inside a quoted attribute value - this is an attribute binding
|
|
3588
4037
|
// For subsequent interpolations in same attribute, keep using same attr name
|
|
3589
4038
|
attrNamesForParts.push(currentAttrName);
|
|
3590
|
-
htmlParts.push(
|
|
4039
|
+
htmlParts.push(templateMarker);
|
|
3591
4040
|
}
|
|
3592
4041
|
else if (inTag) {
|
|
3593
4042
|
// Inside tag but not in attr value - check for special cases
|
|
@@ -3602,7 +4051,7 @@ function prepareTemplate(result) {
|
|
|
3602
4051
|
}
|
|
3603
4052
|
currentAttrName = trimmed.substring(attrStart + 1, trimmed.length - 1).trim();
|
|
3604
4053
|
attrNamesForParts.push(currentAttrName);
|
|
3605
|
-
htmlParts.push(
|
|
4054
|
+
htmlParts.push(templateMarker);
|
|
3606
4055
|
}
|
|
3607
4056
|
else {
|
|
3608
4057
|
// Check if this is a meta element (<if> or <case>)
|
|
@@ -3610,7 +4059,7 @@ function prepareTemplate(result) {
|
|
|
3610
4059
|
if (metaElementMatch) {
|
|
3611
4060
|
currentAttrName = 'value';
|
|
3612
4061
|
attrNamesForParts.push('value');
|
|
3613
|
-
htmlParts.push(`value="${
|
|
4062
|
+
htmlParts.push(`value="${templateMarker}"`);
|
|
3614
4063
|
}
|
|
3615
4064
|
else {
|
|
3616
4065
|
const nearbyTemplate = (str.slice(-40) + '${…}' + (strings[i + 1] ?? '').slice(0, 40)).replace(/\s+/g, ' ').trim();
|
|
@@ -3623,7 +4072,7 @@ function prepareTemplate(result) {
|
|
|
3623
4072
|
else {
|
|
3624
4073
|
// Outside any tag - this is node content
|
|
3625
4074
|
attrNamesForParts.push('');
|
|
3626
|
-
htmlParts.push(
|
|
4075
|
+
htmlParts.push(templateNodeMarker);
|
|
3627
4076
|
}
|
|
3628
4077
|
}
|
|
3629
4078
|
}
|
|
@@ -3657,22 +4106,37 @@ function prepareTemplate(result) {
|
|
|
3657
4106
|
* Extract the key of a list item rendered from a template with a
|
|
3658
4107
|
* `key=${...}` binding. Returns undefined for unkeyed items.
|
|
3659
4108
|
*/
|
|
3660
|
-
function getItemKey(item) {
|
|
4109
|
+
function getItemKey(item, renderIdentity) {
|
|
3661
4110
|
if (!isTemplateResult(item))
|
|
3662
4111
|
return undefined;
|
|
3663
|
-
|
|
4112
|
+
let tmpl;
|
|
4113
|
+
try {
|
|
4114
|
+
tmpl = prepareTemplate(item);
|
|
4115
|
+
}
|
|
4116
|
+
catch (error) {
|
|
4117
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4118
|
+
}
|
|
3664
4119
|
return tmpl.keyIndex === -1 ? undefined : item.values[tmpl.keyIndex];
|
|
3665
4120
|
}
|
|
3666
4121
|
/**
|
|
3667
4122
|
* Instance of a rendered template
|
|
3668
4123
|
*/
|
|
3669
4124
|
class TemplateInstance {
|
|
3670
|
-
|
|
4125
|
+
#renderIdentity;
|
|
4126
|
+
#customElementRegistry;
|
|
4127
|
+
constructor(result, renderIdentity = null, customElementRegistry = document.defaultView?.customElements ?? globalThis.customElements ?? null) {
|
|
3671
4128
|
this.parts = [];
|
|
3672
4129
|
this.fragment = null;
|
|
3673
4130
|
this.conditionalParts = []; // if/case parts with their indices
|
|
3674
4131
|
this.regularParts = []; // all other parts with their indices
|
|
3675
|
-
this
|
|
4132
|
+
this.#renderIdentity = renderIdentity;
|
|
4133
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4134
|
+
try {
|
|
4135
|
+
this.template = prepareTemplate(result);
|
|
4136
|
+
}
|
|
4137
|
+
catch (error) {
|
|
4138
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4139
|
+
}
|
|
3676
4140
|
this.strings = result.strings;
|
|
3677
4141
|
}
|
|
3678
4142
|
/**
|
|
@@ -3682,6 +4146,10 @@ class TemplateInstance {
|
|
|
3682
4146
|
isSameTemplate(strings) {
|
|
3683
4147
|
return this.strings === strings;
|
|
3684
4148
|
}
|
|
4149
|
+
/** Internal attribution value; intentionally contains no live renderer host. */
|
|
4150
|
+
getRenderHostIdentity() {
|
|
4151
|
+
return this.#renderIdentity;
|
|
4152
|
+
}
|
|
3685
4153
|
renderFragment() {
|
|
3686
4154
|
if (!this.fragment) {
|
|
3687
4155
|
// First render - clone template and create parts
|
|
@@ -3710,7 +4178,7 @@ class TemplateInstance {
|
|
|
3710
4178
|
case 'node':
|
|
3711
4179
|
const startNode = nodeMap.get(partDef.startNode);
|
|
3712
4180
|
const endNode = nodeMap.get(partDef.endNode);
|
|
3713
|
-
part = new NodePart(startNode, endNode);
|
|
4181
|
+
part = new NodePart(startNode, endNode, this.#renderIdentity, this.#customElementRegistry);
|
|
3714
4182
|
break;
|
|
3715
4183
|
case 'attribute':
|
|
3716
4184
|
const attrElement = nodeMap.get(partDef.element);
|
|
@@ -3718,7 +4186,7 @@ class TemplateInstance {
|
|
|
3718
4186
|
break;
|
|
3719
4187
|
case 'property':
|
|
3720
4188
|
const propElement = nodeMap.get(partDef.element);
|
|
3721
|
-
part = new PropertyPart(propElement, partDef.name);
|
|
4189
|
+
part = new PropertyPart(propElement, partDef.name, this.#customElementRegistry);
|
|
3722
4190
|
break;
|
|
3723
4191
|
case 'boolean-attribute':
|
|
3724
4192
|
const boolElement = nodeMap.get(partDef.element);
|
|
@@ -3738,7 +4206,7 @@ class TemplateInstance {
|
|
|
3738
4206
|
break;
|
|
3739
4207
|
case 'spread':
|
|
3740
4208
|
const spreadElement = nodeMap.get(partDef.element);
|
|
3741
|
-
part = new SpreadPart(spreadElement, partDef.name);
|
|
4209
|
+
part = new SpreadPart(spreadElement, partDef.name, this.#customElementRegistry);
|
|
3742
4210
|
break;
|
|
3743
4211
|
case 'controller':
|
|
3744
4212
|
const controllerElement = nodeMap.get(partDef.element);
|
|
@@ -3917,7 +4385,9 @@ class Part {
|
|
|
3917
4385
|
* - Reuses child NodeParts when rendering arrays
|
|
3918
4386
|
*/
|
|
3919
4387
|
class NodePart extends Part {
|
|
3920
|
-
|
|
4388
|
+
#renderIdentity;
|
|
4389
|
+
#customElementRegistry;
|
|
4390
|
+
constructor(startNode, endNode, renderIdentity = null, customElementRegistry = null) {
|
|
3921
4391
|
super();
|
|
3922
4392
|
this.type = 'node';
|
|
3923
4393
|
this._committedValue = NOT_COMMITTED;
|
|
@@ -3932,6 +4402,11 @@ class NodePart extends Part {
|
|
|
3932
4402
|
this._committingAsyncValue = false;
|
|
3933
4403
|
this.startNode = startNode;
|
|
3934
4404
|
this.endNode = endNode;
|
|
4405
|
+
this.#renderIdentity = renderIdentity;
|
|
4406
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4407
|
+
}
|
|
4408
|
+
_withRenderContext(error) {
|
|
4409
|
+
return this.#renderIdentity ? contextualizeRenderError(this.#renderIdentity, error) : error;
|
|
3935
4410
|
}
|
|
3936
4411
|
commit(value) {
|
|
3937
4412
|
// Handle noChange sentinel
|
|
@@ -4040,7 +4515,7 @@ class NodePart extends Part {
|
|
|
4040
4515
|
// Different template or first render: prepare and validate the replacement
|
|
4041
4516
|
// while it is detached. If a binding throws, the currently
|
|
4042
4517
|
// committed range remains intact and its lifecycle stays connected.
|
|
4043
|
-
const instance = new TemplateInstance(result);
|
|
4518
|
+
const instance = new TemplateInstance(result, this.#renderIdentity, this.#customElementRegistry);
|
|
4044
4519
|
const fragment = instance.renderFragment();
|
|
4045
4520
|
try {
|
|
4046
4521
|
instance.update(result.values);
|
|
@@ -4091,7 +4566,9 @@ class NodePart extends Part {
|
|
|
4091
4566
|
*/
|
|
4092
4567
|
_commitIterable(value, explicitKeys) {
|
|
4093
4568
|
const items = Array.isArray(value) ? value : Array.from(value);
|
|
4094
|
-
const newKeys = explicitKeys
|
|
4569
|
+
const newKeys = explicitKeys
|
|
4570
|
+
? Array.from(explicitKeys)
|
|
4571
|
+
: items.map(item => getItemKey(item, this.#renderIdentity));
|
|
4095
4572
|
if (newKeys.length !== items.length) {
|
|
4096
4573
|
throw new Error('snice: keyed iterable produced a different number of keys and values.');
|
|
4097
4574
|
}
|
|
@@ -4137,7 +4614,7 @@ class NodePart extends Part {
|
|
|
4137
4614
|
const endMarker = document.createComment('');
|
|
4138
4615
|
this._insertBefore(startMarker);
|
|
4139
4616
|
this._insertBefore(endMarker);
|
|
4140
|
-
itemPart = new NodePart(startMarker, endMarker);
|
|
4617
|
+
itemPart = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4141
4618
|
itemParts.push(itemPart);
|
|
4142
4619
|
}
|
|
4143
4620
|
else {
|
|
@@ -4241,7 +4718,7 @@ class NodePart extends Part {
|
|
|
4241
4718
|
const endMarker = document.createComment('');
|
|
4242
4719
|
parent.insertBefore(startMarker, ref);
|
|
4243
4720
|
parent.insertBefore(endMarker, ref);
|
|
4244
|
-
part = new NodePart(startMarker, endMarker);
|
|
4721
|
+
part = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4245
4722
|
newParts[i] = part;
|
|
4246
4723
|
}
|
|
4247
4724
|
part.commit(items[i]);
|
|
@@ -4313,7 +4790,7 @@ class NodePart extends Part {
|
|
|
4313
4790
|
catch (error) {
|
|
4314
4791
|
this._asyncRunning = false;
|
|
4315
4792
|
this._asyncCompleted = true;
|
|
4316
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4793
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4317
4794
|
return;
|
|
4318
4795
|
}
|
|
4319
4796
|
void (async () => {
|
|
@@ -4326,8 +4803,9 @@ class NodePart extends Part {
|
|
|
4326
4803
|
}
|
|
4327
4804
|
}
|
|
4328
4805
|
catch (error) {
|
|
4329
|
-
if (version === this._asyncVersion)
|
|
4330
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4806
|
+
if (version === this._asyncVersion) {
|
|
4807
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4808
|
+
}
|
|
4331
4809
|
}
|
|
4332
4810
|
finally {
|
|
4333
4811
|
if (version === this._asyncVersion) {
|
|
@@ -4348,14 +4826,14 @@ class NodePart extends Part {
|
|
|
4348
4826
|
this._commitAsyncValue(value);
|
|
4349
4827
|
}
|
|
4350
4828
|
catch (error) {
|
|
4351
|
-
console.error('snice: promise template value failed:', error);
|
|
4829
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4352
4830
|
}
|
|
4353
4831
|
}, error => {
|
|
4354
4832
|
if (version !== this._asyncVersion)
|
|
4355
4833
|
return;
|
|
4356
4834
|
this._asyncRunning = false;
|
|
4357
4835
|
this._asyncCompleted = true;
|
|
4358
|
-
console.error('snice: promise template value failed:', error);
|
|
4836
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4359
4837
|
});
|
|
4360
4838
|
}
|
|
4361
4839
|
_commitAsyncValue(value) {
|
|
@@ -4723,7 +5201,8 @@ function validateSpreadEvents(next, label) {
|
|
|
4723
5201
|
}
|
|
4724
5202
|
/** Named spreads: ...props, ...attrs, and ...events. */
|
|
4725
5203
|
class SpreadPart extends Part {
|
|
4726
|
-
|
|
5204
|
+
#customElementRegistry;
|
|
5205
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4727
5206
|
super();
|
|
4728
5207
|
this.type = 'spread';
|
|
4729
5208
|
this.committed = {};
|
|
@@ -4731,6 +5210,7 @@ class SpreadPart extends Part {
|
|
|
4731
5210
|
this.consumedOnce = new Map();
|
|
4732
5211
|
this.element = element;
|
|
4733
5212
|
this.name = name;
|
|
5213
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4734
5214
|
}
|
|
4735
5215
|
commit(value) {
|
|
4736
5216
|
if (value === noChange)
|
|
@@ -4752,13 +5232,13 @@ class SpreadPart extends Part {
|
|
|
4752
5232
|
commitProperties(next) {
|
|
4753
5233
|
for (const key of Object.keys(this.committed)) {
|
|
4754
5234
|
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
4755
|
-
commitPropertyValue(this.element, key, undefined);
|
|
5235
|
+
commitPropertyValue(this.element, key, undefined, this.#customElementRegistry);
|
|
4756
5236
|
}
|
|
4757
5237
|
}
|
|
4758
5238
|
for (const [key, value] of Object.entries(next)) {
|
|
4759
5239
|
if (Object.is(this.committed[key], value))
|
|
4760
5240
|
continue;
|
|
4761
|
-
commitPropertyValue(this.element, key, value === nothing ? undefined : value);
|
|
5241
|
+
commitPropertyValue(this.element, key, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
4762
5242
|
}
|
|
4763
5243
|
}
|
|
4764
5244
|
commitAttributes(next) {
|
|
@@ -4842,6 +5322,9 @@ class SpreadPart extends Part {
|
|
|
4842
5322
|
this.detachListeners();
|
|
4843
5323
|
}
|
|
4844
5324
|
reconnected() {
|
|
5325
|
+
if (this.name === 'props' || this.name === 'properties') {
|
|
5326
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
5327
|
+
}
|
|
4845
5328
|
if (this.name === 'events' && this.listeners.size === 0)
|
|
4846
5329
|
this.commitEvents(this.committed);
|
|
4847
5330
|
}
|
|
@@ -4856,12 +5339,14 @@ class SpreadPart extends Part {
|
|
|
4856
5339
|
* PropertyPart handles property bindings (.property=${value})
|
|
4857
5340
|
*/
|
|
4858
5341
|
class PropertyPart extends Part {
|
|
4859
|
-
|
|
5342
|
+
#customElementRegistry;
|
|
5343
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4860
5344
|
super();
|
|
4861
5345
|
this.type = 'property';
|
|
4862
5346
|
this._committedValue = NOT_COMMITTED;
|
|
4863
5347
|
this.element = element;
|
|
4864
5348
|
this.name = name;
|
|
5349
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4865
5350
|
}
|
|
4866
5351
|
commit(value) {
|
|
4867
5352
|
if (value === noChange)
|
|
@@ -4885,7 +5370,10 @@ class PropertyPart extends Part {
|
|
|
4885
5370
|
return;
|
|
4886
5371
|
}
|
|
4887
5372
|
this._committedValue = value;
|
|
4888
|
-
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
|
|
5373
|
+
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
5374
|
+
}
|
|
5375
|
+
reconnected() {
|
|
5376
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
4889
5377
|
}
|
|
4890
5378
|
clear() {
|
|
4891
5379
|
// This part is being destroyed with its owning template. The element is
|
|
@@ -5605,16 +6093,6 @@ function resetHostAutofocus(host) {
|
|
|
5605
6093
|
* Production default is false (log and keep the previous DOM).
|
|
5606
6094
|
*/
|
|
5607
6095
|
let strictRenderErrors = false;
|
|
5608
|
-
function contextualizeRenderError(element, error) {
|
|
5609
|
-
const tag = element.tagName?.toLowerCase() || 'element';
|
|
5610
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5611
|
-
const contextual = new Error(`snice: render failed for <${tag}>: ${message}`, {
|
|
5612
|
-
cause: error
|
|
5613
|
-
});
|
|
5614
|
-
if (error instanceof Error)
|
|
5615
|
-
contextual.name = error.name;
|
|
5616
|
-
return contextual;
|
|
5617
|
-
}
|
|
5618
6096
|
function setStrictRenderErrors(value) {
|
|
5619
6097
|
strictRenderErrors = value;
|
|
5620
6098
|
}
|
|
@@ -5717,6 +6195,8 @@ const MAX_RENDER_DEPTH = 50;
|
|
|
5717
6195
|
* Perform the actual render of an element
|
|
5718
6196
|
*/
|
|
5719
6197
|
function performRender(element, options, precomputedResult) {
|
|
6198
|
+
const capturedIdentity = captureRenderHostIdentity(element);
|
|
6199
|
+
let renderIdentity = capturedIdentity;
|
|
5720
6200
|
const hasPrecomputedResult = arguments.length >= 3;
|
|
5721
6201
|
const renderMethod = element[RENDER_METHOD];
|
|
5722
6202
|
if (!renderMethod) {
|
|
@@ -5772,12 +6252,16 @@ function performRender(element, options, precomputedResult) {
|
|
|
5772
6252
|
}
|
|
5773
6253
|
let instance = element[RENDER_INSTANCE];
|
|
5774
6254
|
if (instance && instance.isSameTemplate(result.strings)) {
|
|
6255
|
+
renderIdentity = instance.getRenderHostIdentity() ?? capturedIdentity;
|
|
5775
6256
|
instance.update(result.values);
|
|
5776
6257
|
flushRenderCallbacks(element);
|
|
5777
6258
|
scheduleAutofocus(element);
|
|
5778
6259
|
return;
|
|
5779
6260
|
}
|
|
5780
|
-
const
|
|
6261
|
+
const customElementRegistry = renderRoot instanceof ShadowRoot
|
|
6262
|
+
? renderRoot.customElementRegistry ?? renderRoot.ownerDocument.defaultView?.customElements ?? null
|
|
6263
|
+
: renderRoot.ownerDocument.defaultView?.customElements ?? null;
|
|
6264
|
+
const nextInstance = new TemplateInstance(result, capturedIdentity, customElementRegistry);
|
|
5781
6265
|
const nextFragment = nextInstance.renderFragment();
|
|
5782
6266
|
// Commit while detached first. A malformed binding can then fail without
|
|
5783
6267
|
// exposing a half-updated tree. Connection-aware parts are paired with
|
|
@@ -5837,7 +6321,7 @@ function performRender(element, options, precomputedResult) {
|
|
|
5837
6321
|
throw cleanupError;
|
|
5838
6322
|
}
|
|
5839
6323
|
catch (error) {
|
|
5840
|
-
const contextual = contextualizeRenderError(
|
|
6324
|
+
const contextual = contextualizeRenderError(renderIdentity, error);
|
|
5841
6325
|
if (strictRenderErrors)
|
|
5842
6326
|
throw contextual;
|
|
5843
6327
|
console.error('Error rendering element:', contextual);
|
|
@@ -6125,134 +6609,6 @@ function applyStyles(element) {
|
|
|
6125
6609
|
}
|
|
6126
6610
|
}
|
|
6127
6611
|
|
|
6128
|
-
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
6129
|
-
/**
|
|
6130
|
-
* Decorator that automatically dispatches a custom event after a method is called.
|
|
6131
|
-
* The return value of the method becomes the event detail.
|
|
6132
|
-
*
|
|
6133
|
-
* @param eventName The name of the event to dispatch
|
|
6134
|
-
* @param options Optional configuration extending EventInit
|
|
6135
|
-
*/
|
|
6136
|
-
function dispatch(eventName, options) {
|
|
6137
|
-
return function (originalMethod, _context) {
|
|
6138
|
-
return function (...args) {
|
|
6139
|
-
// Create timing wrappers for dispatch (per-instance)
|
|
6140
|
-
if (!this[DISPATCH_TIMERS]) {
|
|
6141
|
-
this[DISPATCH_TIMERS] = new Map();
|
|
6142
|
-
}
|
|
6143
|
-
const timerKey = `${eventName}_${_context.name}`;
|
|
6144
|
-
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
6145
|
-
this[DISPATCH_TIMERS].set(timerKey, {
|
|
6146
|
-
debounceTimeout: null,
|
|
6147
|
-
throttleLastCall: 0,
|
|
6148
|
-
throttleTimeout: null
|
|
6149
|
-
});
|
|
6150
|
-
}
|
|
6151
|
-
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
6152
|
-
// Call the original method with preserved this context
|
|
6153
|
-
const result = originalMethod.apply(this, args);
|
|
6154
|
-
// Helper to dispatch the event
|
|
6155
|
-
const doDispatch = (detail) => {
|
|
6156
|
-
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
6157
|
-
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
6158
|
-
return;
|
|
6159
|
-
}
|
|
6160
|
-
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
6161
|
-
// not belong in the CustomEvent constructor dictionary.
|
|
6162
|
-
const event = new CustomEvent(eventName, {
|
|
6163
|
-
bubbles: options?.bubbles ?? true,
|
|
6164
|
-
cancelable: options?.cancelable ?? false,
|
|
6165
|
-
composed: options?.composed ?? true,
|
|
6166
|
-
detail
|
|
6167
|
-
});
|
|
6168
|
-
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
6169
|
-
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
6170
|
-
}
|
|
6171
|
-
if (options?.daemon !== undefined) {
|
|
6172
|
-
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
6173
|
-
return;
|
|
6174
|
-
}
|
|
6175
|
-
if (options?.scope !== undefined) {
|
|
6176
|
-
const target = resolveScope(this, options.scope);
|
|
6177
|
-
if (!target) {
|
|
6178
|
-
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
6179
|
-
return;
|
|
6180
|
-
}
|
|
6181
|
-
target.dispatchEvent(event);
|
|
6182
|
-
return;
|
|
6183
|
-
}
|
|
6184
|
-
const target = defaultCommunicationTarget(this);
|
|
6185
|
-
if (!target) {
|
|
6186
|
-
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
6187
|
-
}
|
|
6188
|
-
target.dispatchEvent(event);
|
|
6189
|
-
};
|
|
6190
|
-
// Helper to handle timed dispatch
|
|
6191
|
-
const timedDispatch = (detail) => {
|
|
6192
|
-
if (options?.debounce) {
|
|
6193
|
-
clearTimeout(timers.debounceTimeout);
|
|
6194
|
-
timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
|
|
6195
|
-
return;
|
|
6196
|
-
}
|
|
6197
|
-
if (!options?.throttle) {
|
|
6198
|
-
doDispatch(detail);
|
|
6199
|
-
return;
|
|
6200
|
-
}
|
|
6201
|
-
const now = Date.now();
|
|
6202
|
-
const remaining = options.throttle - (now - timers.throttleLastCall);
|
|
6203
|
-
if (remaining <= 0) {
|
|
6204
|
-
clearTimeout(timers.throttleTimeout);
|
|
6205
|
-
timers.throttleLastCall = now;
|
|
6206
|
-
doDispatch(detail);
|
|
6207
|
-
return;
|
|
6208
|
-
}
|
|
6209
|
-
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
6210
|
-
// data, not the first-suppressed-call detail captured by closure.
|
|
6211
|
-
timers.latestDetail = detail;
|
|
6212
|
-
if (!timers.throttleTimeout) {
|
|
6213
|
-
timers.throttleTimeout = setTimeout(() => {
|
|
6214
|
-
timers.throttleLastCall = Date.now();
|
|
6215
|
-
timers.throttleTimeout = null;
|
|
6216
|
-
const d = timers.latestDetail;
|
|
6217
|
-
timers.latestDetail = undefined;
|
|
6218
|
-
doDispatch(d);
|
|
6219
|
-
}, remaining);
|
|
6220
|
-
}
|
|
6221
|
-
};
|
|
6222
|
-
// Handle async methods
|
|
6223
|
-
if (result instanceof Promise) {
|
|
6224
|
-
return result.then((resolvedResult) => {
|
|
6225
|
-
timedDispatch(resolvedResult);
|
|
6226
|
-
return resolvedResult;
|
|
6227
|
-
});
|
|
6228
|
-
}
|
|
6229
|
-
// Sync method
|
|
6230
|
-
timedDispatch(result);
|
|
6231
|
-
return result;
|
|
6232
|
-
};
|
|
6233
|
-
};
|
|
6234
|
-
}
|
|
6235
|
-
/**
|
|
6236
|
-
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
6237
|
-
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
6238
|
-
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
6239
|
-
*/
|
|
6240
|
-
function clearDispatchTimers(instance) {
|
|
6241
|
-
const timers = instance[DISPATCH_TIMERS];
|
|
6242
|
-
if (!timers)
|
|
6243
|
-
return;
|
|
6244
|
-
for (const t of timers.values()) {
|
|
6245
|
-
if (t.debounceTimeout)
|
|
6246
|
-
clearTimeout(t.debounceTimeout);
|
|
6247
|
-
if (t.throttleTimeout)
|
|
6248
|
-
clearTimeout(t.throttleTimeout);
|
|
6249
|
-
t.debounceTimeout = null;
|
|
6250
|
-
t.throttleTimeout = null;
|
|
6251
|
-
t.throttleLastCall = 0;
|
|
6252
|
-
t.latestDetail = undefined;
|
|
6253
|
-
}
|
|
6254
|
-
}
|
|
6255
|
-
|
|
6256
6612
|
/**
|
|
6257
6613
|
* Method decorators for common patterns
|
|
6258
6614
|
* @debounce, @throttle, @once, @memoize
|
|
@@ -7135,6 +7491,10 @@ function applyElementFunctionality(constructor) {
|
|
|
7135
7491
|
scheduleAutofocus(this);
|
|
7136
7492
|
};
|
|
7137
7493
|
constructor.prototype.disconnectedCallback = async function () {
|
|
7494
|
+
// Invalidate @dispatch work queued or started before disconnect synchronously.
|
|
7495
|
+
// The platform does not await disconnectedCallback, and @dispose may be
|
|
7496
|
+
// async, so waiting until the end permits events from a detached host.
|
|
7497
|
+
clearDispatchTimers(this);
|
|
7138
7498
|
try {
|
|
7139
7499
|
disconnectRenderTree(this);
|
|
7140
7500
|
}
|
|
@@ -7179,7 +7539,6 @@ function applyElementFunctionality(constructor) {
|
|
|
7179
7539
|
if (clearRenderTimers(this)) {
|
|
7180
7540
|
this[PENDING_RECONNECT_RENDER] = true;
|
|
7181
7541
|
}
|
|
7182
|
-
clearDispatchTimers(this);
|
|
7183
7542
|
// @moved / @adopted debounce-throttle timers, likewise dropped so they
|
|
7184
7543
|
// don't fire on a dead element.
|
|
7185
7544
|
clearLifecycleTimers(this, MOVED_TIMERS);
|
|
@@ -7188,6 +7547,11 @@ function applyElementFunctionality(constructor) {
|
|
|
7188
7547
|
constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
|
|
7189
7548
|
originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
|
|
7190
7549
|
if (name === 'controller') {
|
|
7550
|
+
// Parsing a dynamic controller binding briefly creates this marker
|
|
7551
|
+
// attribute on the inert template element. It is renderer metadata,
|
|
7552
|
+
// not a registry name, so it must never start an attachment.
|
|
7553
|
+
if (hasTemplateMarker(newValue))
|
|
7554
|
+
return;
|
|
7191
7555
|
// A class attachment reflects its decorator name for DOM diagnostics,
|
|
7192
7556
|
// but the class reference remains the only attachment authority.
|
|
7193
7557
|
if (this[CONTROLLER_ATTRIBUTE_SYNC])
|
|
@@ -7365,12 +7729,16 @@ function defineElement(tagName, constructor, context, options) {
|
|
|
7365
7729
|
constructor.prototype[RENDER_OPTIONS] = constructor.renderOptions || {};
|
|
7366
7730
|
}
|
|
7367
7731
|
applyElementFunctionality(constructor);
|
|
7368
|
-
|
|
7732
|
+
const existing = customElements.get(tagName);
|
|
7733
|
+
if (existing) {
|
|
7734
|
+
if (existing === constructor)
|
|
7735
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7369
7736
|
if (globalThis.SNICE_DEBUG)
|
|
7370
7737
|
console.warn(`[snice] "${tagName}" is already registered. Skipping.`);
|
|
7371
7738
|
return constructor;
|
|
7372
7739
|
}
|
|
7373
7740
|
customElements.define(tagName, constructor);
|
|
7741
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7374
7742
|
return constructor;
|
|
7375
7743
|
}
|
|
7376
7744
|
function element(tagName, options) {
|
|
@@ -8779,6 +9147,72 @@ function provideRouterContext(root, navigation) {
|
|
|
8779
9147
|
}, navigation);
|
|
8780
9148
|
}
|
|
8781
9149
|
|
|
9150
|
+
const EMPTY_QUERY_MARKER = '__snice_empty_query_value__';
|
|
9151
|
+
const QUERY_PARAM_PATTERN = /=:[\w-]+\)?$/;
|
|
9152
|
+
function containsDecodedMarker(url, marker) {
|
|
9153
|
+
if (url.includes(marker))
|
|
9154
|
+
return true;
|
|
9155
|
+
const queryIndex = url.indexOf('?');
|
|
9156
|
+
if (queryIndex === -1)
|
|
9157
|
+
return false;
|
|
9158
|
+
return url.slice(queryIndex + 1).split('&').some(part => {
|
|
9159
|
+
try {
|
|
9160
|
+
return decodeURIComponent(part).includes(marker);
|
|
9161
|
+
}
|
|
9162
|
+
catch {
|
|
9163
|
+
return false;
|
|
9164
|
+
}
|
|
9165
|
+
});
|
|
9166
|
+
}
|
|
9167
|
+
function emptyQueryMarker(route, url) {
|
|
9168
|
+
let marker = EMPTY_QUERY_MARKER;
|
|
9169
|
+
while (route.spec.includes(marker) || containsDecodedMarker(url, marker)) {
|
|
9170
|
+
marker += '_';
|
|
9171
|
+
}
|
|
9172
|
+
return marker;
|
|
9173
|
+
}
|
|
9174
|
+
function fillEmptyQueryParams(route, url, marker) {
|
|
9175
|
+
const urlQueryIndex = url.indexOf('?');
|
|
9176
|
+
const routeQueryIndex = route.spec.indexOf('?');
|
|
9177
|
+
if (urlQueryIndex === -1 || routeQueryIndex === -1)
|
|
9178
|
+
return null;
|
|
9179
|
+
const urlParts = url.slice(urlQueryIndex + 1).split('&');
|
|
9180
|
+
const routeParts = route.spec.slice(routeQueryIndex + 1).split('&');
|
|
9181
|
+
let changed = false;
|
|
9182
|
+
const normalizedParts = urlParts.map((part, index) => {
|
|
9183
|
+
if (part.endsWith('=') && QUERY_PARAM_PATTERN.test(routeParts[index] ?? '')) {
|
|
9184
|
+
changed = true;
|
|
9185
|
+
return `${part}${marker}`;
|
|
9186
|
+
}
|
|
9187
|
+
return part;
|
|
9188
|
+
});
|
|
9189
|
+
if (!changed)
|
|
9190
|
+
return null;
|
|
9191
|
+
return `${url.slice(0, urlQueryIndex + 1)}${normalizedParts.join('&')}`;
|
|
9192
|
+
}
|
|
9193
|
+
/**
|
|
9194
|
+
* Match through pica-route while preserving legal empty query parameter values.
|
|
9195
|
+
* pica-route 1.1.2 rejects an empty param and therefore discards the whole route.
|
|
9196
|
+
* Keep this compatibility shim in sync with packages/react/src/route-match.ts.
|
|
9197
|
+
*/
|
|
9198
|
+
function matchRoute(route, url) {
|
|
9199
|
+
const directMatch = route.match(url);
|
|
9200
|
+
if (directMatch !== false)
|
|
9201
|
+
return directMatch;
|
|
9202
|
+
const marker = emptyQueryMarker(route, url);
|
|
9203
|
+
const normalizedUrl = fillEmptyQueryParams(route, url, marker);
|
|
9204
|
+
if (normalizedUrl === null)
|
|
9205
|
+
return false;
|
|
9206
|
+
const params = route.match(normalizedUrl);
|
|
9207
|
+
if (params === false)
|
|
9208
|
+
return false;
|
|
9209
|
+
for (const name of Object.keys(params)) {
|
|
9210
|
+
if (params[name] === marker)
|
|
9211
|
+
params[name] = '';
|
|
9212
|
+
}
|
|
9213
|
+
return params;
|
|
9214
|
+
}
|
|
9215
|
+
|
|
8782
9216
|
// Router historically allows a later Router instance to take over the same
|
|
8783
9217
|
// target. Track only Router-owned provider releases so that handoff remains
|
|
8784
9218
|
// explicit without weakening duplicate protection for public provideContext().
|
|
@@ -8894,8 +9328,13 @@ function Router(options) {
|
|
|
8894
9328
|
delete this[CONTEXT_HANDLER];
|
|
8895
9329
|
};
|
|
8896
9330
|
// Define the custom element (skip if already registered)
|
|
8897
|
-
|
|
9331
|
+
const existing = win.customElements.get(pageOptions.tag);
|
|
9332
|
+
if (!existing) {
|
|
8898
9333
|
win.customElements.define(pageOptions.tag, constructor);
|
|
9334
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
9335
|
+
}
|
|
9336
|
+
else if (existing === constructor) {
|
|
9337
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
8899
9338
|
}
|
|
8900
9339
|
// Register the routes with guards, layout, and placard
|
|
8901
9340
|
pageOptions.routes.forEach(route => {
|
|
@@ -9145,7 +9584,7 @@ function Router(options) {
|
|
|
9145
9584
|
}
|
|
9146
9585
|
async function resolveRoute(path, target, stale) {
|
|
9147
9586
|
for (const route of routes) {
|
|
9148
|
-
const params = route.route
|
|
9587
|
+
const params = matchRoute(route.route, path);
|
|
9149
9588
|
const isMatch = params !== false;
|
|
9150
9589
|
if (!isMatch) {
|
|
9151
9590
|
continue;
|