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,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.
|
|
@@ -8,7 +8,7 @@ var Snice = (function (exports) {
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
/*!
|
|
11
|
-
* snice v7.
|
|
11
|
+
* snice v7.6.0
|
|
12
12
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
13
13
|
* (c) 2024
|
|
14
14
|
* Released under the MIT License.
|
|
@@ -723,6 +723,18 @@ var Snice = (function (exports) {
|
|
|
723
723
|
}
|
|
724
724
|
};
|
|
725
725
|
}
|
|
726
|
+
/** Resolve and validate an @on/@dispatch debounce or throttle interval. */
|
|
727
|
+
function resolveEventTiming(instance, timing, decorator, option) {
|
|
728
|
+
if (timing === undefined)
|
|
729
|
+
return undefined;
|
|
730
|
+
const value = typeof timing === 'function'
|
|
731
|
+
? timing.call(instance)
|
|
732
|
+
: timing;
|
|
733
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
734
|
+
throw new TypeError(`${decorator} ${option} must resolve to a finite, non-negative number of milliseconds.`);
|
|
735
|
+
}
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
726
738
|
/**
|
|
727
739
|
* Resolve a scope option to a concrete EventTarget.
|
|
728
740
|
*
|
|
@@ -1856,6 +1868,11 @@ var Snice = (function (exports) {
|
|
|
1856
1868
|
else if (selectorOrOptions === null && options) {
|
|
1857
1869
|
opts = options;
|
|
1858
1870
|
}
|
|
1871
|
+
// `target` is the options-object spelling of the positional selector; fold it
|
|
1872
|
+
// into the same delegation path. The positional argument wins when both are given.
|
|
1873
|
+
if (!selector && opts.target) {
|
|
1874
|
+
selector = opts.target;
|
|
1875
|
+
}
|
|
1859
1876
|
return function (originalMethod, context) {
|
|
1860
1877
|
const methodName = context.name;
|
|
1861
1878
|
context.addInitializer(function () {
|
|
@@ -1867,7 +1884,7 @@ var Snice = (function (exports) {
|
|
|
1867
1884
|
// and child registrations pollute parent (and vice versa) via the
|
|
1868
1885
|
// prototype chain.
|
|
1869
1886
|
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
1870
|
-
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}`;
|
|
1887
|
+
const registrationKey = `${methodName}::${eventNames.join(',')}::${selector ?? ''}::daemon:${opts.daemon ?? ''}::light:${opts.light ?? ''}::shadow:${opts.shadow ?? ''}`;
|
|
1871
1888
|
if (!Object.prototype.hasOwnProperty.call(constructor, ON_METHODS)) {
|
|
1872
1889
|
constructor[ON_METHODS] = new Set();
|
|
1873
1890
|
}
|
|
@@ -1947,6 +1964,10 @@ var Snice = (function (exports) {
|
|
|
1947
1964
|
const currentMethod = instance[handler.methodName];
|
|
1948
1965
|
let boundMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
|
|
1949
1966
|
const handlerOptions = handler.options || {};
|
|
1967
|
+
const debounceDelay = resolveEventTiming(instance, handlerOptions.debounce, '@on', 'debounce');
|
|
1968
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
1969
|
+
? undefined
|
|
1970
|
+
: resolveEventTiming(instance, handlerOptions.throttle, '@on', 'throttle');
|
|
1950
1971
|
// Parse event name for key modifiers
|
|
1951
1972
|
// Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
|
|
1952
1973
|
// Only parse colons for keyboard events, not custom events
|
|
@@ -1967,13 +1988,13 @@ var Snice = (function (exports) {
|
|
|
1967
1988
|
if (delimiterIndex <= 0)
|
|
1968
1989
|
warnIfModifierMisuse(handler.eventName);
|
|
1969
1990
|
// Apply debounce (takes precedence over throttle)
|
|
1970
|
-
if (
|
|
1971
|
-
const debounced = createDebounced(boundMethod,
|
|
1991
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
1992
|
+
const debounced = createDebounced(boundMethod, debounceDelay);
|
|
1972
1993
|
instance[CLEANUP].eventCancels.push(() => debounced.cancel());
|
|
1973
1994
|
boundMethod = debounced;
|
|
1974
1995
|
}
|
|
1975
|
-
else if (
|
|
1976
|
-
boundMethod = throttle$1(boundMethod,
|
|
1996
|
+
else if (throttleDelay && throttleDelay > 0) {
|
|
1997
|
+
boundMethod = throttle$1(boundMethod, throttleDelay);
|
|
1977
1998
|
}
|
|
1978
1999
|
// Create event handler with key modifier support
|
|
1979
2000
|
// Uses shared keyboard filter implementation from parts.ts
|
|
@@ -1981,15 +2002,21 @@ var Snice = (function (exports) {
|
|
|
1981
2002
|
if (keyModifier && ['keydown', 'keyup', 'keypress'].includes(baseEventName)) {
|
|
1982
2003
|
keyFilter = parseKeyboardFilter(keyModifier);
|
|
1983
2004
|
}
|
|
2005
|
+
// Returns whether the method actually ran, so `once` can be managed
|
|
2006
|
+
// manually: native `once` consumes the listener on ANY invocation — a
|
|
2007
|
+
// non-matching key or (with multiple tree listeners) a different root
|
|
2008
|
+
// would either eat the single firing or allow a second one.
|
|
1984
2009
|
const createKeyModifierHandler = (method) => {
|
|
1985
2010
|
if (!keyFilter) {
|
|
1986
|
-
return method;
|
|
2011
|
+
return (event) => { method(event); return true; };
|
|
1987
2012
|
}
|
|
1988
2013
|
return (event) => {
|
|
1989
2014
|
const keyEvent = event;
|
|
1990
2015
|
if (matchesKeyboardFilter(keyEvent, keyFilter)) {
|
|
1991
2016
|
method(event);
|
|
2017
|
+
return true;
|
|
1992
2018
|
}
|
|
2019
|
+
return false;
|
|
1993
2020
|
};
|
|
1994
2021
|
};
|
|
1995
2022
|
// Apply key modifier wrapper
|
|
@@ -2007,6 +2034,20 @@ var Snice = (function (exports) {
|
|
|
2007
2034
|
continue;
|
|
2008
2035
|
}
|
|
2009
2036
|
const hasExplicitTarget = hasExplicitScope || hasDaemon;
|
|
2037
|
+
// Tree toggles — the same light/shadow pair @query uses. Both default to
|
|
2038
|
+
// true: direct listeners attach in both trees, delegated listeners match
|
|
2039
|
+
// in both. An explicit scope/daemon owns the listener target outright, so
|
|
2040
|
+
// the flags are meaningless there and ignored with a warning.
|
|
2041
|
+
const inShadow = handlerOptions.shadow !== false;
|
|
2042
|
+
const inLight = handlerOptions.light !== false;
|
|
2043
|
+
if (hasExplicitTarget
|
|
2044
|
+
&& (handlerOptions.light !== undefined || handlerOptions.shadow !== undefined)) {
|
|
2045
|
+
console.warn(`[snice/@on] light/shadow are ignored for "${handler.eventName}" — an explicit scope/daemon owns the listener target.`);
|
|
2046
|
+
}
|
|
2047
|
+
if (!hasExplicitTarget && !inShadow && !inLight) {
|
|
2048
|
+
console.warn(`[snice/@on] "${handler.eventName}" disables both light and shadow — listener skipped.`);
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2010
2051
|
let scopedTarget = null;
|
|
2011
2052
|
if (hasDaemon) {
|
|
2012
2053
|
try {
|
|
@@ -2028,56 +2069,125 @@ var Snice = (function (exports) {
|
|
|
2028
2069
|
}
|
|
2029
2070
|
// Main event handler with error handling and event delegation
|
|
2030
2071
|
if (handler.selector) {
|
|
2031
|
-
// Delegated event handling.
|
|
2032
|
-
// -
|
|
2033
|
-
// -
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
(
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
keyModifierMethod(event);
|
|
2052
|
-
}
|
|
2053
|
-
catch (error) {
|
|
2054
|
-
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2055
|
-
}
|
|
2056
|
-
};
|
|
2072
|
+
// Delegated event handling. Listener roots by tree toggle:
|
|
2073
|
+
// - shadow → the shadow root (when present)
|
|
2074
|
+
// - light → the host element (hears light-DOM bubbles)
|
|
2075
|
+
// - explicit scope: the scoped target only
|
|
2076
|
+
const componentShadowRoot = targetElement.shadowRoot;
|
|
2077
|
+
const delegationRoots = hasExplicitTarget
|
|
2078
|
+
? [scopedTarget]
|
|
2079
|
+
: [
|
|
2080
|
+
...(inShadow && componentShadowRoot ? [componentShadowRoot] : []),
|
|
2081
|
+
...(inLight ? [targetElement] : []),
|
|
2082
|
+
];
|
|
2083
|
+
if (delegationRoots.length === 0) {
|
|
2084
|
+
console.warn(`[snice/@on] "${handler.eventName}" cannot delegate "${handler.selector}" — no shadow root and light disabled; listener skipped.`);
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
// One event can reach more than one root (e.g. a slotted click seen by
|
|
2088
|
+
// both the shadow root and the host); a per-handler Symbol keeps the
|
|
2089
|
+
// handler to one invocation per event. The symbol is only stamped on a
|
|
2090
|
+
// match so a non-matching root never blocks the other.
|
|
2091
|
+
const handledSymbol = Symbol();
|
|
2057
2092
|
// Auto-enable capture for non-bubbling events when using delegation
|
|
2058
2093
|
const needsCapture = NON_BUBBLING_EVENTS.has(baseEventName);
|
|
2059
2094
|
const useCapture = handlerOptions.capture !== undefined
|
|
2060
2095
|
? handlerOptions.capture
|
|
2061
2096
|
: needsCapture;
|
|
2097
|
+
// `once` is managed manually (never native): the handler fires exactly
|
|
2098
|
+
// once — a non-matching event or key must not consume it, and a match
|
|
2099
|
+
// must retire the listeners on EVERY root, not just the one that fired.
|
|
2062
2100
|
const listenerOptions = {
|
|
2063
2101
|
capture: useCapture,
|
|
2064
|
-
once:
|
|
2102
|
+
once: false,
|
|
2065
2103
|
passive: handlerOptions.passive || false,
|
|
2066
2104
|
};
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2105
|
+
const listenerGroup = [];
|
|
2106
|
+
const removeGroup = () => {
|
|
2107
|
+
for (const entry of listenerGroup) {
|
|
2108
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
for (const eventRoot of delegationRoots) {
|
|
2112
|
+
// Delegation matches only elements visible in the listener's own tree
|
|
2113
|
+
// scope: a shadow-root listener matches its shadow tree, anything else
|
|
2114
|
+
// matches its containing document/root. This mirrors native retargeting
|
|
2115
|
+
// (child-component internals never match) while still matching a shadow
|
|
2116
|
+
// wrapper when the click lands on light-DOM content slotted into it —
|
|
2117
|
+
// `target.closest()` alone cannot cross that slot boundary.
|
|
2118
|
+
const treeScope = eventRoot instanceof ShadowRoot
|
|
2119
|
+
? eventRoot
|
|
2120
|
+
: (eventRoot.getRootNode?.() ?? document);
|
|
2121
|
+
const findDelegateMatch = (event) => {
|
|
2122
|
+
if (typeof event.composedPath !== 'function') {
|
|
2123
|
+
// Fallback for environments without composedPath: ancestor walk.
|
|
2124
|
+
const target = event.target;
|
|
2125
|
+
return (target.matches && target.matches(handler.selector) && target)
|
|
2126
|
+
|| (target.closest && target.closest(handler.selector))
|
|
2127
|
+
|| null;
|
|
2128
|
+
}
|
|
2129
|
+
for (const node of event.composedPath()) {
|
|
2130
|
+
const el = node;
|
|
2131
|
+
if (el === eventRoot)
|
|
2132
|
+
break;
|
|
2133
|
+
if (el.nodeType !== 1)
|
|
2134
|
+
continue;
|
|
2135
|
+
if (el.getRootNode() !== treeScope)
|
|
2136
|
+
continue;
|
|
2137
|
+
if (el.matches(handler.selector))
|
|
2138
|
+
return el;
|
|
2139
|
+
}
|
|
2140
|
+
return null;
|
|
2141
|
+
};
|
|
2142
|
+
const delegatedHandler = (event) => {
|
|
2143
|
+
if (event[handledSymbol])
|
|
2144
|
+
return;
|
|
2145
|
+
const matchingElement = findDelegateMatch(event);
|
|
2146
|
+
if (!matchingElement)
|
|
2147
|
+
return;
|
|
2148
|
+
event[handledSymbol] = true;
|
|
2149
|
+
if (handlerOptions.preventDefault)
|
|
2150
|
+
event.preventDefault();
|
|
2151
|
+
if (handlerOptions.stopPropagation) {
|
|
2152
|
+
event.stopPropagation();
|
|
2153
|
+
event.stopImmediatePropagation();
|
|
2154
|
+
}
|
|
2155
|
+
let fired = false;
|
|
2156
|
+
try {
|
|
2157
|
+
fired = keyModifierMethod(event);
|
|
2158
|
+
}
|
|
2159
|
+
catch (error) {
|
|
2160
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2161
|
+
fired = true;
|
|
2162
|
+
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2163
|
+
}
|
|
2164
|
+
if (fired && handlerOptions.once)
|
|
2165
|
+
removeGroup();
|
|
2166
|
+
};
|
|
2167
|
+
eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
|
|
2168
|
+
listenerGroup.push({ target: eventRoot, handler: delegatedHandler });
|
|
2169
|
+
instance[CLEANUP].events.push({
|
|
2170
|
+
target: eventRoot,
|
|
2171
|
+
eventName: baseEventName,
|
|
2172
|
+
handler: delegatedHandler,
|
|
2173
|
+
options: listenerOptions,
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2074
2176
|
}
|
|
2075
2177
|
else {
|
|
2076
2178
|
// Direct event handling.
|
|
2077
2179
|
// - Default: shadow root + host element (so events inside shadow and on
|
|
2078
2180
|
// the host itself both fire). A per-handler Symbol dedupes.
|
|
2181
|
+
// - light/shadow narrow the attachment: shadow → the shadow root
|
|
2182
|
+
// listener, light → the host listener.
|
|
2079
2183
|
// - With explicit scope: attach to the scoped target ONLY (no duplication).
|
|
2080
|
-
const shadowRoot = hasExplicitTarget
|
|
2184
|
+
const shadowRoot = (hasExplicitTarget || !inShadow)
|
|
2185
|
+
? null
|
|
2186
|
+
: targetElement.shadowRoot;
|
|
2187
|
+
if (!hasExplicitTarget && !shadowRoot && !inLight) {
|
|
2188
|
+
console.warn(`[snice/@on] "${handler.eventName}" requests shadow only but the element has no shadow root — listener skipped.`);
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2081
2191
|
// Per-handler private Symbol so dedup is scoped to THIS handler's two
|
|
2082
2192
|
// listeners (shadowRoot + host). Using Symbol.for() with the method name
|
|
2083
2193
|
// would collide across components that share a method name (e.g. a parent
|
|
@@ -2085,6 +2195,20 @@ var Snice = (function (exports) {
|
|
|
2085
2195
|
// handler to be silently swallowed when events bubble up through a
|
|
2086
2196
|
// shadow boundary.
|
|
2087
2197
|
const handledSymbol = Symbol();
|
|
2198
|
+
// `once` is managed manually (never native): a key-filtered handler must
|
|
2199
|
+
// not be consumed by a non-matching key, and with listeners in two trees
|
|
2200
|
+
// the first real firing must retire BOTH, not just the one that fired.
|
|
2201
|
+
const listenerOptions = {
|
|
2202
|
+
capture: handlerOptions.capture || false,
|
|
2203
|
+
once: false,
|
|
2204
|
+
passive: handlerOptions.passive || false,
|
|
2205
|
+
};
|
|
2206
|
+
const listenerGroup = [];
|
|
2207
|
+
const removeGroup = () => {
|
|
2208
|
+
for (const entry of listenerGroup) {
|
|
2209
|
+
entry.target.removeEventListener(baseEventName, entry.handler, listenerOptions);
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2088
2212
|
const wrappedMethod = (event) => {
|
|
2089
2213
|
if (event[handledSymbol])
|
|
2090
2214
|
return;
|
|
@@ -2093,46 +2217,39 @@ var Snice = (function (exports) {
|
|
|
2093
2217
|
event.preventDefault();
|
|
2094
2218
|
if (handlerOptions.stopPropagation)
|
|
2095
2219
|
event.stopPropagation();
|
|
2220
|
+
let fired = false;
|
|
2096
2221
|
try {
|
|
2097
|
-
keyModifierMethod(event);
|
|
2222
|
+
fired = keyModifierMethod(event);
|
|
2098
2223
|
}
|
|
2099
2224
|
catch (error) {
|
|
2225
|
+
// A throw can only escape user code, so the handler DID run.
|
|
2226
|
+
fired = true;
|
|
2100
2227
|
console.error(`Error in event handler ${handler.methodName}:`, error);
|
|
2101
2228
|
}
|
|
2229
|
+
if (fired && handlerOptions.once)
|
|
2230
|
+
removeGroup();
|
|
2102
2231
|
};
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
passive: handlerOptions.passive || false,
|
|
2107
|
-
};
|
|
2108
|
-
if (hasExplicitTarget) {
|
|
2109
|
-
scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2232
|
+
const attach = (target) => {
|
|
2233
|
+
target.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2234
|
+
listenerGroup.push({ target, handler: wrappedMethod });
|
|
2110
2235
|
instance[CLEANUP].events.push({
|
|
2111
|
-
target
|
|
2236
|
+
target,
|
|
2112
2237
|
eventName: baseEventName,
|
|
2113
2238
|
handler: wrappedMethod,
|
|
2114
2239
|
options: listenerOptions,
|
|
2115
2240
|
});
|
|
2241
|
+
};
|
|
2242
|
+
if (hasExplicitTarget) {
|
|
2243
|
+
attach(scopedTarget);
|
|
2116
2244
|
continue;
|
|
2117
2245
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
shadowRoot
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
options: listenerOptions,
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
// Also listen on host element (for clicks on host itself or when no shadow root)
|
|
2129
|
-
targetElement.addEventListener(baseEventName, wrappedMethod, listenerOptions);
|
|
2130
|
-
instance[CLEANUP].events.push({
|
|
2131
|
-
target: targetElement,
|
|
2132
|
-
eventName: baseEventName,
|
|
2133
|
-
handler: wrappedMethod,
|
|
2134
|
-
options: listenerOptions,
|
|
2135
|
-
});
|
|
2246
|
+
// Listen on shadow root for events inside shadow DOM
|
|
2247
|
+
if (shadowRoot)
|
|
2248
|
+
attach(shadowRoot);
|
|
2249
|
+
// Also listen on host element (for clicks on host itself, light-DOM
|
|
2250
|
+
// bubbles, or when no shadow root) — unless light is disabled.
|
|
2251
|
+
if (inLight)
|
|
2252
|
+
attach(targetElement);
|
|
2136
2253
|
}
|
|
2137
2254
|
}
|
|
2138
2255
|
}
|
|
@@ -2152,6 +2269,185 @@ var Snice = (function (exports) {
|
|
|
2152
2269
|
instance[CLEANUP].events = [];
|
|
2153
2270
|
}
|
|
2154
2271
|
|
|
2272
|
+
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
2273
|
+
const DISPATCH_GENERATION = getSymbol('dispatch-generation');
|
|
2274
|
+
function isDispatchGenerationActive(instance, generation) {
|
|
2275
|
+
return (instance[DISPATCH_GENERATION] ?? 0) === generation;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Decorator that automatically dispatches a custom event after a method is called.
|
|
2279
|
+
* The return value of the method becomes the event detail.
|
|
2280
|
+
*
|
|
2281
|
+
* @param eventName The name of the event to dispatch
|
|
2282
|
+
* @param options Optional configuration extending EventInit
|
|
2283
|
+
*/
|
|
2284
|
+
function dispatch(eventName, options) {
|
|
2285
|
+
return function (originalMethod, _context) {
|
|
2286
|
+
// Decorator-instance identity avoids collisions such as event `a_b` on
|
|
2287
|
+
// method `c` versus event `a` on method `b_c`.
|
|
2288
|
+
const timerKey = Symbol(`@dispatch:${eventName}`);
|
|
2289
|
+
return function (...args) {
|
|
2290
|
+
// Resolve against the actual decorated instance for every invocation.
|
|
2291
|
+
// Capture the generation before calling an async method so teardown can
|
|
2292
|
+
// invalidate work that has not reached the scheduling step yet.
|
|
2293
|
+
const generation = this[DISPATCH_GENERATION] ?? 0;
|
|
2294
|
+
// Create timing wrappers for dispatch (per-instance)
|
|
2295
|
+
if (!this[DISPATCH_TIMERS]) {
|
|
2296
|
+
this[DISPATCH_TIMERS] = new Map();
|
|
2297
|
+
}
|
|
2298
|
+
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
2299
|
+
this[DISPATCH_TIMERS].set(timerKey, {
|
|
2300
|
+
debounceTimeout: null,
|
|
2301
|
+
throttleLastCall: 0,
|
|
2302
|
+
throttleTimeout: null,
|
|
2303
|
+
invocation: 0,
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
2307
|
+
const debounceDelay = resolveEventTiming(this, options?.debounce, '@dispatch', 'debounce');
|
|
2308
|
+
const throttleDelay = debounceDelay && debounceDelay > 0
|
|
2309
|
+
? undefined
|
|
2310
|
+
: resolveEventTiming(this, options?.throttle, '@dispatch', 'throttle');
|
|
2311
|
+
const invocation = ++timers.invocation;
|
|
2312
|
+
// A new invocation supersedes deferred work for this decorated method.
|
|
2313
|
+
// In particular, a resolver changing debounce to 0 must not leave the
|
|
2314
|
+
// previous positive-delay timer armed. Throttle trailing work is also
|
|
2315
|
+
// rebuilt below using this invocation's freshly resolved interval.
|
|
2316
|
+
if (timers.debounceTimeout)
|
|
2317
|
+
clearTimeout(timers.debounceTimeout);
|
|
2318
|
+
timers.debounceTimeout = null;
|
|
2319
|
+
if (timers.throttleTimeout)
|
|
2320
|
+
clearTimeout(timers.throttleTimeout);
|
|
2321
|
+
timers.throttleTimeout = null;
|
|
2322
|
+
timers.latestDetail = undefined;
|
|
2323
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2324
|
+
// Switching from throttle to debounce starts a new timing regime.
|
|
2325
|
+
timers.throttleLastCall = 0;
|
|
2326
|
+
}
|
|
2327
|
+
else if (!throttleDelay || throttleDelay <= 0) {
|
|
2328
|
+
// An un-timed invocation cancels both old regimes completely.
|
|
2329
|
+
timers.throttleLastCall = 0;
|
|
2330
|
+
}
|
|
2331
|
+
// Call the original method with preserved this context
|
|
2332
|
+
const result = originalMethod.apply(this, args);
|
|
2333
|
+
// Helper to dispatch the event
|
|
2334
|
+
const doDispatch = (detail) => {
|
|
2335
|
+
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
2336
|
+
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
2340
|
+
// not belong in the CustomEvent constructor dictionary.
|
|
2341
|
+
const event = new CustomEvent(eventName, {
|
|
2342
|
+
bubbles: options?.bubbles ?? true,
|
|
2343
|
+
cancelable: options?.cancelable ?? false,
|
|
2344
|
+
composed: options?.composed ?? true,
|
|
2345
|
+
detail
|
|
2346
|
+
});
|
|
2347
|
+
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
2348
|
+
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
2349
|
+
}
|
|
2350
|
+
if (options?.daemon !== undefined) {
|
|
2351
|
+
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
if (options?.scope !== undefined) {
|
|
2355
|
+
const target = resolveScope(this, options.scope);
|
|
2356
|
+
if (!target) {
|
|
2357
|
+
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
target.dispatchEvent(event);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const target = defaultCommunicationTarget(this);
|
|
2364
|
+
if (!target) {
|
|
2365
|
+
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
2366
|
+
}
|
|
2367
|
+
target.dispatchEvent(event);
|
|
2368
|
+
};
|
|
2369
|
+
// Helper to handle timed dispatch
|
|
2370
|
+
const timedDispatch = (detail) => {
|
|
2371
|
+
if (!isDispatchGenerationActive(this, generation))
|
|
2372
|
+
return;
|
|
2373
|
+
if (debounceDelay && debounceDelay > 0) {
|
|
2374
|
+
timers.debounceTimeout = setTimeout(() => {
|
|
2375
|
+
timers.debounceTimeout = null;
|
|
2376
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2377
|
+
doDispatch(detail);
|
|
2378
|
+
}
|
|
2379
|
+
}, debounceDelay);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (!throttleDelay || throttleDelay <= 0) {
|
|
2383
|
+
doDispatch(detail);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const now = Date.now();
|
|
2387
|
+
const remaining = throttleDelay - (now - timers.throttleLastCall);
|
|
2388
|
+
if (remaining <= 0) {
|
|
2389
|
+
clearTimeout(timers.throttleTimeout);
|
|
2390
|
+
timers.throttleLastCall = now;
|
|
2391
|
+
doDispatch(detail);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
2395
|
+
// data, not the first-suppressed-call detail captured by closure.
|
|
2396
|
+
timers.latestDetail = detail;
|
|
2397
|
+
// Always schedule from the last actual dispatch using the interval
|
|
2398
|
+
// resolved for THIS invocation. A later invocation can therefore
|
|
2399
|
+
// lengthen or shorten the pending trailing window coherently.
|
|
2400
|
+
timers.throttleTimeout = setTimeout(() => {
|
|
2401
|
+
timers.throttleLastCall = Date.now();
|
|
2402
|
+
timers.throttleTimeout = null;
|
|
2403
|
+
const d = timers.latestDetail;
|
|
2404
|
+
timers.latestDetail = undefined;
|
|
2405
|
+
if (isDispatchGenerationActive(this, generation) && timers.invocation === invocation) {
|
|
2406
|
+
doDispatch(d);
|
|
2407
|
+
}
|
|
2408
|
+
}, remaining);
|
|
2409
|
+
};
|
|
2410
|
+
// Handle async methods
|
|
2411
|
+
if (result instanceof Promise) {
|
|
2412
|
+
return result.then((resolvedResult) => {
|
|
2413
|
+
const usesDeferredTiming = (debounceDelay ?? 0) > 0 || (throttleDelay ?? 0) > 0;
|
|
2414
|
+
if (isDispatchGenerationActive(this, generation)
|
|
2415
|
+
&& (!usesDeferredTiming || timers.invocation === invocation)) {
|
|
2416
|
+
timedDispatch(resolvedResult);
|
|
2417
|
+
}
|
|
2418
|
+
return resolvedResult;
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
// Sync method
|
|
2422
|
+
timedDispatch(result);
|
|
2423
|
+
return result;
|
|
2424
|
+
};
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
2429
|
+
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
2430
|
+
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
2431
|
+
*/
|
|
2432
|
+
function clearDispatchTimers(instance) {
|
|
2433
|
+
const generation = (instance[DISPATCH_GENERATION] ?? 0) + 1;
|
|
2434
|
+
instance[DISPATCH_GENERATION] = generation;
|
|
2435
|
+
const timers = instance[DISPATCH_TIMERS];
|
|
2436
|
+
if (!timers)
|
|
2437
|
+
return;
|
|
2438
|
+
for (const t of timers.values()) {
|
|
2439
|
+
if (t.debounceTimeout)
|
|
2440
|
+
clearTimeout(t.debounceTimeout);
|
|
2441
|
+
if (t.throttleTimeout)
|
|
2442
|
+
clearTimeout(t.throttleTimeout);
|
|
2443
|
+
t.debounceTimeout = null;
|
|
2444
|
+
t.throttleTimeout = null;
|
|
2445
|
+
t.throttleLastCall = 0;
|
|
2446
|
+
t.latestDetail = undefined;
|
|
2447
|
+
t.invocation = (t.invocation ?? 0) + 1;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2155
2451
|
/**
|
|
2156
2452
|
* @context decorator for receiving router context updates
|
|
2157
2453
|
*/
|
|
@@ -2690,6 +2986,9 @@ var Snice = (function (exports) {
|
|
|
2690
2986
|
if (!controllerInstance) {
|
|
2691
2987
|
return;
|
|
2692
2988
|
}
|
|
2989
|
+
// Invalidate dispatch work before every teardown path, including an attach
|
|
2990
|
+
// that is still waiting for host readiness and will return early below.
|
|
2991
|
+
clearDispatchTimers(controllerInstance);
|
|
2693
2992
|
// Claim the controller immediately — before any await — so a second detach
|
|
2694
2993
|
// that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
|
|
2695
2994
|
// controller reassignment) finds no instance and returns, instead of running
|
|
@@ -2722,6 +3021,8 @@ var Snice = (function (exports) {
|
|
|
2722
3021
|
else {
|
|
2723
3022
|
await controllerInstance.detach(element);
|
|
2724
3023
|
}
|
|
3024
|
+
// detach() itself may have invoked a decorated dispatch method.
|
|
3025
|
+
clearDispatchTimers(controllerInstance);
|
|
2725
3026
|
controllerInstance.element = null;
|
|
2726
3027
|
// Cleanup @observe observers for controller
|
|
2727
3028
|
cleanupObservers(controllerInstance);
|
|
@@ -3081,36 +3382,184 @@ var Snice = (function (exports) {
|
|
|
3081
3382
|
return null;
|
|
3082
3383
|
}
|
|
3083
3384
|
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3385
|
+
const SAFE_TAG = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
3386
|
+
const SAFE_CLASS = /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/;
|
|
3387
|
+
const AMBIGUOUS_RENDER_HOST_IDENTITY = Symbol('ambiguous-render-host-identity');
|
|
3388
|
+
// Keys are weak; values contain only a sanitized label or the ambiguity
|
|
3389
|
+
// sentinel. The same frozen identity is recorded for the exact constructor
|
|
3390
|
+
// and its exact prototype only after the caller verifies registration.
|
|
3391
|
+
const registeredRenderHosts = new WeakMap();
|
|
3392
|
+
function registerRenderHostIdentity(constructor, tagName) {
|
|
3393
|
+
try {
|
|
3394
|
+
if (typeof constructor !== 'function' || !SAFE_TAG.test(tagName))
|
|
3395
|
+
return;
|
|
3396
|
+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(constructor, 'prototype');
|
|
3397
|
+
const prototype = prototypeDescriptor && 'value' in prototypeDescriptor
|
|
3398
|
+
? prototypeDescriptor.value
|
|
3399
|
+
: undefined;
|
|
3400
|
+
if ((typeof prototype !== 'object' && typeof prototype !== 'function') || prototype === null)
|
|
3401
|
+
return;
|
|
3402
|
+
const nameDescriptor = Object.getOwnPropertyDescriptor(constructor, 'name');
|
|
3403
|
+
const name = nameDescriptor && 'value' in nameDescriptor && typeof nameDescriptor.value === 'string'
|
|
3404
|
+
? nameDescriptor.value
|
|
3405
|
+
: '';
|
|
3406
|
+
const className = SAFE_CLASS.test(name) ? name : '';
|
|
3407
|
+
const identity = Object.freeze({
|
|
3408
|
+
label: className ? `<${tagName}> (${className})` : `<${tagName}>`,
|
|
3409
|
+
});
|
|
3410
|
+
const constructorState = registeredRenderHosts.get(constructor);
|
|
3411
|
+
const prototypeState = registeredRenderHosts.get(prototype);
|
|
3412
|
+
const existingStates = [constructorState, prototypeState].filter((state) => state !== undefined);
|
|
3413
|
+
const ambiguous = existingStates.some(state => state === AMBIGUOUS_RENDER_HOST_IDENTITY || state.label !== identity.label);
|
|
3414
|
+
const nextState = ambiguous
|
|
3415
|
+
? AMBIGUOUS_RENDER_HOST_IDENTITY
|
|
3416
|
+
: existingStates[0] ?? identity;
|
|
3417
|
+
// Update both exact keys in the same synchronous registration call. Once
|
|
3418
|
+
// either key is ambiguous, no later registration can restore attribution.
|
|
3419
|
+
registeredRenderHosts.set(constructor, nextState);
|
|
3420
|
+
registeredRenderHosts.set(prototype, nextState);
|
|
3421
|
+
}
|
|
3422
|
+
catch {
|
|
3423
|
+
// Registration diagnostics are best-effort and must not affect the
|
|
3424
|
+
// successful custom-elements registration that already occurred.
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function registeredIdentity(host) {
|
|
3428
|
+
const prototype = Object.getPrototypeOf(host);
|
|
3429
|
+
if (!prototype)
|
|
3430
|
+
return undefined;
|
|
3431
|
+
const constructorDescriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor');
|
|
3432
|
+
const constructor = constructorDescriptor && 'value' in constructorDescriptor
|
|
3433
|
+
? constructorDescriptor.value
|
|
3434
|
+
: undefined;
|
|
3435
|
+
if (typeof constructor !== 'function')
|
|
3436
|
+
return undefined;
|
|
3437
|
+
const prototypeIdentity = registeredRenderHosts.get(prototype);
|
|
3438
|
+
const constructorIdentity = registeredRenderHosts.get(constructor);
|
|
3439
|
+
return prototypeIdentity !== AMBIGUOUS_RENDER_HOST_IDENTITY &&
|
|
3440
|
+
prototypeIdentity === constructorIdentity
|
|
3441
|
+
? prototypeIdentity
|
|
3442
|
+
: undefined;
|
|
3443
|
+
}
|
|
3444
|
+
/** Capture once at the render boundary; failures fall back without masking the render error. */
|
|
3445
|
+
function captureRenderHostIdentity(host) {
|
|
3446
|
+
let label = '<element>';
|
|
3447
|
+
try {
|
|
3448
|
+
const registration = registeredIdentity(host);
|
|
3449
|
+
if (registration)
|
|
3450
|
+
label = registration.label;
|
|
3451
|
+
}
|
|
3452
|
+
catch {
|
|
3453
|
+
// Host/prototype objects can be proxied. Attribution is best-effort
|
|
3454
|
+
// and must never replace the original render or template error.
|
|
3455
|
+
}
|
|
3456
|
+
return Object.freeze({ label, marker: Symbol('render-host-identity') });
|
|
3457
|
+
}
|
|
3458
|
+
class ContextualRenderError extends Error {
|
|
3459
|
+
#identityMarker;
|
|
3460
|
+
constructor(identity, message, cause) {
|
|
3461
|
+
super(message, { cause });
|
|
3462
|
+
this.#identityMarker = identity.marker;
|
|
3463
|
+
}
|
|
3464
|
+
belongsTo(identity) {
|
|
3465
|
+
return this.#identityMarker === identity.marker;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
function contextualizeRenderError(identity, error) {
|
|
3469
|
+
if (error instanceof ContextualRenderError && error.belongsTo(identity))
|
|
3470
|
+
return error;
|
|
3471
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3472
|
+
const contextual = new ContextualRenderError(identity, `snice: render failed for ${identity.label}: ${message}`, error);
|
|
3473
|
+
if (error instanceof Error)
|
|
3474
|
+
contextual.name = error.name;
|
|
3475
|
+
return contextual;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// Unique marker for dynamic template parts. It parses as a comment node but
|
|
3479
|
+
// does not get escaped in attributes.
|
|
3480
|
+
const templateMarker = `snice$${Math.random().toFixed(9).slice(2)}$`;
|
|
3481
|
+
const templateMarkerMatch = '?' + templateMarker;
|
|
3088
3482
|
// A true comment, not a processing instruction (<?...>): in HTML content both
|
|
3089
3483
|
// parse to the same comment node, but PIs are dropped inside foreign content
|
|
3090
3484
|
// (<svg>), which would silently kill any node binding inside an svg block.
|
|
3091
|
-
const
|
|
3485
|
+
const templateNodeMarker = `<!--${templateMarkerMatch}-->`;
|
|
3092
3486
|
// Escape the `$` chars — as a bare RegExp they'd be end anchors and the
|
|
3093
3487
|
// pattern would never match, so marker-bearing text/comments never split.
|
|
3094
|
-
const
|
|
3488
|
+
const templateMarkerRegex = new RegExp(templateMarker.replace(/\$/g, '\\$'), 'g');
|
|
3489
|
+
function hasTemplateMarker(value) {
|
|
3490
|
+
return value?.includes(templateMarker) ?? false;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3095
3493
|
// Template cache - templates with same string array can be reused
|
|
3096
3494
|
const templateCache = new WeakMap();
|
|
3097
3495
|
// Sentinel for "not yet set" - distinct from undefined/null
|
|
3098
3496
|
const NOT_COMMITTED = Symbol('not-committed');
|
|
3099
|
-
|
|
3497
|
+
const pendingDefinitionRegistries = new WeakMap();
|
|
3498
|
+
function isUpgradedBy(element, constructor) {
|
|
3499
|
+
return Object.prototype.isPrototypeOf.call(constructor.prototype, element);
|
|
3500
|
+
}
|
|
3501
|
+
function flushPreUpgradePropertyBindings(element, registry) {
|
|
3502
|
+
const tagName = element.localName;
|
|
3503
|
+
const constructor = tagName && registry?.get(tagName);
|
|
3504
|
+
if (!constructor || !isUpgradedBy(element, constructor))
|
|
3505
|
+
return;
|
|
3506
|
+
const target = element;
|
|
3507
|
+
const markedBindings = target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3508
|
+
if (!markedBindings?.size)
|
|
3509
|
+
return;
|
|
3510
|
+
for (const propertyName of Array.from(markedBindings)) {
|
|
3511
|
+
// A Snice @property initializer consumes its own marker and expando while
|
|
3512
|
+
// the element upgrades. Anything left belongs to a class that retained
|
|
3513
|
+
// the native/custom property channel, so replay the latest parked value
|
|
3514
|
+
// through that now-upgraded channel.
|
|
3515
|
+
if (Object.prototype.hasOwnProperty.call(target, propertyName)) {
|
|
3516
|
+
const value = target[propertyName];
|
|
3517
|
+
delete target[propertyName];
|
|
3518
|
+
target[propertyName] = value;
|
|
3519
|
+
}
|
|
3520
|
+
markedBindings.delete(propertyName);
|
|
3521
|
+
}
|
|
3522
|
+
delete target[PRE_UPGRADE_PROPERTY_BINDINGS];
|
|
3523
|
+
}
|
|
3524
|
+
function schedulePreUpgradePropertyFlush(element, registry) {
|
|
3525
|
+
if (!registry?.whenDefined || pendingDefinitionRegistries.get(element) === registry)
|
|
3526
|
+
return;
|
|
3527
|
+
pendingDefinitionRegistries.set(element, registry);
|
|
3528
|
+
const reference = new WeakRef(element);
|
|
3529
|
+
void registry.whenDefined(element.localName).then(() => {
|
|
3530
|
+
const target = reference.deref();
|
|
3531
|
+
if (target)
|
|
3532
|
+
flushPreUpgradePropertyBindings(target, registry);
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
function markPreUpgradePropertyBinding(element, propertyName, registry) {
|
|
3100
3536
|
const tagName = element.localName;
|
|
3101
3537
|
if (!tagName?.includes('-'))
|
|
3102
3538
|
return false;
|
|
3103
|
-
const
|
|
3104
|
-
if (
|
|
3105
|
-
|
|
3539
|
+
const registeredConstructor = registry?.get(tagName);
|
|
3540
|
+
if (registeredConstructor) {
|
|
3541
|
+
// Template contents can still contain an unupgraded HTMLElement after the
|
|
3542
|
+
// definition has been registered. Registry presence alone therefore does
|
|
3543
|
+
// not prove that the reactive accessor exists on this clone yet.
|
|
3544
|
+
const isUpgraded = isUpgradedBy(element, registeredConstructor);
|
|
3545
|
+
if (isUpgraded)
|
|
3546
|
+
return false;
|
|
3547
|
+
// A registered non-Snice element (or a Snice element that did not
|
|
3548
|
+
// redeclare this member) must retain the native IDL channel. Parking its
|
|
3549
|
+
// role/title/etc. would replace native reflection with an own expando.
|
|
3550
|
+
const reactiveProperties = registeredConstructor[PROPERTIES];
|
|
3551
|
+
if (!reactiveProperties?.has(propertyName))
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3106
3554
|
const target = element;
|
|
3107
3555
|
if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
|
|
3108
3556
|
target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
|
|
3109
3557
|
target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
|
|
3558
|
+
schedulePreUpgradePropertyFlush(element, registry);
|
|
3110
3559
|
return true;
|
|
3111
3560
|
}
|
|
3112
|
-
function commitPropertyValue(element, propertyName, value) {
|
|
3113
|
-
if (!markPreUpgradePropertyBinding(element, propertyName)) {
|
|
3561
|
+
function commitPropertyValue(element, propertyName, value, registry) {
|
|
3562
|
+
if (!markPreUpgradePropertyBinding(element, propertyName, registry)) {
|
|
3114
3563
|
element[propertyName] = value;
|
|
3115
3564
|
return;
|
|
3116
3565
|
}
|
|
@@ -3210,7 +3659,7 @@ var Snice = (function (exports) {
|
|
|
3210
3659
|
if (tagName === 'if') {
|
|
3211
3660
|
// <if value="${condition}">children</if>
|
|
3212
3661
|
const valueAttr = element.getAttribute('value');
|
|
3213
|
-
if (valueAttr && valueAttr.includes(
|
|
3662
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3214
3663
|
// Remove the value attribute
|
|
3215
3664
|
element.removeAttribute('value');
|
|
3216
3665
|
this.parts.push({
|
|
@@ -3229,7 +3678,7 @@ var Snice = (function (exports) {
|
|
|
3229
3678
|
if (tagName === 'case') {
|
|
3230
3679
|
// <case value="${value}">children</case>
|
|
3231
3680
|
const valueAttr = element.getAttribute('value');
|
|
3232
|
-
if (valueAttr && valueAttr.includes(
|
|
3681
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3233
3682
|
for (const child of Array.from(element.childNodes)) {
|
|
3234
3683
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
3235
3684
|
const branchTag = child.tagName.toLowerCase();
|
|
@@ -3238,7 +3687,7 @@ var Snice = (function (exports) {
|
|
|
3238
3687
|
}
|
|
3239
3688
|
}
|
|
3240
3689
|
else if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim() ||
|
|
3241
|
-
child.nodeType === Node.COMMENT_NODE && child.data.includes(
|
|
3690
|
+
child.nodeType === Node.COMMENT_NODE && child.data.includes(templateMarker)) {
|
|
3242
3691
|
throw new Error('snice: content inside <case> must be nested in a <when> or <default> branch.');
|
|
3243
3692
|
}
|
|
3244
3693
|
}
|
|
@@ -3258,7 +3707,7 @@ var Snice = (function (exports) {
|
|
|
3258
3707
|
}
|
|
3259
3708
|
if (tagName === 'else-if') {
|
|
3260
3709
|
const valueAttr = element.getAttribute('value');
|
|
3261
|
-
if (valueAttr && valueAttr.includes(
|
|
3710
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3262
3711
|
element.removeAttribute('value');
|
|
3263
3712
|
this.parts.push({
|
|
3264
3713
|
type: 'conditional-else-if',
|
|
@@ -3273,7 +3722,7 @@ var Snice = (function (exports) {
|
|
|
3273
3722
|
}
|
|
3274
3723
|
if (tagName === 'when') {
|
|
3275
3724
|
const valueAttr = element.getAttribute('value');
|
|
3276
|
-
if (valueAttr && valueAttr.includes(
|
|
3725
|
+
if (valueAttr && valueAttr.includes(templateMarker)) {
|
|
3277
3726
|
element.removeAttribute('value');
|
|
3278
3727
|
this.parts.push({
|
|
3279
3728
|
type: 'conditional-when',
|
|
@@ -3290,12 +3739,12 @@ var Snice = (function (exports) {
|
|
|
3290
3739
|
const attr = attributes[i];
|
|
3291
3740
|
const value = attr.value;
|
|
3292
3741
|
// Check for attribute bindings
|
|
3293
|
-
if (value.includes(
|
|
3742
|
+
if (value.includes(templateMarker)) {
|
|
3294
3743
|
attrsToRemove.push(attr);
|
|
3295
3744
|
// Get original attribute name with preserved case
|
|
3296
3745
|
const originalName = attrNamesForParts[partIndex] || attr.name;
|
|
3297
3746
|
// Extract static string segments by splitting on marker
|
|
3298
|
-
const attrStrings = value.split(
|
|
3747
|
+
const attrStrings = value.split(templateMarker);
|
|
3299
3748
|
// Number of expressions = number of markers = attrStrings.length - 1
|
|
3300
3749
|
const expressionCount = attrStrings.length - 1;
|
|
3301
3750
|
// Property/boolean/event bindings take a SINGLE expression — they
|
|
@@ -3436,7 +3885,7 @@ var Snice = (function (exports) {
|
|
|
3436
3885
|
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
3437
3886
|
const comment = node;
|
|
3438
3887
|
// Check for marker match (processing instruction becomes comment)
|
|
3439
|
-
if (comment.data ===
|
|
3888
|
+
if (comment.data === templateMarkerMatch) {
|
|
3440
3889
|
// Node part
|
|
3441
3890
|
const parent = comment.parentNode;
|
|
3442
3891
|
const endNode = document.createComment('');
|
|
@@ -3448,11 +3897,11 @@ var Snice = (function (exports) {
|
|
|
3448
3897
|
endNode
|
|
3449
3898
|
});
|
|
3450
3899
|
}
|
|
3451
|
-
else if (comment.data.includes(
|
|
3900
|
+
else if (comment.data.includes(templateMarker)) {
|
|
3452
3901
|
// Binding(s) inside an authored HTML comment (<!-- ${x} -->).
|
|
3453
3902
|
// Render values into the comment text; critically, consume the
|
|
3454
3903
|
// right number of value indices so later parts stay aligned.
|
|
3455
|
-
const commentStrings = comment.data.split(
|
|
3904
|
+
const commentStrings = comment.data.split(templateMarkerRegex);
|
|
3456
3905
|
this.parts.push({
|
|
3457
3906
|
type: 'comment',
|
|
3458
3907
|
index: partIndex,
|
|
@@ -3465,10 +3914,10 @@ var Snice = (function (exports) {
|
|
|
3465
3914
|
else if (node.nodeType === Node.TEXT_NODE) {
|
|
3466
3915
|
const text = node;
|
|
3467
3916
|
const data = text.data;
|
|
3468
|
-
if (data.includes(
|
|
3917
|
+
if (data.includes(templateMarker)) {
|
|
3469
3918
|
// Split text node at markers
|
|
3470
3919
|
const parent = text.parentNode;
|
|
3471
|
-
const parts = data.split(
|
|
3920
|
+
const parts = data.split(templateMarkerRegex);
|
|
3472
3921
|
const lastIndex = parts.length - 1;
|
|
3473
3922
|
for (let i = 0; i < lastIndex; i++) {
|
|
3474
3923
|
parent.insertBefore(document.createTextNode(parts[i]), text);
|
|
@@ -3590,13 +4039,13 @@ var Snice = (function (exports) {
|
|
|
3590
4039
|
// the value index stays aligned (a swallowed marker would shift every
|
|
3591
4040
|
// binding after the comment).
|
|
3592
4041
|
attrNamesForParts.push('');
|
|
3593
|
-
htmlParts.push(
|
|
4042
|
+
htmlParts.push(templateMarker);
|
|
3594
4043
|
}
|
|
3595
4044
|
else if (inAttrValue) {
|
|
3596
4045
|
// We're inside a quoted attribute value - this is an attribute binding
|
|
3597
4046
|
// For subsequent interpolations in same attribute, keep using same attr name
|
|
3598
4047
|
attrNamesForParts.push(currentAttrName);
|
|
3599
|
-
htmlParts.push(
|
|
4048
|
+
htmlParts.push(templateMarker);
|
|
3600
4049
|
}
|
|
3601
4050
|
else if (inTag) {
|
|
3602
4051
|
// Inside tag but not in attr value - check for special cases
|
|
@@ -3611,7 +4060,7 @@ var Snice = (function (exports) {
|
|
|
3611
4060
|
}
|
|
3612
4061
|
currentAttrName = trimmed.substring(attrStart + 1, trimmed.length - 1).trim();
|
|
3613
4062
|
attrNamesForParts.push(currentAttrName);
|
|
3614
|
-
htmlParts.push(
|
|
4063
|
+
htmlParts.push(templateMarker);
|
|
3615
4064
|
}
|
|
3616
4065
|
else {
|
|
3617
4066
|
// Check if this is a meta element (<if> or <case>)
|
|
@@ -3619,7 +4068,7 @@ var Snice = (function (exports) {
|
|
|
3619
4068
|
if (metaElementMatch) {
|
|
3620
4069
|
currentAttrName = 'value';
|
|
3621
4070
|
attrNamesForParts.push('value');
|
|
3622
|
-
htmlParts.push(`value="${
|
|
4071
|
+
htmlParts.push(`value="${templateMarker}"`);
|
|
3623
4072
|
}
|
|
3624
4073
|
else {
|
|
3625
4074
|
const nearbyTemplate = (str.slice(-40) + '${…}' + (strings[i + 1] ?? '').slice(0, 40)).replace(/\s+/g, ' ').trim();
|
|
@@ -3632,7 +4081,7 @@ var Snice = (function (exports) {
|
|
|
3632
4081
|
else {
|
|
3633
4082
|
// Outside any tag - this is node content
|
|
3634
4083
|
attrNamesForParts.push('');
|
|
3635
|
-
htmlParts.push(
|
|
4084
|
+
htmlParts.push(templateNodeMarker);
|
|
3636
4085
|
}
|
|
3637
4086
|
}
|
|
3638
4087
|
}
|
|
@@ -3666,22 +4115,37 @@ var Snice = (function (exports) {
|
|
|
3666
4115
|
* Extract the key of a list item rendered from a template with a
|
|
3667
4116
|
* `key=${...}` binding. Returns undefined for unkeyed items.
|
|
3668
4117
|
*/
|
|
3669
|
-
function getItemKey(item) {
|
|
4118
|
+
function getItemKey(item, renderIdentity) {
|
|
3670
4119
|
if (!isTemplateResult(item))
|
|
3671
4120
|
return undefined;
|
|
3672
|
-
|
|
4121
|
+
let tmpl;
|
|
4122
|
+
try {
|
|
4123
|
+
tmpl = prepareTemplate(item);
|
|
4124
|
+
}
|
|
4125
|
+
catch (error) {
|
|
4126
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4127
|
+
}
|
|
3673
4128
|
return tmpl.keyIndex === -1 ? undefined : item.values[tmpl.keyIndex];
|
|
3674
4129
|
}
|
|
3675
4130
|
/**
|
|
3676
4131
|
* Instance of a rendered template
|
|
3677
4132
|
*/
|
|
3678
4133
|
class TemplateInstance {
|
|
3679
|
-
|
|
4134
|
+
#renderIdentity;
|
|
4135
|
+
#customElementRegistry;
|
|
4136
|
+
constructor(result, renderIdentity = null, customElementRegistry = document.defaultView?.customElements ?? globalThis.customElements ?? null) {
|
|
3680
4137
|
this.parts = [];
|
|
3681
4138
|
this.fragment = null;
|
|
3682
4139
|
this.conditionalParts = []; // if/case parts with their indices
|
|
3683
4140
|
this.regularParts = []; // all other parts with their indices
|
|
3684
|
-
this
|
|
4141
|
+
this.#renderIdentity = renderIdentity;
|
|
4142
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4143
|
+
try {
|
|
4144
|
+
this.template = prepareTemplate(result);
|
|
4145
|
+
}
|
|
4146
|
+
catch (error) {
|
|
4147
|
+
throw renderIdentity ? contextualizeRenderError(renderIdentity, error) : error;
|
|
4148
|
+
}
|
|
3685
4149
|
this.strings = result.strings;
|
|
3686
4150
|
}
|
|
3687
4151
|
/**
|
|
@@ -3691,6 +4155,10 @@ var Snice = (function (exports) {
|
|
|
3691
4155
|
isSameTemplate(strings) {
|
|
3692
4156
|
return this.strings === strings;
|
|
3693
4157
|
}
|
|
4158
|
+
/** Internal attribution value; intentionally contains no live renderer host. */
|
|
4159
|
+
getRenderHostIdentity() {
|
|
4160
|
+
return this.#renderIdentity;
|
|
4161
|
+
}
|
|
3694
4162
|
renderFragment() {
|
|
3695
4163
|
if (!this.fragment) {
|
|
3696
4164
|
// First render - clone template and create parts
|
|
@@ -3719,7 +4187,7 @@ var Snice = (function (exports) {
|
|
|
3719
4187
|
case 'node':
|
|
3720
4188
|
const startNode = nodeMap.get(partDef.startNode);
|
|
3721
4189
|
const endNode = nodeMap.get(partDef.endNode);
|
|
3722
|
-
part = new NodePart(startNode, endNode);
|
|
4190
|
+
part = new NodePart(startNode, endNode, this.#renderIdentity, this.#customElementRegistry);
|
|
3723
4191
|
break;
|
|
3724
4192
|
case 'attribute':
|
|
3725
4193
|
const attrElement = nodeMap.get(partDef.element);
|
|
@@ -3727,7 +4195,7 @@ var Snice = (function (exports) {
|
|
|
3727
4195
|
break;
|
|
3728
4196
|
case 'property':
|
|
3729
4197
|
const propElement = nodeMap.get(partDef.element);
|
|
3730
|
-
part = new PropertyPart(propElement, partDef.name);
|
|
4198
|
+
part = new PropertyPart(propElement, partDef.name, this.#customElementRegistry);
|
|
3731
4199
|
break;
|
|
3732
4200
|
case 'boolean-attribute':
|
|
3733
4201
|
const boolElement = nodeMap.get(partDef.element);
|
|
@@ -3747,7 +4215,7 @@ var Snice = (function (exports) {
|
|
|
3747
4215
|
break;
|
|
3748
4216
|
case 'spread':
|
|
3749
4217
|
const spreadElement = nodeMap.get(partDef.element);
|
|
3750
|
-
part = new SpreadPart(spreadElement, partDef.name);
|
|
4218
|
+
part = new SpreadPart(spreadElement, partDef.name, this.#customElementRegistry);
|
|
3751
4219
|
break;
|
|
3752
4220
|
case 'controller':
|
|
3753
4221
|
const controllerElement = nodeMap.get(partDef.element);
|
|
@@ -3926,7 +4394,9 @@ var Snice = (function (exports) {
|
|
|
3926
4394
|
* - Reuses child NodeParts when rendering arrays
|
|
3927
4395
|
*/
|
|
3928
4396
|
class NodePart extends Part {
|
|
3929
|
-
|
|
4397
|
+
#renderIdentity;
|
|
4398
|
+
#customElementRegistry;
|
|
4399
|
+
constructor(startNode, endNode, renderIdentity = null, customElementRegistry = null) {
|
|
3930
4400
|
super();
|
|
3931
4401
|
this.type = 'node';
|
|
3932
4402
|
this._committedValue = NOT_COMMITTED;
|
|
@@ -3941,6 +4411,11 @@ var Snice = (function (exports) {
|
|
|
3941
4411
|
this._committingAsyncValue = false;
|
|
3942
4412
|
this.startNode = startNode;
|
|
3943
4413
|
this.endNode = endNode;
|
|
4414
|
+
this.#renderIdentity = renderIdentity;
|
|
4415
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4416
|
+
}
|
|
4417
|
+
_withRenderContext(error) {
|
|
4418
|
+
return this.#renderIdentity ? contextualizeRenderError(this.#renderIdentity, error) : error;
|
|
3944
4419
|
}
|
|
3945
4420
|
commit(value) {
|
|
3946
4421
|
// Handle noChange sentinel
|
|
@@ -4049,7 +4524,7 @@ var Snice = (function (exports) {
|
|
|
4049
4524
|
// Different template or first render: prepare and validate the replacement
|
|
4050
4525
|
// while it is detached. If a binding throws, the currently
|
|
4051
4526
|
// committed range remains intact and its lifecycle stays connected.
|
|
4052
|
-
const instance = new TemplateInstance(result);
|
|
4527
|
+
const instance = new TemplateInstance(result, this.#renderIdentity, this.#customElementRegistry);
|
|
4053
4528
|
const fragment = instance.renderFragment();
|
|
4054
4529
|
try {
|
|
4055
4530
|
instance.update(result.values);
|
|
@@ -4100,7 +4575,9 @@ var Snice = (function (exports) {
|
|
|
4100
4575
|
*/
|
|
4101
4576
|
_commitIterable(value, explicitKeys) {
|
|
4102
4577
|
const items = Array.isArray(value) ? value : Array.from(value);
|
|
4103
|
-
const newKeys = explicitKeys
|
|
4578
|
+
const newKeys = explicitKeys
|
|
4579
|
+
? Array.from(explicitKeys)
|
|
4580
|
+
: items.map(item => getItemKey(item, this.#renderIdentity));
|
|
4104
4581
|
if (newKeys.length !== items.length) {
|
|
4105
4582
|
throw new Error('snice: keyed iterable produced a different number of keys and values.');
|
|
4106
4583
|
}
|
|
@@ -4146,7 +4623,7 @@ var Snice = (function (exports) {
|
|
|
4146
4623
|
const endMarker = document.createComment('');
|
|
4147
4624
|
this._insertBefore(startMarker);
|
|
4148
4625
|
this._insertBefore(endMarker);
|
|
4149
|
-
itemPart = new NodePart(startMarker, endMarker);
|
|
4626
|
+
itemPart = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4150
4627
|
itemParts.push(itemPart);
|
|
4151
4628
|
}
|
|
4152
4629
|
else {
|
|
@@ -4250,7 +4727,7 @@ var Snice = (function (exports) {
|
|
|
4250
4727
|
const endMarker = document.createComment('');
|
|
4251
4728
|
parent.insertBefore(startMarker, ref);
|
|
4252
4729
|
parent.insertBefore(endMarker, ref);
|
|
4253
|
-
part = new NodePart(startMarker, endMarker);
|
|
4730
|
+
part = new NodePart(startMarker, endMarker, this.#renderIdentity, this.#customElementRegistry);
|
|
4254
4731
|
newParts[i] = part;
|
|
4255
4732
|
}
|
|
4256
4733
|
part.commit(items[i]);
|
|
@@ -4322,7 +4799,7 @@ var Snice = (function (exports) {
|
|
|
4322
4799
|
catch (error) {
|
|
4323
4800
|
this._asyncRunning = false;
|
|
4324
4801
|
this._asyncCompleted = true;
|
|
4325
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4802
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4326
4803
|
return;
|
|
4327
4804
|
}
|
|
4328
4805
|
void (async () => {
|
|
@@ -4335,8 +4812,9 @@ var Snice = (function (exports) {
|
|
|
4335
4812
|
}
|
|
4336
4813
|
}
|
|
4337
4814
|
catch (error) {
|
|
4338
|
-
if (version === this._asyncVersion)
|
|
4339
|
-
console.error('snice: async iterable template value failed:', error);
|
|
4815
|
+
if (version === this._asyncVersion) {
|
|
4816
|
+
console.error('snice: async iterable template value failed:', this._withRenderContext(error));
|
|
4817
|
+
}
|
|
4340
4818
|
}
|
|
4341
4819
|
finally {
|
|
4342
4820
|
if (version === this._asyncVersion) {
|
|
@@ -4357,14 +4835,14 @@ var Snice = (function (exports) {
|
|
|
4357
4835
|
this._commitAsyncValue(value);
|
|
4358
4836
|
}
|
|
4359
4837
|
catch (error) {
|
|
4360
|
-
console.error('snice: promise template value failed:', error);
|
|
4838
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4361
4839
|
}
|
|
4362
4840
|
}, error => {
|
|
4363
4841
|
if (version !== this._asyncVersion)
|
|
4364
4842
|
return;
|
|
4365
4843
|
this._asyncRunning = false;
|
|
4366
4844
|
this._asyncCompleted = true;
|
|
4367
|
-
console.error('snice: promise template value failed:', error);
|
|
4845
|
+
console.error('snice: promise template value failed:', this._withRenderContext(error));
|
|
4368
4846
|
});
|
|
4369
4847
|
}
|
|
4370
4848
|
_commitAsyncValue(value) {
|
|
@@ -4732,7 +5210,8 @@ var Snice = (function (exports) {
|
|
|
4732
5210
|
}
|
|
4733
5211
|
/** Named spreads: ...props, ...attrs, and ...events. */
|
|
4734
5212
|
class SpreadPart extends Part {
|
|
4735
|
-
|
|
5213
|
+
#customElementRegistry;
|
|
5214
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4736
5215
|
super();
|
|
4737
5216
|
this.type = 'spread';
|
|
4738
5217
|
this.committed = {};
|
|
@@ -4740,6 +5219,7 @@ var Snice = (function (exports) {
|
|
|
4740
5219
|
this.consumedOnce = new Map();
|
|
4741
5220
|
this.element = element;
|
|
4742
5221
|
this.name = name;
|
|
5222
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4743
5223
|
}
|
|
4744
5224
|
commit(value) {
|
|
4745
5225
|
if (value === noChange)
|
|
@@ -4761,13 +5241,13 @@ var Snice = (function (exports) {
|
|
|
4761
5241
|
commitProperties(next) {
|
|
4762
5242
|
for (const key of Object.keys(this.committed)) {
|
|
4763
5243
|
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
4764
|
-
commitPropertyValue(this.element, key, undefined);
|
|
5244
|
+
commitPropertyValue(this.element, key, undefined, this.#customElementRegistry);
|
|
4765
5245
|
}
|
|
4766
5246
|
}
|
|
4767
5247
|
for (const [key, value] of Object.entries(next)) {
|
|
4768
5248
|
if (Object.is(this.committed[key], value))
|
|
4769
5249
|
continue;
|
|
4770
|
-
commitPropertyValue(this.element, key, value === nothing ? undefined : value);
|
|
5250
|
+
commitPropertyValue(this.element, key, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
4771
5251
|
}
|
|
4772
5252
|
}
|
|
4773
5253
|
commitAttributes(next) {
|
|
@@ -4851,6 +5331,9 @@ var Snice = (function (exports) {
|
|
|
4851
5331
|
this.detachListeners();
|
|
4852
5332
|
}
|
|
4853
5333
|
reconnected() {
|
|
5334
|
+
if (this.name === 'props' || this.name === 'properties') {
|
|
5335
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
5336
|
+
}
|
|
4854
5337
|
if (this.name === 'events' && this.listeners.size === 0)
|
|
4855
5338
|
this.commitEvents(this.committed);
|
|
4856
5339
|
}
|
|
@@ -4865,12 +5348,14 @@ var Snice = (function (exports) {
|
|
|
4865
5348
|
* PropertyPart handles property bindings (.property=${value})
|
|
4866
5349
|
*/
|
|
4867
5350
|
class PropertyPart extends Part {
|
|
4868
|
-
|
|
5351
|
+
#customElementRegistry;
|
|
5352
|
+
constructor(element, name, customElementRegistry = null) {
|
|
4869
5353
|
super();
|
|
4870
5354
|
this.type = 'property';
|
|
4871
5355
|
this._committedValue = NOT_COMMITTED;
|
|
4872
5356
|
this.element = element;
|
|
4873
5357
|
this.name = name;
|
|
5358
|
+
this.#customElementRegistry = customElementRegistry;
|
|
4874
5359
|
}
|
|
4875
5360
|
commit(value) {
|
|
4876
5361
|
if (value === noChange)
|
|
@@ -4894,7 +5379,10 @@ var Snice = (function (exports) {
|
|
|
4894
5379
|
return;
|
|
4895
5380
|
}
|
|
4896
5381
|
this._committedValue = value;
|
|
4897
|
-
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
|
|
5382
|
+
commitPropertyValue(this.element, this.name, value === nothing ? undefined : value, this.#customElementRegistry);
|
|
5383
|
+
}
|
|
5384
|
+
reconnected() {
|
|
5385
|
+
flushPreUpgradePropertyBindings(this.element, this.#customElementRegistry);
|
|
4898
5386
|
}
|
|
4899
5387
|
clear() {
|
|
4900
5388
|
// This part is being destroyed with its owning template. The element is
|
|
@@ -5614,16 +6102,6 @@ var Snice = (function (exports) {
|
|
|
5614
6102
|
* Production default is false (log and keep the previous DOM).
|
|
5615
6103
|
*/
|
|
5616
6104
|
let strictRenderErrors = false;
|
|
5617
|
-
function contextualizeRenderError(element, error) {
|
|
5618
|
-
const tag = element.tagName?.toLowerCase() || 'element';
|
|
5619
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5620
|
-
const contextual = new Error(`snice: render failed for <${tag}>: ${message}`, {
|
|
5621
|
-
cause: error
|
|
5622
|
-
});
|
|
5623
|
-
if (error instanceof Error)
|
|
5624
|
-
contextual.name = error.name;
|
|
5625
|
-
return contextual;
|
|
5626
|
-
}
|
|
5627
6105
|
function setStrictRenderErrors(value) {
|
|
5628
6106
|
strictRenderErrors = value;
|
|
5629
6107
|
}
|
|
@@ -5726,6 +6204,8 @@ var Snice = (function (exports) {
|
|
|
5726
6204
|
* Perform the actual render of an element
|
|
5727
6205
|
*/
|
|
5728
6206
|
function performRender(element, options, precomputedResult) {
|
|
6207
|
+
const capturedIdentity = captureRenderHostIdentity(element);
|
|
6208
|
+
let renderIdentity = capturedIdentity;
|
|
5729
6209
|
const hasPrecomputedResult = arguments.length >= 3;
|
|
5730
6210
|
const renderMethod = element[RENDER_METHOD];
|
|
5731
6211
|
if (!renderMethod) {
|
|
@@ -5781,12 +6261,16 @@ var Snice = (function (exports) {
|
|
|
5781
6261
|
}
|
|
5782
6262
|
let instance = element[RENDER_INSTANCE];
|
|
5783
6263
|
if (instance && instance.isSameTemplate(result.strings)) {
|
|
6264
|
+
renderIdentity = instance.getRenderHostIdentity() ?? capturedIdentity;
|
|
5784
6265
|
instance.update(result.values);
|
|
5785
6266
|
flushRenderCallbacks(element);
|
|
5786
6267
|
scheduleAutofocus(element);
|
|
5787
6268
|
return;
|
|
5788
6269
|
}
|
|
5789
|
-
const
|
|
6270
|
+
const customElementRegistry = renderRoot instanceof ShadowRoot
|
|
6271
|
+
? renderRoot.customElementRegistry ?? renderRoot.ownerDocument.defaultView?.customElements ?? null
|
|
6272
|
+
: renderRoot.ownerDocument.defaultView?.customElements ?? null;
|
|
6273
|
+
const nextInstance = new TemplateInstance(result, capturedIdentity, customElementRegistry);
|
|
5790
6274
|
const nextFragment = nextInstance.renderFragment();
|
|
5791
6275
|
// Commit while detached first. A malformed binding can then fail without
|
|
5792
6276
|
// exposing a half-updated tree. Connection-aware parts are paired with
|
|
@@ -5846,7 +6330,7 @@ var Snice = (function (exports) {
|
|
|
5846
6330
|
throw cleanupError;
|
|
5847
6331
|
}
|
|
5848
6332
|
catch (error) {
|
|
5849
|
-
const contextual = contextualizeRenderError(
|
|
6333
|
+
const contextual = contextualizeRenderError(renderIdentity, error);
|
|
5850
6334
|
if (strictRenderErrors)
|
|
5851
6335
|
throw contextual;
|
|
5852
6336
|
console.error('Error rendering element:', contextual);
|
|
@@ -6134,134 +6618,6 @@ var Snice = (function (exports) {
|
|
|
6134
6618
|
}
|
|
6135
6619
|
}
|
|
6136
6620
|
|
|
6137
|
-
// @dispatch decorator - auto-dispatches custom events from method return values
|
|
6138
|
-
/**
|
|
6139
|
-
* Decorator that automatically dispatches a custom event after a method is called.
|
|
6140
|
-
* The return value of the method becomes the event detail.
|
|
6141
|
-
*
|
|
6142
|
-
* @param eventName The name of the event to dispatch
|
|
6143
|
-
* @param options Optional configuration extending EventInit
|
|
6144
|
-
*/
|
|
6145
|
-
function dispatch(eventName, options) {
|
|
6146
|
-
return function (originalMethod, _context) {
|
|
6147
|
-
return function (...args) {
|
|
6148
|
-
// Create timing wrappers for dispatch (per-instance)
|
|
6149
|
-
if (!this[DISPATCH_TIMERS]) {
|
|
6150
|
-
this[DISPATCH_TIMERS] = new Map();
|
|
6151
|
-
}
|
|
6152
|
-
const timerKey = `${eventName}_${_context.name}`;
|
|
6153
|
-
if (!this[DISPATCH_TIMERS].has(timerKey)) {
|
|
6154
|
-
this[DISPATCH_TIMERS].set(timerKey, {
|
|
6155
|
-
debounceTimeout: null,
|
|
6156
|
-
throttleLastCall: 0,
|
|
6157
|
-
throttleTimeout: null
|
|
6158
|
-
});
|
|
6159
|
-
}
|
|
6160
|
-
const timers = this[DISPATCH_TIMERS].get(timerKey);
|
|
6161
|
-
// Call the original method with preserved this context
|
|
6162
|
-
const result = originalMethod.apply(this, args);
|
|
6163
|
-
// Helper to dispatch the event
|
|
6164
|
-
const doDispatch = (detail) => {
|
|
6165
|
-
// Skip dispatch if result is undefined and dispatchOnUndefined is false
|
|
6166
|
-
if (detail === undefined && options?.dispatchOnUndefined === false) {
|
|
6167
|
-
return;
|
|
6168
|
-
}
|
|
6169
|
-
// Copy only platform EventInit fields; Snice-only decorator options do
|
|
6170
|
-
// not belong in the CustomEvent constructor dictionary.
|
|
6171
|
-
const event = new CustomEvent(eventName, {
|
|
6172
|
-
bubbles: options?.bubbles ?? true,
|
|
6173
|
-
cancelable: options?.cancelable ?? false,
|
|
6174
|
-
composed: options?.composed ?? true,
|
|
6175
|
-
detail
|
|
6176
|
-
});
|
|
6177
|
-
if (options?.scope !== undefined && options?.daemon !== undefined) {
|
|
6178
|
-
throw new TypeError(`@dispatch('${eventName}') cannot use both scope and daemon.`);
|
|
6179
|
-
}
|
|
6180
|
-
if (options?.daemon !== undefined) {
|
|
6181
|
-
requireDaemonTarget(this, options.daemon).dispatchEvent(event);
|
|
6182
|
-
return;
|
|
6183
|
-
}
|
|
6184
|
-
if (options?.scope !== undefined) {
|
|
6185
|
-
const target = resolveScope(this, options.scope);
|
|
6186
|
-
if (!target) {
|
|
6187
|
-
console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
|
|
6188
|
-
return;
|
|
6189
|
-
}
|
|
6190
|
-
target.dispatchEvent(event);
|
|
6191
|
-
return;
|
|
6192
|
-
}
|
|
6193
|
-
const target = defaultCommunicationTarget(this);
|
|
6194
|
-
if (!target) {
|
|
6195
|
-
throw new TypeError(`@dispatch('${eventName}') requires an element, attached controller, or provided @daemon instance.`);
|
|
6196
|
-
}
|
|
6197
|
-
target.dispatchEvent(event);
|
|
6198
|
-
};
|
|
6199
|
-
// Helper to handle timed dispatch
|
|
6200
|
-
const timedDispatch = (detail) => {
|
|
6201
|
-
if (options?.debounce) {
|
|
6202
|
-
clearTimeout(timers.debounceTimeout);
|
|
6203
|
-
timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
|
|
6204
|
-
return;
|
|
6205
|
-
}
|
|
6206
|
-
if (!options?.throttle) {
|
|
6207
|
-
doDispatch(detail);
|
|
6208
|
-
return;
|
|
6209
|
-
}
|
|
6210
|
-
const now = Date.now();
|
|
6211
|
-
const remaining = options.throttle - (now - timers.throttleLastCall);
|
|
6212
|
-
if (remaining <= 0) {
|
|
6213
|
-
clearTimeout(timers.throttleTimeout);
|
|
6214
|
-
timers.throttleLastCall = now;
|
|
6215
|
-
doDispatch(detail);
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
// Record the LATEST detail so the trailing dispatch carries fresh
|
|
6219
|
-
// data, not the first-suppressed-call detail captured by closure.
|
|
6220
|
-
timers.latestDetail = detail;
|
|
6221
|
-
if (!timers.throttleTimeout) {
|
|
6222
|
-
timers.throttleTimeout = setTimeout(() => {
|
|
6223
|
-
timers.throttleLastCall = Date.now();
|
|
6224
|
-
timers.throttleTimeout = null;
|
|
6225
|
-
const d = timers.latestDetail;
|
|
6226
|
-
timers.latestDetail = undefined;
|
|
6227
|
-
doDispatch(d);
|
|
6228
|
-
}, remaining);
|
|
6229
|
-
}
|
|
6230
|
-
};
|
|
6231
|
-
// Handle async methods
|
|
6232
|
-
if (result instanceof Promise) {
|
|
6233
|
-
return result.then((resolvedResult) => {
|
|
6234
|
-
timedDispatch(resolvedResult);
|
|
6235
|
-
return resolvedResult;
|
|
6236
|
-
});
|
|
6237
|
-
}
|
|
6238
|
-
// Sync method
|
|
6239
|
-
timedDispatch(result);
|
|
6240
|
-
return result;
|
|
6241
|
-
};
|
|
6242
|
-
};
|
|
6243
|
-
}
|
|
6244
|
-
/**
|
|
6245
|
-
* Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
|
|
6246
|
-
* disconnect, so a queued event doesn't fire into a detached node). A dispatch
|
|
6247
|
-
* is a one-shot signal, so pending ones are dropped, not replayed.
|
|
6248
|
-
*/
|
|
6249
|
-
function clearDispatchTimers(instance) {
|
|
6250
|
-
const timers = instance[DISPATCH_TIMERS];
|
|
6251
|
-
if (!timers)
|
|
6252
|
-
return;
|
|
6253
|
-
for (const t of timers.values()) {
|
|
6254
|
-
if (t.debounceTimeout)
|
|
6255
|
-
clearTimeout(t.debounceTimeout);
|
|
6256
|
-
if (t.throttleTimeout)
|
|
6257
|
-
clearTimeout(t.throttleTimeout);
|
|
6258
|
-
t.debounceTimeout = null;
|
|
6259
|
-
t.throttleTimeout = null;
|
|
6260
|
-
t.throttleLastCall = 0;
|
|
6261
|
-
t.latestDetail = undefined;
|
|
6262
|
-
}
|
|
6263
|
-
}
|
|
6264
|
-
|
|
6265
6621
|
/**
|
|
6266
6622
|
* Method decorators for common patterns
|
|
6267
6623
|
* @debounce, @throttle, @once, @memoize
|
|
@@ -7144,6 +7500,10 @@ var Snice = (function (exports) {
|
|
|
7144
7500
|
scheduleAutofocus(this);
|
|
7145
7501
|
};
|
|
7146
7502
|
constructor.prototype.disconnectedCallback = async function () {
|
|
7503
|
+
// Invalidate @dispatch work queued or started before disconnect synchronously.
|
|
7504
|
+
// The platform does not await disconnectedCallback, and @dispose may be
|
|
7505
|
+
// async, so waiting until the end permits events from a detached host.
|
|
7506
|
+
clearDispatchTimers(this);
|
|
7147
7507
|
try {
|
|
7148
7508
|
disconnectRenderTree(this);
|
|
7149
7509
|
}
|
|
@@ -7188,7 +7548,6 @@ var Snice = (function (exports) {
|
|
|
7188
7548
|
if (clearRenderTimers(this)) {
|
|
7189
7549
|
this[PENDING_RECONNECT_RENDER] = true;
|
|
7190
7550
|
}
|
|
7191
|
-
clearDispatchTimers(this);
|
|
7192
7551
|
// @moved / @adopted debounce-throttle timers, likewise dropped so they
|
|
7193
7552
|
// don't fire on a dead element.
|
|
7194
7553
|
clearLifecycleTimers(this, MOVED_TIMERS);
|
|
@@ -7197,6 +7556,11 @@ var Snice = (function (exports) {
|
|
|
7197
7556
|
constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
|
|
7198
7557
|
originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
|
|
7199
7558
|
if (name === 'controller') {
|
|
7559
|
+
// Parsing a dynamic controller binding briefly creates this marker
|
|
7560
|
+
// attribute on the inert template element. It is renderer metadata,
|
|
7561
|
+
// not a registry name, so it must never start an attachment.
|
|
7562
|
+
if (hasTemplateMarker(newValue))
|
|
7563
|
+
return;
|
|
7200
7564
|
// A class attachment reflects its decorator name for DOM diagnostics,
|
|
7201
7565
|
// but the class reference remains the only attachment authority.
|
|
7202
7566
|
if (this[CONTROLLER_ATTRIBUTE_SYNC])
|
|
@@ -7374,12 +7738,16 @@ var Snice = (function (exports) {
|
|
|
7374
7738
|
constructor.prototype[RENDER_OPTIONS] = constructor.renderOptions || {};
|
|
7375
7739
|
}
|
|
7376
7740
|
applyElementFunctionality(constructor);
|
|
7377
|
-
|
|
7741
|
+
const existing = customElements.get(tagName);
|
|
7742
|
+
if (existing) {
|
|
7743
|
+
if (existing === constructor)
|
|
7744
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7378
7745
|
if (globalThis.SNICE_DEBUG)
|
|
7379
7746
|
console.warn(`[snice] "${tagName}" is already registered. Skipping.`);
|
|
7380
7747
|
return constructor;
|
|
7381
7748
|
}
|
|
7382
7749
|
customElements.define(tagName, constructor);
|
|
7750
|
+
registerRenderHostIdentity(constructor, tagName);
|
|
7383
7751
|
return constructor;
|
|
7384
7752
|
}
|
|
7385
7753
|
function element(tagName, options) {
|
|
@@ -8788,6 +9156,72 @@ var Snice = (function (exports) {
|
|
|
8788
9156
|
}, navigation);
|
|
8789
9157
|
}
|
|
8790
9158
|
|
|
9159
|
+
const EMPTY_QUERY_MARKER = '__snice_empty_query_value__';
|
|
9160
|
+
const QUERY_PARAM_PATTERN = /=:[\w-]+\)?$/;
|
|
9161
|
+
function containsDecodedMarker(url, marker) {
|
|
9162
|
+
if (url.includes(marker))
|
|
9163
|
+
return true;
|
|
9164
|
+
const queryIndex = url.indexOf('?');
|
|
9165
|
+
if (queryIndex === -1)
|
|
9166
|
+
return false;
|
|
9167
|
+
return url.slice(queryIndex + 1).split('&').some(part => {
|
|
9168
|
+
try {
|
|
9169
|
+
return decodeURIComponent(part).includes(marker);
|
|
9170
|
+
}
|
|
9171
|
+
catch {
|
|
9172
|
+
return false;
|
|
9173
|
+
}
|
|
9174
|
+
});
|
|
9175
|
+
}
|
|
9176
|
+
function emptyQueryMarker(route, url) {
|
|
9177
|
+
let marker = EMPTY_QUERY_MARKER;
|
|
9178
|
+
while (route.spec.includes(marker) || containsDecodedMarker(url, marker)) {
|
|
9179
|
+
marker += '_';
|
|
9180
|
+
}
|
|
9181
|
+
return marker;
|
|
9182
|
+
}
|
|
9183
|
+
function fillEmptyQueryParams(route, url, marker) {
|
|
9184
|
+
const urlQueryIndex = url.indexOf('?');
|
|
9185
|
+
const routeQueryIndex = route.spec.indexOf('?');
|
|
9186
|
+
if (urlQueryIndex === -1 || routeQueryIndex === -1)
|
|
9187
|
+
return null;
|
|
9188
|
+
const urlParts = url.slice(urlQueryIndex + 1).split('&');
|
|
9189
|
+
const routeParts = route.spec.slice(routeQueryIndex + 1).split('&');
|
|
9190
|
+
let changed = false;
|
|
9191
|
+
const normalizedParts = urlParts.map((part, index) => {
|
|
9192
|
+
if (part.endsWith('=') && QUERY_PARAM_PATTERN.test(routeParts[index] ?? '')) {
|
|
9193
|
+
changed = true;
|
|
9194
|
+
return `${part}${marker}`;
|
|
9195
|
+
}
|
|
9196
|
+
return part;
|
|
9197
|
+
});
|
|
9198
|
+
if (!changed)
|
|
9199
|
+
return null;
|
|
9200
|
+
return `${url.slice(0, urlQueryIndex + 1)}${normalizedParts.join('&')}`;
|
|
9201
|
+
}
|
|
9202
|
+
/**
|
|
9203
|
+
* Match through pica-route while preserving legal empty query parameter values.
|
|
9204
|
+
* pica-route 1.1.2 rejects an empty param and therefore discards the whole route.
|
|
9205
|
+
* Keep this compatibility shim in sync with packages/react/src/route-match.ts.
|
|
9206
|
+
*/
|
|
9207
|
+
function matchRoute(route, url) {
|
|
9208
|
+
const directMatch = route.match(url);
|
|
9209
|
+
if (directMatch !== false)
|
|
9210
|
+
return directMatch;
|
|
9211
|
+
const marker = emptyQueryMarker(route, url);
|
|
9212
|
+
const normalizedUrl = fillEmptyQueryParams(route, url, marker);
|
|
9213
|
+
if (normalizedUrl === null)
|
|
9214
|
+
return false;
|
|
9215
|
+
const params = route.match(normalizedUrl);
|
|
9216
|
+
if (params === false)
|
|
9217
|
+
return false;
|
|
9218
|
+
for (const name of Object.keys(params)) {
|
|
9219
|
+
if (params[name] === marker)
|
|
9220
|
+
params[name] = '';
|
|
9221
|
+
}
|
|
9222
|
+
return params;
|
|
9223
|
+
}
|
|
9224
|
+
|
|
8791
9225
|
// Router historically allows a later Router instance to take over the same
|
|
8792
9226
|
// target. Track only Router-owned provider releases so that handoff remains
|
|
8793
9227
|
// explicit without weakening duplicate protection for public provideContext().
|
|
@@ -8903,8 +9337,13 @@ var Snice = (function (exports) {
|
|
|
8903
9337
|
delete this[CONTEXT_HANDLER];
|
|
8904
9338
|
};
|
|
8905
9339
|
// Define the custom element (skip if already registered)
|
|
8906
|
-
|
|
9340
|
+
const existing = win.customElements.get(pageOptions.tag);
|
|
9341
|
+
if (!existing) {
|
|
8907
9342
|
win.customElements.define(pageOptions.tag, constructor);
|
|
9343
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
9344
|
+
}
|
|
9345
|
+
else if (existing === constructor) {
|
|
9346
|
+
registerRenderHostIdentity(constructor, pageOptions.tag);
|
|
8908
9347
|
}
|
|
8909
9348
|
// Register the routes with guards, layout, and placard
|
|
8910
9349
|
pageOptions.routes.forEach(route => {
|
|
@@ -9154,7 +9593,7 @@ var Snice = (function (exports) {
|
|
|
9154
9593
|
}
|
|
9155
9594
|
async function resolveRoute(path, target, stale) {
|
|
9156
9595
|
for (const route of routes) {
|
|
9157
|
-
const params = route.route
|
|
9596
|
+
const params = matchRoute(route.route, path);
|
|
9158
9597
|
const isMatch = params !== false;
|
|
9159
9598
|
if (!isMatch) {
|
|
9160
9599
|
continue;
|
|
@@ -9731,7 +10170,7 @@ var Snice = (function (exports) {
|
|
|
9731
10170
|
}
|
|
9732
10171
|
|
|
9733
10172
|
/*!
|
|
9734
|
-
* snice v7.
|
|
10173
|
+
* snice v7.6.0
|
|
9735
10174
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
9736
10175
|
* (c) 2024
|
|
9737
10176
|
* Released under the MIT License.
|
|
@@ -9753,7 +10192,7 @@ var Snice = (function (exports) {
|
|
|
9753
10192
|
globalThis.snice.pendingControllerAttachments ??= new Map();
|
|
9754
10193
|
|
|
9755
10194
|
/*!
|
|
9756
|
-
* snice v7.
|
|
10195
|
+
* snice v7.6.0
|
|
9757
10196
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
9758
10197
|
* (c) 2024
|
|
9759
10198
|
* Released under the MIT License.
|