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