foldkit-primitives 0.15.3 → 0.16.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/dist/device/fullscreen.d.ts +28 -0
- package/dist/device/fullscreen.d.ts.map +1 -0
- package/dist/device/fullscreen.js +69 -0
- package/dist/device/fullscreen.js.map +1 -0
- package/dist/device/geolocation.d.ts +56 -0
- package/dist/device/geolocation.d.ts.map +1 -0
- package/dist/device/geolocation.js +87 -0
- package/dist/device/geolocation.js.map +1 -0
- package/dist/device/index.d.ts +5 -0
- package/dist/device/index.js +6 -0
- package/dist/device/media-devices.d.ts +88 -0
- package/dist/device/media-devices.d.ts.map +1 -0
- package/dist/device/media-devices.js +117 -0
- package/dist/device/media-devices.js.map +1 -0
- package/dist/device/media-stream.d.ts +93 -0
- package/dist/device/media-stream.d.ts.map +1 -0
- package/dist/device/media-stream.js +106 -0
- package/dist/device/media-stream.js.map +1 -0
- package/dist/device/permissions.d.ts +106 -0
- package/dist/device/permissions.d.ts.map +1 -0
- package/dist/device/permissions.js +143 -0
- package/dist/device/permissions.js.map +1 -0
- package/dist/dom/autofocus.d.ts +15 -0
- package/dist/dom/autofocus.d.ts.map +1 -0
- package/dist/dom/autofocus.js +22 -0
- package/dist/dom/autofocus.js.map +1 -0
- package/dist/dom/clipboard.d.ts +30 -0
- package/dist/dom/clipboard.d.ts.map +1 -0
- package/dist/dom/clipboard.js +50 -0
- package/dist/dom/clipboard.js.map +1 -0
- package/dist/dom/editable-text.d.ts +72 -0
- package/dist/dom/editable-text.d.ts.map +1 -0
- package/dist/dom/editable-text.js +232 -0
- package/dist/dom/editable-text.js.map +1 -0
- package/dist/dom/fluid-hover.d.ts +93 -0
- package/dist/dom/fluid-hover.d.ts.map +1 -0
- package/dist/dom/fluid-hover.js +294 -0
- package/dist/dom/fluid-hover.js.map +1 -0
- package/dist/dom/focus-scope.d.ts +21 -0
- package/dist/dom/focus-scope.d.ts.map +1 -0
- package/dist/dom/focus-scope.js +93 -0
- package/dist/dom/focus-scope.js.map +1 -0
- package/dist/dom/follow-tab-stop.d.ts +11 -0
- package/dist/dom/follow-tab-stop.d.ts.map +1 -0
- package/dist/dom/follow-tab-stop.js +54 -0
- package/dist/dom/follow-tab-stop.js.map +1 -0
- package/dist/dom/image-decode.d.ts +27 -0
- package/dist/dom/image-decode.d.ts.map +1 -0
- package/dist/dom/image-decode.js +50 -0
- package/dist/dom/image-decode.js.map +1 -0
- package/dist/dom/index.d.ts +24 -0
- package/dist/dom/index.js +25 -0
- package/dist/dom/input-mask.d.ts +35 -0
- package/dist/dom/input-mask.d.ts.map +1 -0
- package/dist/dom/input-mask.js +90 -0
- package/dist/dom/input-mask.js.map +1 -0
- package/dist/dom/keep-in-view.d.ts +16 -0
- package/dist/dom/keep-in-view.d.ts.map +1 -0
- package/dist/dom/keep-in-view.js +38 -0
- package/dist/dom/keep-in-view.js.map +1 -0
- package/dist/dom/keyboard-inset.d.ts +29 -0
- package/dist/dom/keyboard-inset.d.ts.map +1 -0
- package/dist/dom/keyboard-inset.js +45 -0
- package/dist/dom/keyboard-inset.js.map +1 -0
- package/dist/dom/layers.d.ts +6 -0
- package/dist/dom/layers.d.ts.map +1 -0
- package/dist/dom/layers.js +35 -0
- package/dist/dom/layers.js.map +1 -0
- package/dist/dom/measure.d.ts +42 -0
- package/dist/dom/measure.d.ts.map +1 -0
- package/dist/dom/measure.js +123 -0
- package/dist/dom/measure.js.map +1 -0
- package/dist/dom/move.d.ts +45 -0
- package/dist/dom/move.d.ts.map +1 -0
- package/dist/dom/move.js +96 -0
- package/dist/dom/move.js.map +1 -0
- package/dist/dom/pointed-links.d.ts +13 -0
- package/dist/dom/pointed-links.d.ts.map +1 -0
- package/dist/dom/pointed-links.js +59 -0
- package/dist/dom/pointed-links.js.map +1 -0
- package/dist/dom/pointer-drag.d.ts +132 -0
- package/dist/dom/pointer-drag.d.ts.map +1 -0
- package/dist/dom/pointer-drag.js +307 -0
- package/dist/dom/pointer-drag.js.map +1 -0
- package/dist/dom/pointer-position.d.ts +133 -0
- package/dist/dom/pointer-position.d.ts.map +1 -0
- package/dist/dom/pointer-position.js +173 -0
- package/dist/dom/pointer-position.js.map +1 -0
- package/dist/dom/script-loader.d.ts +12 -0
- package/dist/dom/script-loader.d.ts.map +1 -0
- package/dist/dom/script-loader.js +54 -0
- package/dist/dom/script-loader.js.map +1 -0
- package/dist/dom/scroll-bars.d.ts +52 -0
- package/dist/dom/scroll-bars.d.ts.map +1 -0
- package/dist/dom/scroll-bars.js +259 -0
- package/dist/dom/scroll-bars.js.map +1 -0
- package/dist/dom/scroll-settle.d.ts +33 -0
- package/dist/dom/scroll-settle.d.ts.map +1 -0
- package/dist/dom/scroll-settle.js +84 -0
- package/dist/dom/scroll-settle.js.map +1 -0
- package/dist/dom/scroll.d.ts +21 -0
- package/dist/dom/scroll.d.ts.map +1 -0
- package/dist/dom/scroll.js +141 -0
- package/dist/dom/scroll.js.map +1 -0
- package/dist/dom/share.d.ts +19 -0
- package/dist/dom/share.d.ts.map +1 -0
- package/dist/dom/share.js +41 -0
- package/dist/dom/share.js.map +1 -0
- package/dist/dom/swipe.d.ts +121 -0
- package/dist/dom/swipe.d.ts.map +1 -0
- package/dist/dom/swipe.js +291 -0
- package/dist/dom/swipe.js.map +1 -0
- package/dist/dom/targets.d.ts +45 -0
- package/dist/dom/targets.d.ts.map +1 -0
- package/dist/dom/targets.js +70 -0
- package/dist/dom/targets.js.map +1 -0
- package/dist/dom/track-box.d.ts +61 -0
- package/dist/dom/track-box.d.ts.map +1 -0
- package/dist/dom/track-box.js +131 -0
- package/dist/dom/track-box.js.map +1 -0
- package/dist/events/active-element.d.ts +15 -0
- package/dist/events/active-element.d.ts.map +1 -0
- package/dist/events/active-element.js +33 -0
- package/dist/events/active-element.js.map +1 -0
- package/dist/events/hotkeys.d.ts +22 -0
- package/dist/events/hotkeys.d.ts.map +1 -0
- package/dist/events/hotkeys.js +56 -0
- package/dist/events/hotkeys.js.map +1 -0
- package/dist/events/idle.d.ts +29 -0
- package/dist/events/idle.d.ts.map +1 -0
- package/dist/events/idle.js +51 -0
- package/dist/events/idle.js.map +1 -0
- package/dist/events/index.d.ts +9 -0
- package/dist/events/index.js +10 -0
- package/dist/{input-modality-BY-bSrEc.d.mts → events/input-modality.d.ts} +9 -12
- package/dist/events/input-modality.d.ts.map +1 -0
- package/dist/{input-modality-BXD383jg.mjs → events/input-modality.js} +3 -4
- package/dist/events/input-modality.js.map +1 -0
- package/dist/events/keyboard.d.ts +30 -0
- package/dist/events/keyboard.d.ts.map +1 -0
- package/dist/events/keyboard.js +55 -0
- package/dist/events/keyboard.js.map +1 -0
- package/dist/events/pointer.d.ts +15 -0
- package/dist/events/pointer.d.ts.map +1 -0
- package/dist/events/pointer.js +28 -0
- package/dist/events/pointer.js.map +1 -0
- package/dist/events/scroll.d.ts +16 -0
- package/dist/events/scroll.d.ts.map +1 -0
- package/dist/events/scroll.js +29 -0
- package/dist/events/scroll.js.map +1 -0
- package/dist/events/visibility.d.ts +25 -0
- package/dist/events/visibility.d.ts.map +1 -0
- package/dist/events/visibility.js +28 -0
- package/dist/events/visibility.js.map +1 -0
- package/dist/events/window-size.d.ts +30 -0
- package/dist/events/window-size.d.ts.map +1 -0
- package/dist/events/window-size.js +63 -0
- package/dist/events/window-size.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/interaction/bundle-behavior.d.ts +37 -0
- package/dist/interaction/bundle-behavior.d.ts.map +1 -0
- package/dist/interaction/bundle-behavior.js +34 -0
- package/dist/interaction/bundle-behavior.js.map +1 -0
- package/dist/interaction/dismiss-layer.d.ts +100 -0
- package/dist/interaction/dismiss-layer.d.ts.map +1 -0
- package/dist/interaction/dismiss-layer.js +140 -0
- package/dist/interaction/dismiss-layer.js.map +1 -0
- package/dist/interaction/editable-text.d.ts +13 -0
- package/dist/interaction/editable-text.d.ts.map +1 -0
- package/dist/interaction/editable-text.js +17 -0
- package/dist/interaction/editable-text.js.map +1 -0
- package/dist/interaction/focus-scope.d.ts +21 -0
- package/dist/interaction/focus-scope.d.ts.map +1 -0
- package/dist/interaction/focus-scope.js +25 -0
- package/dist/interaction/focus-scope.js.map +1 -0
- package/dist/interaction/focus-visible.d.ts +9 -0
- package/dist/interaction/focus-visible.d.ts.map +1 -0
- package/dist/interaction/focus-visible.js +10 -0
- package/dist/interaction/focus-visible.js.map +1 -0
- package/dist/interaction/grid-navigation.d.ts +73 -0
- package/dist/interaction/grid-navigation.d.ts.map +1 -0
- package/dist/interaction/grid-navigation.js +137 -0
- package/dist/interaction/grid-navigation.js.map +1 -0
- package/dist/interaction/index.d.ts +21 -0
- package/dist/interaction/index.js +21 -0
- package/dist/interaction/layers.d.ts +12 -0
- package/dist/interaction/layers.d.ts.map +1 -0
- package/dist/interaction/layers.js +14 -0
- package/dist/interaction/layers.js.map +1 -0
- package/dist/interaction/list-navigation.d.ts +105 -0
- package/dist/interaction/list-navigation.d.ts.map +1 -0
- package/dist/interaction/list-navigation.js +168 -0
- package/dist/interaction/list-navigation.js.map +1 -0
- package/dist/interaction/live-announce.d.ts +70 -0
- package/dist/interaction/live-announce.d.ts.map +1 -0
- package/dist/interaction/live-announce.js +115 -0
- package/dist/interaction/live-announce.js.map +1 -0
- package/dist/interaction/long-press.d.ts +102 -0
- package/dist/interaction/long-press.d.ts.map +1 -0
- package/dist/interaction/long-press.js +115 -0
- package/dist/interaction/long-press.js.map +1 -0
- package/dist/interaction/move.d.ts +10 -0
- package/dist/interaction/move.d.ts.map +1 -0
- package/dist/interaction/move.js +17 -0
- package/dist/interaction/move.js.map +1 -0
- package/dist/interaction/overlay.d.ts +45 -0
- package/dist/interaction/overlay.d.ts.map +1 -0
- package/dist/interaction/overlay.js +57 -0
- package/dist/interaction/overlay.js.map +1 -0
- package/dist/interaction/placing.d.ts +86 -0
- package/dist/interaction/placing.d.ts.map +1 -0
- package/dist/interaction/placing.js +239 -0
- package/dist/interaction/placing.js.map +1 -0
- package/dist/interaction/pointer-drag.d.ts +23 -0
- package/dist/interaction/pointer-drag.d.ts.map +1 -0
- package/dist/interaction/pointer-drag.js +21 -0
- package/dist/interaction/pointer-drag.js.map +1 -0
- package/dist/interaction/press.d.ts +152 -0
- package/dist/interaction/press.d.ts.map +1 -0
- package/dist/interaction/press.js +229 -0
- package/dist/interaction/press.js.map +1 -0
- package/dist/interaction/roving-tabindex.d.ts +85 -0
- package/dist/interaction/roving-tabindex.d.ts.map +1 -0
- package/dist/interaction/roving-tabindex.js +142 -0
- package/dist/interaction/roving-tabindex.js.map +1 -0
- package/dist/interaction/selection.d.ts +72 -0
- package/dist/interaction/selection.d.ts.map +1 -0
- package/dist/interaction/selection.js +129 -0
- package/dist/interaction/selection.js.map +1 -0
- package/dist/interaction/targets.d.ts +15 -0
- package/dist/interaction/targets.d.ts.map +1 -0
- package/dist/interaction/targets.js +20 -0
- package/dist/interaction/targets.js.map +1 -0
- package/dist/interaction/tree-navigation.d.ts +111 -0
- package/dist/interaction/tree-navigation.d.ts.map +1 -0
- package/dist/interaction/tree-navigation.js +207 -0
- package/dist/interaction/tree-navigation.js.map +1 -0
- package/dist/interaction/typeahead.d.ts +73 -0
- package/dist/interaction/typeahead.d.ts.map +1 -0
- package/dist/interaction/typeahead.js +113 -0
- package/dist/interaction/typeahead.js.map +1 -0
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/{internal-DP-xLKbT.mjs → internal.js} +8 -7
- package/dist/internal.js.map +1 -0
- package/dist/media/breakpoints.d.ts +37 -0
- package/dist/media/breakpoints.d.ts.map +1 -0
- package/dist/media/breakpoints.js +64 -0
- package/dist/media/breakpoints.js.map +1 -0
- package/dist/media/index.d.ts +3 -0
- package/dist/media/index.js +4 -0
- package/dist/media/mediaquery.d.ts +40 -0
- package/dist/media/mediaquery.d.ts.map +1 -0
- package/dist/media/mediaquery.js +42 -0
- package/dist/media/mediaquery.js.map +1 -0
- package/dist/media/platform.d.ts +30 -0
- package/dist/media/platform.d.ts.map +1 -0
- package/dist/media/platform.js +50 -0
- package/dist/media/platform.js.map +1 -0
- package/dist/motion/index.d.ts +5 -0
- package/dist/motion/index.js +5 -0
- package/dist/motion/motion.d.ts +16 -0
- package/dist/motion/motion.d.ts.map +1 -0
- package/dist/motion/motion.js +25 -0
- package/dist/motion/motion.js.map +1 -0
- package/dist/motion/presence.d.ts +37 -0
- package/dist/motion/presence.d.ts.map +1 -0
- package/dist/motion/presence.js +65 -0
- package/dist/motion/presence.js.map +1 -0
- package/dist/motion/spring.d.ts +50 -0
- package/dist/motion/spring.d.ts.map +1 -0
- package/dist/motion/spring.js +92 -0
- package/dist/motion/spring.js.map +1 -0
- package/dist/motion/tween.d.ts +44 -0
- package/dist/motion/tween.d.ts.map +1 -0
- package/dist/motion/tween.js +65 -0
- package/dist/motion/tween.js.map +1 -0
- package/dist/net/broadcast.d.ts +39 -0
- package/dist/net/broadcast.d.ts.map +1 -0
- package/dist/net/broadcast.js +73 -0
- package/dist/net/broadcast.js.map +1 -0
- package/dist/net/index.d.ts +5 -0
- package/dist/net/index.js +6 -0
- package/dist/net/online.d.ts +24 -0
- package/dist/net/online.d.ts.map +1 -0
- package/dist/net/online.js +26 -0
- package/dist/net/online.js.map +1 -0
- package/dist/net/sharedHost.d.ts +64 -0
- package/dist/net/sharedHost.d.ts.map +1 -0
- package/dist/net/sharedHost.js +124 -0
- package/dist/net/sharedHost.js.map +1 -0
- package/dist/net/sse.d.ts +115 -0
- package/dist/net/sse.d.ts.map +1 -0
- package/dist/net/sse.js +138 -0
- package/dist/net/sse.js.map +1 -0
- package/dist/net/websocket.d.ts +172 -0
- package/dist/net/websocket.d.ts.map +1 -0
- package/dist/net/websocket.js +199 -0
- package/dist/net/websocket.js.map +1 -0
- package/dist/observers/bounds.d.ts +24 -0
- package/dist/observers/bounds.d.ts.map +1 -0
- package/dist/observers/bounds.js +51 -0
- package/dist/observers/bounds.js.map +1 -0
- package/dist/observers/index.d.ts +4 -0
- package/dist/observers/index.js +5 -0
- package/dist/observers/intersection.d.ts +18 -0
- package/dist/observers/intersection.d.ts.map +1 -0
- package/dist/observers/intersection.js +33 -0
- package/dist/observers/intersection.js.map +1 -0
- package/dist/observers/mutation.d.ts +25 -0
- package/dist/observers/mutation.d.ts.map +1 -0
- package/dist/observers/mutation.js +53 -0
- package/dist/observers/mutation.js.map +1 -0
- package/dist/observers/resize.d.ts +18 -0
- package/dist/observers/resize.d.ts.map +1 -0
- package/dist/observers/resize.js +33 -0
- package/dist/observers/resize.js.map +1 -0
- package/dist/state/history.d.ts +115 -0
- package/dist/state/history.d.ts.map +1 -0
- package/dist/state/history.js +167 -0
- package/dist/state/history.js.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.js +7 -0
- package/dist/state/locale.d.ts +27 -0
- package/dist/state/locale.d.ts.map +1 -0
- package/dist/state/locale.js +25 -0
- package/dist/state/locale.js.map +1 -0
- package/dist/state/pagination.d.ts +53 -0
- package/dist/state/pagination.d.ts.map +1 -0
- package/dist/state/pagination.js +72 -0
- package/dist/state/pagination.js.map +1 -0
- package/dist/state/range.d.ts +9 -0
- package/dist/state/range.d.ts.map +1 -0
- package/dist/state/range.js +18 -0
- package/dist/state/range.js.map +1 -0
- package/dist/state/selection.d.ts +47 -0
- package/dist/state/selection.d.ts.map +1 -0
- package/dist/state/selection.js +39 -0
- package/dist/state/selection.js.map +1 -0
- package/dist/state/virtual.d.ts +201 -0
- package/dist/state/virtual.d.ts.map +1 -0
- package/dist/state/{index.mjs → virtual.js} +11 -313
- package/dist/state/virtual.js.map +1 -0
- package/dist/time/debounce.d.ts +43 -0
- package/dist/time/debounce.d.ts.map +1 -0
- package/dist/time/debounce.js +65 -0
- package/dist/time/debounce.js.map +1 -0
- package/dist/time/index.d.ts +6 -0
- package/dist/time/index.js +7 -0
- package/dist/time/interval.d.ts +37 -0
- package/dist/time/interval.d.ts.map +1 -0
- package/dist/time/interval.js +52 -0
- package/dist/time/interval.js.map +1 -0
- package/dist/time/relative.d.ts +8 -0
- package/dist/time/relative.d.ts.map +1 -0
- package/dist/time/relative.js +27 -0
- package/dist/time/relative.js.map +1 -0
- package/dist/time/throttle.d.ts +38 -0
- package/dist/time/throttle.d.ts.map +1 -0
- package/dist/time/throttle.js +39 -0
- package/dist/time/throttle.js.map +1 -0
- package/dist/time/ticks.d.ts +21 -0
- package/dist/time/ticks.d.ts.map +1 -0
- package/dist/time/ticks.js +30 -0
- package/dist/time/ticks.js.map +1 -0
- package/dist/time/timer.d.ts +32 -0
- package/dist/time/timer.d.ts.map +1 -0
- package/dist/time/timer.js +50 -0
- package/dist/time/timer.js.map +1 -0
- package/package.json +31 -31
- package/dist/device/index.d.mts +0 -345
- package/dist/device/index.d.mts.map +0 -1
- package/dist/device/index.mjs +0 -498
- package/dist/device/index.mjs.map +0 -1
- package/dist/dom/index.d.mts +0 -750
- package/dist/dom/index.d.mts.map +0 -1
- package/dist/dom/index.mjs +0 -1857
- package/dist/dom/index.mjs.map +0 -1
- package/dist/editable-text-1D31qthY.d.mts +0 -319
- package/dist/editable-text-1D31qthY.d.mts.map +0 -1
- package/dist/events/index.d.mts +0 -184
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs +0 -315
- package/dist/events/index.mjs.map +0 -1
- package/dist/follow-tab-stop-Caog2qg1.mjs +0 -863
- package/dist/follow-tab-stop-Caog2qg1.mjs.map +0 -1
- package/dist/index-CmmxqSEi.d.mts +0 -149
- package/dist/index-CmmxqSEi.d.mts.map +0 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.mjs +0 -12
- package/dist/input-modality-BXD383jg.mjs.map +0 -1
- package/dist/input-modality-BY-bSrEc.d.mts.map +0 -1
- package/dist/interaction/index.d.mts +0 -1194
- package/dist/interaction/index.d.mts.map +0 -1
- package/dist/interaction/index.mjs +0 -2012
- package/dist/interaction/index.mjs.map +0 -1
- package/dist/internal-DP-xLKbT.mjs.map +0 -1
- package/dist/media/index.d.mts +0 -109
- package/dist/media/index.d.mts.map +0 -1
- package/dist/media/index.mjs +0 -149
- package/dist/media/index.mjs.map +0 -1
- package/dist/motion/index.d.mts +0 -2
- package/dist/motion/index.mjs +0 -1
- package/dist/motion-DmFDppRn.mjs +0 -240
- package/dist/motion-DmFDppRn.mjs.map +0 -1
- package/dist/net/index.d.mts +0 -348
- package/dist/net/index.d.mts.map +0 -1
- package/dist/net/index.mjs +0 -535
- package/dist/net/index.mjs.map +0 -1
- package/dist/observers/index.d.mts +0 -84
- package/dist/observers/index.d.mts.map +0 -1
- package/dist/observers/index.mjs +0 -159
- package/dist/observers/index.mjs.map +0 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
- package/dist/state/index.d.mts +0 -444
- package/dist/state/index.d.mts.map +0 -1
- package/dist/state/index.mjs.map +0 -1
- package/dist/time/index.d.mts +0 -179
- package/dist/time/index.d.mts.map +0 -1
- package/dist/time/index.mjs +0 -242
- package/dist/time/index.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fluid-hover.js","names":[],"sources":["../../src/dom/fluid-hover.ts"],"sourcesContent":["import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** The custom properties `FluidHover` writes on its container for a highlight named `name`. */\nexport const fluidHovered = (name: string) =>\n ({\n x: `--fk-${name}-x`,\n y: `--fk-${name}-y`,\n w: `--fk-${name}-w`,\n h: `--fk-${name}-h`,\n /** `1` while an item is highlighted, `0` once the pointer and focus have left. */\n shown: `--fk-${name}-shown`,\n /**\n * `0` for the frame the highlight appears in, then `1`: a highlight's\n * movement scales its duration by it, so it appears on its item rather\n * than sliding in from where it last was.\n */\n moving: `--fk-${name}-moving`,\n }) as const\n\n/**\n * The attribute `FluidHover` sets on its container, to the highlight's name,\n * while it runs: a stylist draws the highlight, and drops each item's own\n * hover, only under it.\n */\nexport const FLUID_HOVER_ATTRIBUTE = 'data-fluid-hover'\n\n/** The attribute that names, inside an item, the control a press in a gap reaches. */\nexport const FLUID_PRESS_ATTRIBUTE = 'data-fluid-press'\n\n/** The attribute on the item the highlight is shown on, for what CSS draws beside it. */\nexport const FLUID_LIT_ATTRIBUTE = 'data-fluid-lit'\n\n/**\n * The attribute `FluidHover` sets on its container, to the highlight's name,\n * while the item that highlight is on is held. A stylist draws the press on\n * the highlight (`data-fluid-held`) rather than on the item, whose own press\n * is hidden under it; a press in a gap marks it too where `gapClick` sends\n * that press to the lit item.\n */\nexport const FLUID_HELD_ATTRIBUTE = 'data-fluid-held'\n\nconst Axis = Schema.Literals(['x', 'y', 'xy'])\nexport type FluidAxis = typeof Axis.Type\n\n/** How far a point is from a rectangle along each axis: zero inside it. */\nconst gap = (low: number, high: number, at: number) => Math.max(low - at, 0, at - high)\n\n// On one axis the other only breaks ties, so a list's highlight never leaves\n// the row level with the pointer for a nearer edge of the next column.\nconst away: Record<FluidAxis, (dx: number, dy: number) => number> = {\n x: (dx, dy) => dx * 1e6 + dy,\n y: (dx, dy) => dy * 1e6 + dx,\n xy: Math.hypot,\n}\n\nconst distance = (rect: DOMRect, x: number, y: number, axis: FluidAxis) =>\n away[axis](gap(rect.left, rect.right, x), gap(rect.top, rect.bottom, y))\n\nconst isDisabled = (item: Element) =>\n item.matches('[aria-disabled=\"true\"], [data-disabled]:not([data-disabled=\"false\"]), :disabled')\n\n/**\n * One highlight that follows the pointer across a container's items, as a\n * Mount, with no Message and no Model: which item the pointer is nearest is\n * presentation, known to the DOM and to no transition.\n *\n * The item nearest the pointer is the one highlighted, so the pointer in a gap\n * between two items still points at one, and the highlight moves from item to\n * item instead of blinking off between them. On the `y` axis nearness is read\n * vertically (a list), on `x` horizontally (a strip), on `xy` both (a grid). A\n * disabled item (`aria-disabled=\"true\"`, `data-disabled`, `:disabled`) is never\n * highlighted. An item reached by the keyboard (`:focus-visible`) is\n * highlighted too, and with `focused`, the item that selector finds from a\n * focused element that is no item (a tree's row, from its focused\n * `treeitem`, which also holds the rows beneath it); a touch highlights\n * nothing, since there is no pointer to\n * follow before the press.\n *\n * The container gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, relative\n * to its scroll box like `Measure`'s, and `-shown` and `-moving` (see\n * `fluidHovered`); a view draws the highlight as an absolutely placed child\n * that reads them. With `gapClick`, a click in a gap presses the highlighted\n * item: its descendant marked `data-fluid-press`, or the item itself; with\n * `gapReach` too, only a click within that many pixels of it, as a grid of\n * cards with wide gutters wants (Fluid Functionalism's uses 16). A popup\n * measured before it has a layout box keeps its last complete place, and\n * tries again for three frames, rather than drawing the highlight at zero.\n * The item the highlight is shown on carries `data-fluid-lit`, so a style\n * can change what sits beside it (a table hides the lines around the lit\n * row); its container carries `data-fluid-held` while that item is pressed.\n *\n * `follows` names the item the highlight sits on while the pointer is not\n * moving it: a menu marks the row its keys reach (`[data-active]`), and the\n * highlight goes there whenever that mark moves, so the keys and the pointer\n * light the same one. `from` names the item a fresh highlight grows out of,\n * as a tab's hover leaves the chosen tab. A highlight always fades where it\n * is: once the pointer leaves, nothing should move. It stays only for an item\n * the keyboard reached (`:focus-visible`), not one a click focused.\n */\nexport const FluidHover = Mount.defineStream('FluidHover', {\n messages: [Schema.Never],\n args: {\n /** The highlight's name, which names the custom properties. */\n name: Schema.String,\n /** The selector of an item within the container. */\n item: Schema.String,\n axis: Axis,\n gapClick: Schema.Boolean,\n /** How far from the highlighted item, in pixels, a click in a gap still presses it. */\n gapReach: Schema.optional(Schema.Number),\n /** The selector of the item the highlight sits on while the pointer is not moving it. */\n follows: Schema.optional(Schema.String),\n /** The selector of the item a fresh highlight grows out of. */\n from: Schema.optional(Schema.String),\n /** The selector, run from a focused element outside any item, of the item it lights. */\n focused: Schema.optional(Schema.String),\n },\n execute: ({ element, name, item: selector, axis, gapClick, gapReach, follows, from, focused }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const container = element as HTMLElement\n const property = fluidHovered(name)\n let active: Element | null = null\n let followed: Element | null = null\n let pointer: { readonly x: number; readonly y: number } | null = null\n let frame: number | undefined\n\n const origin = () => (from === undefined ? null : container.querySelector(from))\n let retries = 0\n const place = (at: Element | null = active) => {\n if (at === null) return\n const box = container.getBoundingClientRect()\n const rect = at.getBoundingClientRect()\n // No layout box yet (a popup still opening): keep the last place and look again.\n if (rect.width === 0 && rect.height === 0) {\n if (retries < 3) {\n retries += 1\n requestAnimationFrame(() => place(at))\n }\n return\n }\n retries = 0\n const x = rect.left - box.left - container.clientLeft + container.scrollLeft\n const y = rect.top - box.top - container.clientTop + container.scrollTop\n container.style.setProperty(property.x, `${x}px`)\n container.style.setProperty(property.y, `${y}px`)\n container.style.setProperty(property.w, `${rect.width}px`)\n container.style.setProperty(property.h, `${rect.height}px`)\n }\n /** Marks `item` as the lit one, and nothing else. */\n const light = (item: Element | null) => {\n container\n .querySelectorAll(`[${FLUID_LIT_ATTRIBUTE}]`)\n .forEach(each => each !== item && each.removeAttribute(FLUID_LIT_ATTRIBUTE))\n item?.setAttribute(FLUID_LIT_ATTRIBUTE, '')\n }\n const show = (next: Element) => {\n const appearing = container.style.getPropertyValue(property.shown) !== '1'\n active = next\n light(next)\n const grows = appearing ? origin() : null\n if (appearing) container.style.setProperty(property.moving, '0')\n if (grows === null) {\n place()\n container.style.setProperty(property.shown, '1')\n } else {\n // Drawn first where it grows from, then moved: it travels out of that item.\n place(grows)\n requestAnimationFrame(() => {\n if (active !== next) return\n place()\n container.style.setProperty(property.shown, '1')\n })\n }\n if (appearing) {\n // The frame after: by then the jump to the item has been drawn without moving.\n requestAnimationFrame(() => container.style.setProperty(property.moving, '1'))\n }\n }\n const hide = () => {\n container.style.setProperty(property.shown, '0')\n container.removeAttribute(FLUID_HELD_ATTRIBUTE)\n light(null)\n }\n /** Whether the keyboard has an item focused, which the highlight then stays on. */\n const keyed = () => {\n const focused = document.activeElement\n return (\n focused !== null && container.contains(focused) && focused.matches(':focus-visible')\n )\n }\n const nearest = (x: number, y: number) => {\n let best: Element | null = null\n let bestDistance = Infinity\n for (const each of Array.from(container.querySelectorAll(selector))) {\n if (isDisabled(each)) continue\n const far = distance(each.getBoundingClientRect(), x, y, axis)\n if (far < bestDistance) {\n best = each\n bestDistance = far\n }\n }\n return best\n }\n const follow = () => {\n frame = undefined\n if (pointer === null) return\n const next = nearest(pointer.x, pointer.y)\n if (next === null) hide()\n else if (next !== active || container.style.getPropertyValue(property.shown) !== '1')\n show(next)\n }\n\n const move = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return\n pointer = { x: event.clientX, y: event.clientY }\n if (frame === undefined) frame = requestAnimationFrame(follow)\n }\n const leave = () => {\n pointer = null\n release()\n if (!keyed()) hide()\n }\n const focus = (event: FocusEvent) => {\n if (!(event.target instanceof Element) || !event.target.matches(':focus-visible')) {\n return\n }\n const owner =\n event.target.closest(selector) ??\n (focused === undefined ? null : event.target.querySelector(focused))\n if (owner !== null && container.contains(owner) && !isDisabled(owner)) show(owner)\n }\n const blur = (event: FocusEvent) => {\n const to = event.relatedTarget\n if (pointer === null && !(to instanceof Node && container.contains(to))) hide()\n }\n const reachOf = (item: Element, event: MouseEvent) =>\n gapReach === undefined ||\n distance(item.getBoundingClientRect(), event.clientX, event.clientY, 'xy') <= gapReach\n /** Whether a click in a gap presses the lit item, within `gapReach` of it. */\n const reaches = (item: Element, event: MouseEvent) => gapClick && reachOf(item, event)\n const click = (event: MouseEvent) => {\n if (active === null) return\n if (container.style.getPropertyValue(property.shown) !== '1') return\n const target = event.target\n if (!(target instanceof Element) || target.closest(selector) !== null) return\n if (!reaches(active, event)) return\n const press = active.querySelector(`[${FLUID_PRESS_ATTRIBUTE}]`) ?? active\n if (press instanceof HTMLElement) press.click()\n }\n const soon = () => {\n if (active !== null) requestAnimationFrame(() => place())\n }\n /**\n * Marks the container as held where the press is the lit item's: the\n * item itself, or a gap a click there reaches through (`gapClick`). A\n * press on another item is that item's, once the pointer has lit it.\n */\n const press = (event: PointerEvent) => {\n if (event.button !== 0 || active === null) return\n if (container.style.getPropertyValue(property.shown) !== '1') return\n const target = event.target\n if (!(target instanceof Element)) return\n const inItem = target.closest(selector) !== null\n if (inItem && !active.contains(target)) return\n if (!inItem && !reaches(active, event)) return\n container.setAttribute(FLUID_HELD_ATTRIBUTE, name)\n }\n const release = () => container.removeAttribute(FLUID_HELD_ATTRIBUTE)\n /** The marked item moved: the highlight goes with it, or fades if it is gone. */\n const marked = () => {\n if (follows === undefined) return\n const next = container.querySelector(follows)\n if (next === followed) return\n followed = next\n if (next !== null && !isDisabled(next)) show(next)\n else if (pointer === null && !keyed()) hide()\n }\n\n // Where there is no layout (jsdom has no ResizeObserver) nothing moves to\n // follow, and a view drawn there is no worse for the highlight's absence.\n const sizes = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(soon)\n const marks = follows === undefined ? undefined : new MutationObserver(marked)\n sizes?.observe(container)\n marks?.observe(container, { subtree: true, childList: true, attributes: true })\n container.addEventListener('pointermove', move, { passive: true })\n container.addEventListener('pointerdown', press)\n container.addEventListener('pointerleave', leave)\n // Released outside the container, or cancelled by the browser (a\n // scroll taking the gesture): the press ended either way.\n window.addEventListener('pointerup', release)\n window.addEventListener('pointercancel', release)\n container.addEventListener('focusin', focus)\n container.addEventListener('focusout', blur)\n container.addEventListener('click', click)\n container.addEventListener('scroll', soon, { passive: true })\n window.addEventListener('resize', soon)\n container.style.setProperty(property.shown, '0')\n container.setAttribute(FLUID_HOVER_ATTRIBUTE, name)\n marked()\n return () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n sizes?.disconnect()\n marks?.disconnect()\n container.removeAttribute(FLUID_HOVER_ATTRIBUTE)\n container.removeAttribute(FLUID_HELD_ATTRIBUTE)\n light(null)\n container.removeEventListener('pointermove', move)\n container.removeEventListener('pointerdown', press)\n container.removeEventListener('pointerleave', leave)\n window.removeEventListener('pointerup', release)\n window.removeEventListener('pointercancel', release)\n container.removeEventListener('focusin', focus)\n container.removeEventListener('focusout', blur)\n container.removeEventListener('click', click)\n container.removeEventListener('scroll', soon)\n window.removeEventListener('resize', soon)\n }\n }),\n stop => Effect.sync(stop),\n ),\n ),\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
/** Tabbable descendants in DOM order: focusable, visible, and not `tabindex="-1"`. */
|
|
4
|
+
export declare const tabbableWithin: (root: Element) => ReadonlyArray<HTMLElement>;
|
|
5
|
+
/**
|
|
6
|
+
* A focus scope as a Mount: on insert it remembers what was focused, focuses
|
|
7
|
+
* an initial element inside, and with `contain` keeps Tab and Shift+Tab
|
|
8
|
+
* cycling inside; on unmount it gives focus back. Nothing crosses to the
|
|
9
|
+
* Model: which element had focus is a DOM fact, so this is element lifecycle,
|
|
10
|
+
* not state. It emits no Messages.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FocusScope: Mount.MountDefinitionWithArgs<"FocusScope", {
|
|
13
|
+
/** Keep Tab, Shift+Tab, and a stray focus inside. */
|
|
14
|
+
contain: Schema.Boolean;
|
|
15
|
+
/** Give focus back to what had it, on unmount. */
|
|
16
|
+
restore: Schema.Boolean;
|
|
17
|
+
/** A selector inside the element to focus first; else the first tabbable
|
|
18
|
+
* descendant, else the element itself when it is focusable. */
|
|
19
|
+
initialFocus: Schema.NullOr<Schema.String>;
|
|
20
|
+
}, never>;
|
|
21
|
+
//# sourceMappingURL=focus-scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-scope.d.ts","names":[],"sources":["../../src/dom/focus-scope.ts"],"mappings":";;;qBAmBa,iBAAc,MAAU,YAAU,cAAc;;;;;;;;qBAmBhD,YAAU,MAAA"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/focus-scope.ts
|
|
4
|
+
const FOCUSABLE = [
|
|
5
|
+
"a[href]",
|
|
6
|
+
"button:not([disabled])",
|
|
7
|
+
"input:not([disabled]):not([type=\"hidden\"])",
|
|
8
|
+
"select:not([disabled])",
|
|
9
|
+
"textarea:not([disabled])",
|
|
10
|
+
"[tabindex]",
|
|
11
|
+
"[contenteditable=\"true\"]"
|
|
12
|
+
].join(", ");
|
|
13
|
+
const isVisible = (element) => {
|
|
14
|
+
const probe = element;
|
|
15
|
+
return element.closest("[hidden], [inert]") === null && (probe.checkVisibility?.() ?? true);
|
|
16
|
+
};
|
|
17
|
+
/** Tabbable descendants in DOM order: focusable, visible, and not `tabindex="-1"`. */
|
|
18
|
+
const tabbableWithin = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter((element) => element.tabIndex >= 0 && isVisible(element));
|
|
19
|
+
const focusFirst = (elements) => {
|
|
20
|
+
const first = elements[0];
|
|
21
|
+
if (first === void 0) return false;
|
|
22
|
+
first.focus();
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A focus scope as a Mount: on insert it remembers what was focused, focuses
|
|
27
|
+
* an initial element inside, and with `contain` keeps Tab and Shift+Tab
|
|
28
|
+
* cycling inside; on unmount it gives focus back. Nothing crosses to the
|
|
29
|
+
* Model: which element had focus is a DOM fact, so this is element lifecycle,
|
|
30
|
+
* not state. It emits no Messages.
|
|
31
|
+
*/
|
|
32
|
+
const FocusScope = Mount.defineStream("FocusScope", {
|
|
33
|
+
messages: [Schema.Never],
|
|
34
|
+
args: {
|
|
35
|
+
/** Keep Tab, Shift+Tab, and a stray focus inside. */
|
|
36
|
+
contain: Schema.Boolean,
|
|
37
|
+
/** Give focus back to what had it, on unmount. */
|
|
38
|
+
restore: Schema.Boolean,
|
|
39
|
+
/** A selector inside the element to focus first; else the first tabbable
|
|
40
|
+
* descendant, else the element itself when it is focusable. */
|
|
41
|
+
initialFocus: Schema.NullOr(Schema.String)
|
|
42
|
+
},
|
|
43
|
+
execute: ({ element, contain, restore, initialFocus }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
44
|
+
const ownerDocument = element.ownerDocument;
|
|
45
|
+
const previous = ownerDocument.activeElement;
|
|
46
|
+
const initial = initialFocus === null ? null : element.querySelector(initialFocus);
|
|
47
|
+
if (initial !== null) initial.focus();
|
|
48
|
+
else if (!focusFirst(tabbableWithin(element)) && element instanceof HTMLElement) element.focus();
|
|
49
|
+
const onKeyDown = (event) => {
|
|
50
|
+
const key = event;
|
|
51
|
+
if (key.key !== "Tab") return;
|
|
52
|
+
const tabbable = tabbableWithin(element);
|
|
53
|
+
const first = tabbable[0];
|
|
54
|
+
const last = tabbable[tabbable.length - 1];
|
|
55
|
+
if (first === void 0 || last === void 0) {
|
|
56
|
+
key.preventDefault();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const active = ownerDocument.activeElement;
|
|
60
|
+
if (key.shiftKey && (active === first || !element.contains(active))) {
|
|
61
|
+
key.preventDefault();
|
|
62
|
+
last.focus();
|
|
63
|
+
} else if (!key.shiftKey && (active === last || !element.contains(active))) {
|
|
64
|
+
key.preventDefault();
|
|
65
|
+
first.focus();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const onFocusIn = (event) => {
|
|
69
|
+
const target = event.target;
|
|
70
|
+
if (target instanceof Node && element.contains(target)) return;
|
|
71
|
+
focusFirst(tabbableWithin(element));
|
|
72
|
+
};
|
|
73
|
+
if (contain) {
|
|
74
|
+
element.addEventListener("keydown", onKeyDown);
|
|
75
|
+
ownerDocument.addEventListener("focusin", onFocusIn);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
previous,
|
|
79
|
+
onKeyDown,
|
|
80
|
+
onFocusIn,
|
|
81
|
+
ownerDocument
|
|
82
|
+
};
|
|
83
|
+
}), ({ previous, onKeyDown, onFocusIn, ownerDocument }) => Effect.sync(() => {
|
|
84
|
+
if (contain) {
|
|
85
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
86
|
+
ownerDocument.removeEventListener("focusin", onFocusIn);
|
|
87
|
+
}
|
|
88
|
+
if (restore && previous instanceof HTMLElement && ownerDocument.contains(previous)) previous.focus();
|
|
89
|
+
})))
|
|
90
|
+
});
|
|
91
|
+
export { FocusScope, tabbableWithin };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=focus-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-scope.js","names":[],"sources":["../../src/dom/focus-scope.ts"],"sourcesContent":["import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nconst FOCUSABLE = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]',\n '[contenteditable=\"true\"]',\n].join(', ')\n\nconst isVisible = (element: Element): boolean => {\n const probe = element as { checkVisibility?: () => boolean }\n return element.closest('[hidden], [inert]') === null && (probe.checkVisibility?.() ?? true)\n}\n\n/** Tabbable descendants in DOM order: focusable, visible, and not `tabindex=\"-1\"`. */\nexport const tabbableWithin = (root: Element): ReadonlyArray<HTMLElement> =>\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n element => element.tabIndex >= 0 && isVisible(element),\n )\n\nconst focusFirst = (elements: ReadonlyArray<HTMLElement>): boolean => {\n const first = elements[0]\n if (first === undefined) return false\n first.focus()\n return true\n}\n\n/**\n * A focus scope as a Mount: on insert it remembers what was focused, focuses\n * an initial element inside, and with `contain` keeps Tab and Shift+Tab\n * cycling inside; on unmount it gives focus back. Nothing crosses to the\n * Model: which element had focus is a DOM fact, so this is element lifecycle,\n * not state. It emits no Messages.\n */\nexport const FocusScope = Mount.defineStream('FocusScope', {\n messages: [Schema.Never],\n args: {\n /** Keep Tab, Shift+Tab, and a stray focus inside. */\n contain: Schema.Boolean,\n /** Give focus back to what had it, on unmount. */\n restore: Schema.Boolean,\n /** A selector inside the element to focus first; else the first tabbable\n * descendant, else the element itself when it is focusable. */\n initialFocus: Schema.NullOr(Schema.String),\n },\n execute: ({ element, contain, restore, initialFocus }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const ownerDocument = element.ownerDocument\n const previous = ownerDocument.activeElement\n const initial =\n initialFocus === null ? null : element.querySelector<HTMLElement>(initialFocus)\n if (initial !== null) initial.focus()\n else if (!focusFirst(tabbableWithin(element)) && element instanceof HTMLElement) {\n element.focus()\n }\n const onKeyDown = (event: Event) => {\n const key = event as KeyboardEvent\n if (key.key !== 'Tab') return\n const tabbable = tabbableWithin(element)\n const first = tabbable[0]\n const last = tabbable[tabbable.length - 1]\n if (first === undefined || last === undefined) {\n key.preventDefault()\n return\n }\n const active = ownerDocument.activeElement\n if (key.shiftKey && (active === first || !element.contains(active))) {\n key.preventDefault()\n last.focus()\n } else if (!key.shiftKey && (active === last || !element.contains(active))) {\n key.preventDefault()\n first.focus()\n }\n }\n const onFocusIn = (event: Event) => {\n const target = event.target\n if (target instanceof Node && element.contains(target)) return\n focusFirst(tabbableWithin(element))\n }\n if (contain) {\n element.addEventListener('keydown', onKeyDown)\n ownerDocument.addEventListener('focusin', onFocusIn)\n }\n return { previous, onKeyDown, onFocusIn, ownerDocument }\n }),\n ({ previous, onKeyDown, onFocusIn, ownerDocument }) =>\n Effect.sync(() => {\n if (contain) {\n element.removeEventListener('keydown', onKeyDown)\n ownerDocument.removeEventListener('focusin', onFocusIn)\n }\n if (restore && previous instanceof HTMLElement && ownerDocument.contains(previous)) {\n previous.focus()\n }\n }),\n ),\n ),\n})\n"],"mappings":";;;AAGA,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAEX,MAAM,aAAa,YAA8B;CAC/C,MAAM,QAAQ;CACd,OAAO,QAAQ,QAAQ,mBAAmB,MAAM,SAAS,MAAM,kBAAkB,KAAK;AACxF;;AAGA,MAAa,kBAAkB,SAC7B,MAAM,KAAK,KAAK,iBAA8B,SAAS,CAAC,CAAC,CAAC,QACxD,YAAW,QAAQ,YAAY,KAAK,UAAU,OAAO,CACvD;AAEF,MAAM,cAAc,aAAkD;CACpE,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,MAAM;CACZ,OAAO;AACT;;;;;;;;AASA,MAAa,aAAa,MAAM,aAAa,cAAc;CACzD,UAAU,CAAC,OAAO,KAAK;CACvB,MAAM;;EAEJ,SAAS,OAAO;;EAEhB,SAAS,OAAO;;;EAGhB,cAAc,OAAO,OAAO,OAAO,MAAM;CAC3C;CACA,UAAU,EAAE,SAAS,SAAS,SAAS,mBACrC,OAAO,eACL,OAAO,eACL,OAAO,WAAW;EAChB,MAAM,gBAAgB,QAAQ;EAC9B,MAAM,WAAW,cAAc;EAC/B,MAAM,UACJ,iBAAiB,OAAO,OAAO,QAAQ,cAA2B,YAAY;EAChF,IAAI,YAAY,MAAM,QAAQ,MAAM;OAC/B,IAAI,CAAC,WAAW,eAAe,OAAO,CAAC,KAAK,mBAAmB,aAClE,QAAQ,MAAM;EAEhB,MAAM,aAAa,UAAiB;GAClC,MAAM,MAAM;GACZ,IAAI,IAAI,QAAQ,OAAO;GACvB,MAAM,WAAW,eAAe,OAAO;GACvC,MAAM,QAAQ,SAAS;GACvB,MAAM,OAAO,SAAS,SAAS,SAAS;GACxC,IAAI,UAAU,KAAA,KAAa,SAAS,KAAA,GAAW;IAC7C,IAAI,eAAe;IACnB;GACF;GACA,MAAM,SAAS,cAAc;GAC7B,IAAI,IAAI,aAAa,WAAW,SAAS,CAAC,QAAQ,SAAS,MAAM,IAAI;IACnE,IAAI,eAAe;IACnB,KAAK,MAAM;GACb,OAAO,IAAI,CAAC,IAAI,aAAa,WAAW,QAAQ,CAAC,QAAQ,SAAS,MAAM,IAAI;IAC1E,IAAI,eAAe;IACnB,MAAM,MAAM;GACd;EACF;EACA,MAAM,aAAa,UAAiB;GAClC,MAAM,SAAS,MAAM;GACrB,IAAI,kBAAkB,QAAQ,QAAQ,SAAS,MAAM,GAAG;GACxD,WAAW,eAAe,OAAO,CAAC;EACpC;EACA,IAAI,SAAS;GACX,QAAQ,iBAAiB,WAAW,SAAS;GAC7C,cAAc,iBAAiB,WAAW,SAAS;EACrD;EACA,OAAO;GAAE;GAAU;GAAW;GAAW;EAAc;CACzD,CAAC,IACA,EAAE,UAAU,WAAW,WAAW,oBACjC,OAAO,WAAW;EAChB,IAAI,SAAS;GACX,QAAQ,oBAAoB,WAAW,SAAS;GAChD,cAAc,oBAAoB,WAAW,SAAS;EACxD;EACA,IAAI,WAAW,oBAAoB,eAAe,cAAc,SAAS,QAAQ,GAC/E,SAAS,MAAM;CAEnB,CAAC,CACL,CACF;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Mount from "foldkit/mount";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps focus that is inside a container of roving tab stops on its stop, as a
|
|
4
|
+
* Mount: when a transition moves the stop (a row made current by a command the
|
|
5
|
+
* container did not see, such as a duplicate) or removes the focused
|
|
6
|
+
* descendant (a deleted row), focus goes to the descendant with
|
|
7
|
+
* `tabindex="0"`. It sends no Message. Focus outside the container is never
|
|
8
|
+
* taken, and focus the user moved away stays where it went.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FollowTabStop: Mount.MountDefinitionNoArgs<"FollowTabStop", never>;
|
|
11
|
+
//# sourceMappingURL=follow-tab-stop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"follow-tab-stop.d.ts","names":[],"sources":["../../src/dom/follow-tab-stop.ts"],"mappings":";;;;;;;;;qBAWa,eAAa,MAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/follow-tab-stop.ts
|
|
4
|
+
/**
|
|
5
|
+
* Keeps focus that is inside a container of roving tab stops on its stop, as a
|
|
6
|
+
* Mount: when a transition moves the stop (a row made current by a command the
|
|
7
|
+
* container did not see, such as a duplicate) or removes the focused
|
|
8
|
+
* descendant (a deleted row), focus goes to the descendant with
|
|
9
|
+
* `tabindex="0"`. It sends no Message. Focus outside the container is never
|
|
10
|
+
* taken, and focus the user moved away stays where it went.
|
|
11
|
+
*/
|
|
12
|
+
const FollowTabStop = Mount.defineStream("FollowTabStop", {
|
|
13
|
+
messages: [Schema.Never],
|
|
14
|
+
execute: ({ element }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
15
|
+
let within = element.contains(document.activeElement);
|
|
16
|
+
const entered = () => {
|
|
17
|
+
within = true;
|
|
18
|
+
};
|
|
19
|
+
const left = (event) => {
|
|
20
|
+
const next = event.relatedTarget;
|
|
21
|
+
if (next instanceof Node && element.contains(next)) return;
|
|
22
|
+
const from = event.target;
|
|
23
|
+
queueMicrotask(() => {
|
|
24
|
+
if (!(from instanceof Node) || from.isConnected) within = false;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const follow = () => {
|
|
28
|
+
if (!within) return;
|
|
29
|
+
const stop = element.querySelector("[tabindex=\"0\"]");
|
|
30
|
+
const focused = document.activeElement;
|
|
31
|
+
if (!(stop instanceof HTMLElement) || stop === focused) return;
|
|
32
|
+
const lost = focused === null || focused === document.body;
|
|
33
|
+
const passed = focused !== null && element.contains(focused) && focused.getAttribute("tabindex") === "-1";
|
|
34
|
+
if (lost || passed) stop.focus();
|
|
35
|
+
};
|
|
36
|
+
const changes = new MutationObserver(follow);
|
|
37
|
+
element.addEventListener("focusin", entered);
|
|
38
|
+
element.addEventListener("focusout", left);
|
|
39
|
+
changes.observe(element, {
|
|
40
|
+
subtree: true,
|
|
41
|
+
childList: true,
|
|
42
|
+
attributes: true,
|
|
43
|
+
attributeFilter: ["tabindex"]
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
changes.disconnect();
|
|
47
|
+
element.removeEventListener("focusin", entered);
|
|
48
|
+
element.removeEventListener("focusout", left);
|
|
49
|
+
};
|
|
50
|
+
}), (stop) => Effect.sync(stop)))
|
|
51
|
+
});
|
|
52
|
+
export { FollowTabStop };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=follow-tab-stop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"follow-tab-stop.js","names":[],"sources":["../../src/dom/follow-tab-stop.ts"],"sourcesContent":["import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/**\n * Keeps focus that is inside a container of roving tab stops on its stop, as a\n * Mount: when a transition moves the stop (a row made current by a command the\n * container did not see, such as a duplicate) or removes the focused\n * descendant (a deleted row), focus goes to the descendant with\n * `tabindex=\"0\"`. It sends no Message. Focus outside the container is never\n * taken, and focus the user moved away stays where it went.\n */\nexport const FollowTabStop = Mount.defineStream('FollowTabStop', {\n messages: [Schema.Never],\n execute: ({ element }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let within = element.contains(document.activeElement)\n const entered = () => {\n within = true\n }\n const left = (event: Event) => {\n const next = (event as FocusEvent).relatedTarget\n if (next instanceof Node && element.contains(next)) return\n const from = event.target\n // Asked once the change that may have taken it away is done: a descendant\n // removed with focus in it keeps the claim, one the user left does not.\n queueMicrotask(() => {\n if (!(from instanceof Node) || from.isConnected) within = false\n })\n }\n const follow = () => {\n if (!within) return\n const stop = element.querySelector('[tabindex=\"0\"]')\n const focused = document.activeElement\n if (!(stop instanceof HTMLElement) || stop === focused) return\n const lost = focused === null || focused === document.body\n const passed =\n focused !== null &&\n element.contains(focused) &&\n focused.getAttribute('tabindex') === '-1'\n if (lost || passed) stop.focus()\n }\n const changes = new MutationObserver(follow)\n element.addEventListener('focusin', entered)\n element.addEventListener('focusout', left)\n // A stop moved, or a descendant drawn or removed.\n changes.observe(element, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['tabindex'],\n })\n return () => {\n changes.disconnect()\n element.removeEventListener('focusin', entered)\n element.removeEventListener('focusout', left)\n }\n }),\n stop => Effect.sync(stop),\n ),\n ),\n})\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,gBAAgB,MAAM,aAAa,iBAAiB;CAC/D,UAAU,CAAC,OAAO,KAAK;CACvB,UAAU,EAAE,cACV,OAAO,eACL,OAAO,eACL,OAAO,WAAW;EAChB,IAAI,SAAS,QAAQ,SAAS,SAAS,aAAa;EACpD,MAAM,gBAAgB;GACpB,SAAS;EACX;EACA,MAAM,QAAQ,UAAiB;GAC7B,MAAM,OAAQ,MAAqB;GACnC,IAAI,gBAAgB,QAAQ,QAAQ,SAAS,IAAI,GAAG;GACpD,MAAM,OAAO,MAAM;GAGnB,qBAAqB;IACnB,IAAI,EAAE,gBAAgB,SAAS,KAAK,aAAa,SAAS;GAC5D,CAAC;EACH;EACA,MAAM,eAAe;GACnB,IAAI,CAAC,QAAQ;GACb,MAAM,OAAO,QAAQ,cAAc,kBAAgB;GACnD,MAAM,UAAU,SAAS;GACzB,IAAI,EAAE,gBAAgB,gBAAgB,SAAS,SAAS;GACxD,MAAM,OAAO,YAAY,QAAQ,YAAY,SAAS;GACtD,MAAM,SACJ,YAAY,QACZ,QAAQ,SAAS,OAAO,KACxB,QAAQ,aAAa,UAAU,MAAM;GACvC,IAAI,QAAQ,QAAQ,KAAK,MAAM;EACjC;EACA,MAAM,UAAU,IAAI,iBAAiB,MAAM;EAC3C,QAAQ,iBAAiB,WAAW,OAAO;EAC3C,QAAQ,iBAAiB,YAAY,IAAI;EAEzC,QAAQ,QAAQ,SAAS;GACvB,SAAS;GACT,WAAW;GACX,YAAY;GACZ,iBAAiB,CAAC,UAAU;EAC9B,CAAC;EACD,aAAa;GACX,QAAQ,WAAW;GACnB,QAAQ,oBAAoB,WAAW,OAAO;GAC9C,QAAQ,oBAAoB,YAAY,IAAI;EAC9C;CACF,CAAC,IACD,SAAQ,OAAO,KAAK,IAAI,CAC1B,CACF;AACJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Command } from "foldkit/command";
|
|
3
|
+
export declare const ImageDecodeMessage: import("foldkit/message").MessageUnion<{
|
|
4
|
+
readonly Decoded: {
|
|
5
|
+
src: Schema.String;
|
|
6
|
+
};
|
|
7
|
+
readonly DecodeFailed: {
|
|
8
|
+
src: Schema.String;
|
|
9
|
+
message: Schema.String;
|
|
10
|
+
};
|
|
11
|
+
}>;
|
|
12
|
+
export type ImageDecodeMessage = typeof ImageDecodeMessage.Type;
|
|
13
|
+
/** What to decode: a URL, or an `img`'s candidates, so the browser decodes the one it would draw. */
|
|
14
|
+
export interface ImageSource {
|
|
15
|
+
readonly src: string;
|
|
16
|
+
readonly srcset?: string | undefined;
|
|
17
|
+
readonly sizes?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Fetches and decodes `source` (`img.decode()`), yielding `Decoded` once it
|
|
21
|
+
* can be painted at once, or `DecodeFailed` when it cannot be fetched or read,
|
|
22
|
+
* or there is no document (a server render). Given `srcset` and `sizes`, as
|
|
23
|
+
* `foldkit-media`'s `Picture.Img` has them, the browser picks the candidate
|
|
24
|
+
* it would draw, so the decoded file is the one shown.
|
|
25
|
+
*/
|
|
26
|
+
export declare const decodeImage: (source: ImageSource) => Command<ImageDecodeMessage, never, never>;
|
|
27
|
+
//# sourceMappingURL=image-decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-decode.d.ts","names":[],"sources":["../../src/dom/image-decode.ts"],"mappings":";;qBAWa,8CAAkB;;IAClB,KAAG,OAAA;;;IACE,KAAG,OAAA;IAAiB,SAAO,OAAA;;;YAEjC,4BAA4B,mBAAmB;;iBAG1C;WACN;WACA;WACA;;;;;;;;;qBAUE,cAAW,QAAY,gBAAc,QAAQ"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
// src/dom/image-decode.ts
|
|
4
|
+
/**
|
|
5
|
+
* An image fetched and decoded before it is shown, as a Command: a lightbox
|
|
6
|
+
* or a carousel asks for the next picture, and shows it on `Decoded`, so the
|
|
7
|
+
* switch never passes through a blank frame. A Command, not a Mount: the
|
|
8
|
+
* Model decides to show the image, so the Message that leads there carries
|
|
9
|
+
* the news that it is ready.
|
|
10
|
+
*/
|
|
11
|
+
const ImageDecodeMessage = defineMessageUnion({
|
|
12
|
+
Decoded: { src: Schema.String },
|
|
13
|
+
DecodeFailed: {
|
|
14
|
+
src: Schema.String,
|
|
15
|
+
message: Schema.String
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Fetches and decodes `source` (`img.decode()`), yielding `Decoded` once it
|
|
20
|
+
* can be painted at once, or `DecodeFailed` when it cannot be fetched or read,
|
|
21
|
+
* or there is no document (a server render). Given `srcset` and `sizes`, as
|
|
22
|
+
* `foldkit-media`'s `Picture.Img` has them, the browser picks the candidate
|
|
23
|
+
* it would draw, so the decoded file is the one shown.
|
|
24
|
+
*/
|
|
25
|
+
const decodeImage = (source) => ({
|
|
26
|
+
name: "Image.decode",
|
|
27
|
+
args: { src: source.src },
|
|
28
|
+
effect: typeof document === "undefined" ? Effect.succeed(ImageDecodeMessage.DecodeFailed({
|
|
29
|
+
src: source.src,
|
|
30
|
+
message: "there is no document"
|
|
31
|
+
})) : Effect.matchEffect(Effect.tryPromise({
|
|
32
|
+
try: () => {
|
|
33
|
+
const image = new Image();
|
|
34
|
+
if (source.sizes !== void 0) image.sizes = source.sizes;
|
|
35
|
+
if (source.srcset !== void 0) image.srcset = source.srcset;
|
|
36
|
+
image.src = source.src;
|
|
37
|
+
return image.decode();
|
|
38
|
+
},
|
|
39
|
+
catch: (error) => error instanceof Error ? error.message : String(error)
|
|
40
|
+
}), {
|
|
41
|
+
onFailure: (message) => Effect.succeed(ImageDecodeMessage.DecodeFailed({
|
|
42
|
+
src: source.src,
|
|
43
|
+
message
|
|
44
|
+
})),
|
|
45
|
+
onSuccess: () => Effect.succeed(ImageDecodeMessage.Decoded({ src: source.src }))
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
export { ImageDecodeMessage, decodeImage };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=image-decode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-decode.js","names":[],"sources":["../../src/dom/image-decode.ts"],"sourcesContent":["/**\n * An image fetched and decoded before it is shown, as a Command: a lightbox\n * or a carousel asks for the next picture, and shows it on `Decoded`, so the\n * switch never passes through a blank frame. A Command, not a Mount: the\n * Model decides to show the image, so the Message that leads there carries\n * the news that it is ready.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ImageDecodeMessage = defineMessageUnion({\n Decoded: { src: Schema.String },\n DecodeFailed: { src: Schema.String, message: Schema.String },\n})\nexport type ImageDecodeMessage = typeof ImageDecodeMessage.Type\n\n/** What to decode: a URL, or an `img`'s candidates, so the browser decodes the one it would draw. */\nexport interface ImageSource {\n readonly src: string\n readonly srcset?: string | undefined\n readonly sizes?: string | undefined\n}\n\n/**\n * Fetches and decodes `source` (`img.decode()`), yielding `Decoded` once it\n * can be painted at once, or `DecodeFailed` when it cannot be fetched or read,\n * or there is no document (a server render). Given `srcset` and `sizes`, as\n * `foldkit-media`'s `Picture.Img` has them, the browser picks the candidate\n * it would draw, so the decoded file is the one shown.\n */\nexport const decodeImage = (source: ImageSource): Command<ImageDecodeMessage, never, never> => ({\n name: 'Image.decode',\n args: { src: source.src },\n effect:\n typeof document === 'undefined'\n ? Effect.succeed(\n ImageDecodeMessage.DecodeFailed({ src: source.src, message: 'there is no document' }),\n )\n : Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n const image = new Image()\n // Before `src`, so the browser chooses among the candidates, not the fallback.\n if (source.sizes !== undefined) image.sizes = source.sizes\n if (source.srcset !== undefined) image.srcset = source.srcset\n image.src = source.src\n return image.decode()\n },\n catch: error => (error instanceof Error ? error.message : String(error)),\n }),\n {\n onFailure: message =>\n Effect.succeed(ImageDecodeMessage.DecodeFailed({ src: source.src, message })),\n onSuccess: () => Effect.succeed(ImageDecodeMessage.Decoded({ src: source.src })),\n },\n ),\n})\n"],"mappings":";;;;;;;;;;AAWA,MAAa,qBAAqB,mBAAmB;CACnD,SAAS,EAAE,KAAK,OAAO,OAAO;CAC9B,cAAc;EAAE,KAAK,OAAO;EAAQ,SAAS,OAAO;CAAO;AAC7D,CAAC;;;;;;;;AAiBD,MAAa,eAAe,YAAoE;CAC9F,MAAM;CACN,MAAM,EAAE,KAAK,OAAO,IAAI;CACxB,QACE,OAAO,aAAa,cAChB,OAAO,QACL,mBAAmB,aAAa;EAAE,KAAK,OAAO;EAAK,SAAS;CAAuB,CAAC,CACtF,IACA,OAAO,YACL,OAAO,WAAW;EAChB,WAAW;GACT,MAAM,QAAQ,IAAI,MAAM;GAExB,IAAI,OAAO,UAAU,KAAA,GAAW,MAAM,QAAQ,OAAO;GACrD,IAAI,OAAO,WAAW,KAAA,GAAW,MAAM,SAAS,OAAO;GACvD,MAAM,MAAM,OAAO;GACnB,OAAO,MAAM,OAAO;EACtB;EACA,QAAO,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACxE,CAAC,GACD;EACE,YAAW,YACT,OAAO,QAAQ,mBAAmB,aAAa;GAAE,KAAK,OAAO;GAAK;EAAQ,CAAC,CAAC;EAC9E,iBAAiB,OAAO,QAAQ,mBAAmB,QAAQ,EAAE,KAAK,OAAO,IAAI,CAAC,CAAC;CACjF,CACF;AACR"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export * from "./autofocus.js";
|
|
2
|
+
export * from "./clipboard.js";
|
|
3
|
+
export * from "./focus-scope.js";
|
|
4
|
+
export * from "./input-mask.js";
|
|
5
|
+
export * from "./keep-in-view.js";
|
|
6
|
+
export * from "./layers.js";
|
|
7
|
+
export * from "./move.js";
|
|
8
|
+
export * from "./script-loader.js";
|
|
9
|
+
export * from "./share.js";
|
|
10
|
+
export * from "./targets.js";
|
|
11
|
+
export * from "./pointer-drag.js";
|
|
12
|
+
export * from "./measure.js";
|
|
13
|
+
export * from "./editable-text.js";
|
|
14
|
+
export * from "./follow-tab-stop.js";
|
|
15
|
+
export * from "./scroll.js";
|
|
16
|
+
export * from "./pointed-links.js";
|
|
17
|
+
export * from "./fluid-hover.js";
|
|
18
|
+
export * from "./keyboard-inset.js";
|
|
19
|
+
export * from "./scroll-bars.js";
|
|
20
|
+
export * from "./swipe.js";
|
|
21
|
+
export * from "./scroll-settle.js";
|
|
22
|
+
export * from "./pointer-position.js";
|
|
23
|
+
export * from "./track-box.js";
|
|
24
|
+
export * from "./image-decode.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export * from "./autofocus.js";
|
|
2
|
+
export * from "./clipboard.js";
|
|
3
|
+
export * from "./focus-scope.js";
|
|
4
|
+
export * from "./input-mask.js";
|
|
5
|
+
export * from "./keep-in-view.js";
|
|
6
|
+
export * from "./layers.js";
|
|
7
|
+
export * from "./move.js";
|
|
8
|
+
export * from "./script-loader.js";
|
|
9
|
+
export * from "./share.js";
|
|
10
|
+
export * from "./targets.js";
|
|
11
|
+
export * from "./pointer-drag.js";
|
|
12
|
+
export * from "./measure.js";
|
|
13
|
+
export * from "./editable-text.js";
|
|
14
|
+
export * from "./follow-tab-stop.js";
|
|
15
|
+
export * from "./scroll.js";
|
|
16
|
+
export * from "./pointed-links.js";
|
|
17
|
+
export * from "./fluid-hover.js";
|
|
18
|
+
export * from "./keyboard-inset.js";
|
|
19
|
+
export * from "./scroll-bars.js";
|
|
20
|
+
export * from "./swipe.js";
|
|
21
|
+
export * from "./scroll-settle.js";
|
|
22
|
+
export * from "./pointer-position.js";
|
|
23
|
+
export * from "./track-box.js";
|
|
24
|
+
export * from "./image-decode.js";
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
/** What `InputMask` reports after each edit: the masked text shown, and the characters typed. */
|
|
4
|
+
export declare const Input: Schema.TaggedStruct<"Input", {
|
|
5
|
+
readonly value: Schema.String;
|
|
6
|
+
readonly raw: Schema.String;
|
|
7
|
+
}>;
|
|
8
|
+
export type Input = typeof Input.Type;
|
|
9
|
+
/**
|
|
10
|
+
* The mask as a pure function: `#` takes a digit, `A` a letter, `*` either, and
|
|
11
|
+
* any other character of the pattern is written in once input reaches past it.
|
|
12
|
+
* Input that fits no placeholder is skipped. `masked` is what the field shows;
|
|
13
|
+
* `raw` is only the characters typed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const applyMask: (value: string, pattern: string) => {
|
|
16
|
+
readonly masked: string;
|
|
17
|
+
readonly raw: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* An input mask as a Mount plus a pure mask function. The pattern language
|
|
21
|
+
* is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —
|
|
22
|
+
* with every other character literal. Literals appear only once a later placeholder
|
|
23
|
+
* consumes input, so partial values never dangle a separator. The Mount
|
|
24
|
+
* rewrites the field, restores the caret approximately, and emits `Input`
|
|
25
|
+
* with both the masked and raw values; the parent owns the state, like any
|
|
26
|
+
* controlled input.
|
|
27
|
+
*/
|
|
28
|
+
export declare const InputMask: Mount.MountDefinitionWithArgs<"InputMask", {
|
|
29
|
+
pattern: Schema.String;
|
|
30
|
+
}, {
|
|
31
|
+
readonly _tag: "Input";
|
|
32
|
+
readonly value: string;
|
|
33
|
+
readonly raw: string;
|
|
34
|
+
}>;
|
|
35
|
+
//# sourceMappingURL=input-mask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-mask.d.ts","names":[],"sources":["../../src/dom/input-mask.ts"],"mappings":";;;qBAIa,OAAK,OAAA;;;;YAIN,eAAe,MAAM;;;;;;;qBAiCpB,YAAS,eACP;WAED;WAAyB;;;;;;;;;;;qBA6C1B,WAAS,MAAA"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/input-mask.ts
|
|
4
|
+
/** What `InputMask` reports after each edit: the masked text shown, and the characters typed. */
|
|
5
|
+
const Input = Schema.TaggedStruct("Input", {
|
|
6
|
+
value: Schema.String,
|
|
7
|
+
raw: Schema.String
|
|
8
|
+
});
|
|
9
|
+
const isDigit = (char) => char >= "0" && char <= "9";
|
|
10
|
+
const isLetter = (char) => char >= "a" && char <= "z" || char >= "A" && char <= "Z";
|
|
11
|
+
const matches = (placeholder, char) => placeholder === "#" ? isDigit(char) : placeholder === "A" ? isLetter(char) : isDigit(char) || isLetter(char);
|
|
12
|
+
const isPlaceholder = (char) => char === "#" || char === "A" || char === "*";
|
|
13
|
+
/** Whether any placeholder at or after `from` consumes any input at or after `at`. */
|
|
14
|
+
const fillable = (pattern, from, value, at) => {
|
|
15
|
+
for (let i = from; i < pattern.length; i++) {
|
|
16
|
+
const placeholder = pattern[i];
|
|
17
|
+
if (!isPlaceholder(placeholder)) continue;
|
|
18
|
+
for (let j = at; j < value.length; j++) if (matches(placeholder, value[j])) return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The mask as a pure function: `#` takes a digit, `A` a letter, `*` either, and
|
|
24
|
+
* any other character of the pattern is written in once input reaches past it.
|
|
25
|
+
* Input that fits no placeholder is skipped. `masked` is what the field shows;
|
|
26
|
+
* `raw` is only the characters typed.
|
|
27
|
+
*/
|
|
28
|
+
const applyMask = (value, pattern) => {
|
|
29
|
+
let masked = "";
|
|
30
|
+
let raw = "";
|
|
31
|
+
let at = 0;
|
|
32
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
33
|
+
const slot = pattern[i];
|
|
34
|
+
if (isPlaceholder(slot)) {
|
|
35
|
+
while (at < value.length && !matches(slot, value[at])) at++;
|
|
36
|
+
if (at >= value.length) break;
|
|
37
|
+
masked += value[at];
|
|
38
|
+
raw += value[at];
|
|
39
|
+
at++;
|
|
40
|
+
} else if (fillable(pattern, i + 1, value, at)) masked += slot;
|
|
41
|
+
else break;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
masked,
|
|
45
|
+
raw
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const asField = (element) => {
|
|
49
|
+
const field = element;
|
|
50
|
+
return typeof field.value === "string" && typeof field.addEventListener === "function" ? field : null;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* An input mask as a Mount plus a pure mask function. The pattern language
|
|
54
|
+
* is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —
|
|
55
|
+
* with every other character literal. Literals appear only once a later placeholder
|
|
56
|
+
* consumes input, so partial values never dangle a separator. The Mount
|
|
57
|
+
* rewrites the field, restores the caret approximately, and emits `Input`
|
|
58
|
+
* with both the masked and raw values; the parent owns the state, like any
|
|
59
|
+
* controlled input.
|
|
60
|
+
*/
|
|
61
|
+
const InputMask = Mount.defineStream("InputMask", {
|
|
62
|
+
messages: [Input],
|
|
63
|
+
args: { pattern: Schema.String },
|
|
64
|
+
execute: ({ element, pattern }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
65
|
+
const field = asField(element);
|
|
66
|
+
if (field === null) return;
|
|
67
|
+
const onInput = () => {
|
|
68
|
+
const { masked, raw } = applyMask(field.value, pattern);
|
|
69
|
+
if (masked !== field.value) {
|
|
70
|
+
const caret = field.selectionStart ?? masked.length;
|
|
71
|
+
field.value = masked;
|
|
72
|
+
const at = Math.min(caret, masked.length);
|
|
73
|
+
try {
|
|
74
|
+
field.setSelectionRange?.(at, at);
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
Queue.offerUnsafe(queue, Input.make({
|
|
78
|
+
value: masked,
|
|
79
|
+
raw
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
83
|
+
field.addEventListener("input", onInput);
|
|
84
|
+
return field;
|
|
85
|
+
}), (current) => Effect.sync(() => current.removeEventListener("input", onInput)));
|
|
86
|
+
}))
|
|
87
|
+
});
|
|
88
|
+
export { Input, InputMask, applyMask };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=input-mask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-mask.js","names":[],"sources":["../../src/dom/input-mask.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** What `InputMask` reports after each edit: the masked text shown, and the characters typed. */\nexport const Input = Schema.TaggedStruct('Input', {\n value: Schema.String,\n raw: Schema.String,\n})\nexport type Input = typeof Input.Type\n\nconst isDigit = (char: string): boolean => char >= '0' && char <= '9'\nconst isLetter = (char: string): boolean =>\n (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')\n\nconst matches = (placeholder: string, char: string): boolean =>\n placeholder === '#'\n ? isDigit(char)\n : placeholder === 'A'\n ? isLetter(char)\n : isDigit(char) || isLetter(char)\n\nconst isPlaceholder = (char: string): boolean => char === '#' || char === 'A' || char === '*'\n\n/** Whether any placeholder at or after `from` consumes any input at or after `at`. */\nconst fillable = (pattern: string, from: number, value: string, at: number): boolean => {\n for (let i = from; i < pattern.length; i++) {\n const placeholder = pattern[i]!\n if (!isPlaceholder(placeholder)) continue\n for (let j = at; j < value.length; j++) {\n if (matches(placeholder, value[j]!)) return true\n }\n }\n return false\n}\n\n/**\n * The mask as a pure function: `#` takes a digit, `A` a letter, `*` either, and\n * any other character of the pattern is written in once input reaches past it.\n * Input that fits no placeholder is skipped. `masked` is what the field shows;\n * `raw` is only the characters typed.\n */\nexport const applyMask = (\n value: string,\n pattern: string,\n): { readonly masked: string; readonly raw: string } => {\n let masked = ''\n let raw = ''\n let at = 0\n for (let i = 0; i < pattern.length; i++) {\n const slot = pattern[i]!\n if (isPlaceholder(slot)) {\n while (at < value.length && !matches(slot, value[at]!)) at++\n if (at >= value.length) break\n masked += value[at]!\n raw += value[at]!\n at++\n } else if (fillable(pattern, i + 1, value, at)) {\n masked += slot\n } else {\n break\n }\n }\n return { masked, raw }\n}\n\ninterface MaskableField {\n value: string\n selectionStart: number | null\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n setSelectionRange?(start: number, end: number): void\n}\n\nconst asField = (element: Element): MaskableField | null => {\n const field = element as unknown as Partial<MaskableField>\n return typeof field.value === 'string' && typeof field.addEventListener === 'function'\n ? (field as MaskableField)\n : null\n}\n\n/**\n * An input mask as a Mount plus a pure mask function. The pattern language\n * is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —\n * with every other character literal. Literals appear only once a later placeholder\n * consumes input, so partial values never dangle a separator. The Mount\n * rewrites the field, restores the caret approximately, and emits `Input`\n * with both the masked and raw values; the parent owns the state, like any\n * controlled input.\n */\nexport const InputMask = Mount.defineStream('InputMask', {\n messages: [Input],\n args: { pattern: Schema.String },\n execute: ({ element, pattern }) =>\n Stream.callback<typeof Input.Type>(queue =>\n Effect.gen(function* () {\n const field = asField(element)\n if (field === null) return\n const onInput = () => {\n const { masked, raw } = applyMask(field.value, pattern)\n if (masked !== field.value) {\n const caret = field.selectionStart ?? masked.length\n field.value = masked\n const at = Math.min(caret, masked.length)\n try {\n field.setSelectionRange?.(at, at)\n } catch {\n // Select-all style inputs reject ranges; the value still stands.\n }\n }\n Queue.offerUnsafe(queue, Input.make({ value: masked, raw }))\n }\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n field.addEventListener('input', onInput)\n return field\n }),\n current => Effect.sync(() => current.removeEventListener('input', onInput)),\n )\n }),\n ),\n})\n"],"mappings":";;;;AAIA,MAAa,QAAQ,OAAO,aAAa,SAAS;CAChD,OAAO,OAAO;CACd,KAAK,OAAO;AACd,CAAC;AAGD,MAAM,WAAW,SAA0B,QAAQ,OAAO,QAAQ;AAClE,MAAM,YAAY,SACf,QAAQ,OAAO,QAAQ,OAAS,QAAQ,OAAO,QAAQ;AAE1D,MAAM,WAAW,aAAqB,SACpC,gBAAgB,MACZ,QAAQ,IAAI,IACZ,gBAAgB,MACd,SAAS,IAAI,IACb,QAAQ,IAAI,KAAK,SAAS,IAAI;AAEtC,MAAM,iBAAiB,SAA0B,SAAS,OAAO,SAAS,OAAO,SAAS;;AAG1F,MAAM,YAAY,SAAiB,MAAc,OAAe,OAAwB;CACtF,KAAK,IAAI,IAAI,MAAM,IAAI,QAAQ,QAAQ,KAAK;EAC1C,MAAM,cAAc,QAAQ;EAC5B,IAAI,CAAC,cAAc,WAAW,GAAG;EACjC,KAAK,IAAI,IAAI,IAAI,IAAI,MAAM,QAAQ,KACjC,IAAI,QAAQ,aAAa,MAAM,EAAG,GAAG,OAAO;CAEhD;CACA,OAAO;AACT;;;;;;;AAQA,MAAa,aACX,OACA,YACsD;CACtD,IAAI,SAAS;CACb,IAAI,MAAM;CACV,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,OAAO,QAAQ;EACrB,IAAI,cAAc,IAAI,GAAG;GACvB,OAAO,KAAK,MAAM,UAAU,CAAC,QAAQ,MAAM,MAAM,GAAI,GAAG;GACxD,IAAI,MAAM,MAAM,QAAQ;GACxB,UAAU,MAAM;GAChB,OAAO,MAAM;GACb;EACF,OAAO,IAAI,SAAS,SAAS,IAAI,GAAG,OAAO,EAAE,GAC3C,UAAU;OAEV;CAEJ;CACA,OAAO;EAAE;EAAQ;CAAI;AACvB;AAUA,MAAM,WAAW,YAA2C;CAC1D,MAAM,QAAQ;CACd,OAAO,OAAO,MAAM,UAAU,YAAY,OAAO,MAAM,qBAAqB,aACvE,QACD;AACN;;;;;;;;;;AAWA,MAAa,YAAY,MAAM,aAAa,aAAa;CACvD,UAAU,CAAC,KAAK;CAChB,MAAM,EAAE,SAAS,OAAO,OAAO;CAC/B,UAAU,EAAE,SAAS,cACnB,OAAO,UAA4B,UACjC,OAAO,IAAI,aAAa;EACtB,MAAM,QAAQ,QAAQ,OAAO;EAC7B,IAAI,UAAU,MAAM;EACpB,MAAM,gBAAgB;GACpB,MAAM,EAAE,QAAQ,QAAQ,UAAU,MAAM,OAAO,OAAO;GACtD,IAAI,WAAW,MAAM,OAAO;IAC1B,MAAM,QAAQ,MAAM,kBAAkB,OAAO;IAC7C,MAAM,QAAQ;IACd,MAAM,KAAK,KAAK,IAAI,OAAO,OAAO,MAAM;IACxC,IAAI;KACF,MAAM,oBAAoB,IAAI,EAAE;IAClC,QAAQ,CAER;GACF;GACA,MAAM,YAAY,OAAO,MAAM,KAAK;IAAE,OAAO;IAAQ;GAAI,CAAC,CAAC;EAC7D;EACA,OAAO,OAAO,eACZ,OAAO,WAAW;GAChB,MAAM,iBAAiB,SAAS,OAAO;GACvC,OAAO;EACT,CAAC,IACD,YAAW,OAAO,WAAW,QAAQ,oBAAoB,SAAS,OAAO,CAAC,CAC5E;CACF,CAAC,CACH;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
/**
|
|
4
|
+
* Keeps what is marked in view as a Mount: whenever an element in the mounted
|
|
5
|
+
* one's subtree newly matches `selector`, such as the row just selected in a
|
|
6
|
+
* list, it is scrolled into view the least amount that shows it (`nearest`).
|
|
7
|
+
* The element keeps its identity and its focus: nothing is redrawn to move it.
|
|
8
|
+
*
|
|
9
|
+
* It watches with a `MutationObserver` for as long as it is mounted, which is as
|
|
10
|
+
* long as the list or canvas that can change what is marked.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KeepInView: Mount.MountDefinitionWithArgs<"KeepInView", {
|
|
13
|
+
/** What is marked, as a selector within the element: `[aria-selected="true"]`. */
|
|
14
|
+
selector: Schema.String;
|
|
15
|
+
}, never>;
|
|
16
|
+
//# sourceMappingURL=keep-in-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keep-in-view.d.ts","names":[],"sources":["../../src/dom/keep-in-view.ts"],"mappings":";;;;;;;;;;;qBAYa,YAAU,MAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/keep-in-view.ts
|
|
4
|
+
/**
|
|
5
|
+
* Keeps what is marked in view as a Mount: whenever an element in the mounted
|
|
6
|
+
* one's subtree newly matches `selector`, such as the row just selected in a
|
|
7
|
+
* list, it is scrolled into view the least amount that shows it (`nearest`).
|
|
8
|
+
* The element keeps its identity and its focus: nothing is redrawn to move it.
|
|
9
|
+
*
|
|
10
|
+
* It watches with a `MutationObserver` for as long as it is mounted, which is as
|
|
11
|
+
* long as the list or canvas that can change what is marked.
|
|
12
|
+
*/
|
|
13
|
+
const KeepInView = Mount.defineStream("KeepInView", {
|
|
14
|
+
messages: [Schema.Never],
|
|
15
|
+
args: {
|
|
16
|
+
/** What is marked, as a selector within the element: `[aria-selected="true"]`. */
|
|
17
|
+
selector: Schema.String },
|
|
18
|
+
execute: ({ element, selector }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
19
|
+
let marked = element.querySelector(selector);
|
|
20
|
+
const observer = new MutationObserver(() => {
|
|
21
|
+
const now = element.querySelector(selector);
|
|
22
|
+
if (now !== null && now !== marked && typeof now.scrollIntoView === "function") now.scrollIntoView({
|
|
23
|
+
block: "nearest",
|
|
24
|
+
inline: "nearest"
|
|
25
|
+
});
|
|
26
|
+
marked = now;
|
|
27
|
+
});
|
|
28
|
+
observer.observe(element, {
|
|
29
|
+
subtree: true,
|
|
30
|
+
childList: true,
|
|
31
|
+
attributes: true
|
|
32
|
+
});
|
|
33
|
+
return observer;
|
|
34
|
+
}), (observer) => Effect.sync(() => observer.disconnect())))
|
|
35
|
+
});
|
|
36
|
+
export { KeepInView };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=keep-in-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keep-in-view.js","names":[],"sources":["../../src/dom/keep-in-view.ts"],"sourcesContent":["import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/**\n * Keeps what is marked in view as a Mount: whenever an element in the mounted\n * one's subtree newly matches `selector`, such as the row just selected in a\n * list, it is scrolled into view the least amount that shows it (`nearest`).\n * The element keeps its identity and its focus: nothing is redrawn to move it.\n *\n * It watches with a `MutationObserver` for as long as it is mounted, which is as\n * long as the list or canvas that can change what is marked.\n */\nexport const KeepInView = Mount.defineStream('KeepInView', {\n messages: [Schema.Never],\n args: {\n /** What is marked, as a selector within the element: `[aria-selected=\"true\"]`. */\n selector: Schema.String,\n },\n execute: ({ element, selector }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let marked = element.querySelector(selector)\n const observer = new MutationObserver(() => {\n const now = element.querySelector(selector)\n // Only a change of what is marked scrolls: the author's own scrolling stays.\n // A DOM with no layout (jsdom, in a test) has no scrollIntoView and nothing to scroll.\n if (now !== null && now !== marked && typeof now.scrollIntoView === 'function')\n now.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n marked = now\n })\n observer.observe(element, { subtree: true, childList: true, attributes: true })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n ),\n ),\n})\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,aAAa,MAAM,aAAa,cAAc;CACzD,UAAU,CAAC,OAAO,KAAK;CACvB,MAAM;;AAEJ,UAAU,OAAO,OACnB;CACA,UAAU,EAAE,SAAS,eACnB,OAAO,eACL,OAAO,eACL,OAAO,WAAW;EAChB,IAAI,SAAS,QAAQ,cAAc,QAAQ;EAC3C,MAAM,WAAW,IAAI,uBAAuB;GAC1C,MAAM,MAAM,QAAQ,cAAc,QAAQ;GAG1C,IAAI,QAAQ,QAAQ,QAAQ,UAAU,OAAO,IAAI,mBAAmB,YAClE,IAAI,eAAe;IAAE,OAAO;IAAW,QAAQ;GAAU,CAAC;GAC5D,SAAS;EACX,CAAC;EACD,SAAS,QAAQ,SAAS;GAAE,SAAS;GAAM,WAAW;GAAM,YAAY;EAAK,CAAC;EAC9E,OAAO;CACT,CAAC,IACD,aAAY,OAAO,WAAW,SAAS,WAAW,CAAC,CACrD,CACF;AACJ,CAAC"}
|