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,232 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/editable-text.ts
|
|
4
|
+
const TextEdited = Schema.TaggedStruct("TextEdited", {
|
|
5
|
+
/** The field, by its marking attribute's value. */
|
|
6
|
+
field: Schema.String,
|
|
7
|
+
text: Schema.String
|
|
8
|
+
});
|
|
9
|
+
const TextCommitted = Schema.TaggedStruct("TextCommitted", {
|
|
10
|
+
field: Schema.String,
|
|
11
|
+
text: Schema.String
|
|
12
|
+
});
|
|
13
|
+
const TextCancelled = Schema.TaggedStruct("TextCancelled", {
|
|
14
|
+
field: Schema.String,
|
|
15
|
+
/** The text the field had when editing began, which the DOM shows again. */
|
|
16
|
+
initial: Schema.String
|
|
17
|
+
});
|
|
18
|
+
const EditAsked = Schema.TaggedStruct("EditAsked", {
|
|
19
|
+
/** A marked field, not editable yet, that was double-clicked. */
|
|
20
|
+
field: Schema.String });
|
|
21
|
+
/** Whether a field takes more than one line. */
|
|
22
|
+
const multiline = (element) => element.getAttribute("aria-multiline") === "true";
|
|
23
|
+
/**
|
|
24
|
+
* A field's text. A browser ends an edited element with a line break it adds
|
|
25
|
+
* itself, which is not the author's; in one line, the author's breaks are spaces.
|
|
26
|
+
*/
|
|
27
|
+
const textOf = (element) => {
|
|
28
|
+
const text = element.innerText.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
29
|
+
return multiline(element) ? text : text.replace(/\n+/g, " ");
|
|
30
|
+
};
|
|
31
|
+
/** Puts the caret at the end of a field, which focuses it too. */
|
|
32
|
+
const caretAtEnd = (field) => {
|
|
33
|
+
field.focus();
|
|
34
|
+
const caret = document.createRange();
|
|
35
|
+
caret.selectNodeContents(field);
|
|
36
|
+
caret.collapse(false);
|
|
37
|
+
const selection = window.getSelection();
|
|
38
|
+
selection?.removeAllRanges();
|
|
39
|
+
selection?.addRange(caret);
|
|
40
|
+
};
|
|
41
|
+
/** Whether a key press belongs to an input method still composing, not to the field. */
|
|
42
|
+
const composing = (event) => event.isComposing || event.keyCode === 229;
|
|
43
|
+
/**
|
|
44
|
+
* Text typed into an editable descendant of an element, as a Mount: one set of
|
|
45
|
+
* listeners on the container, like `Targets`. A field is a descendant marked
|
|
46
|
+
* by an attribute whose value names it, while it is `contenteditable`. The
|
|
47
|
+
* view decides which one is; the Mount focuses it as it becomes so (made
|
|
48
|
+
* editable, or drawn editable), with the caret at its end, and at no other
|
|
49
|
+
* time. When Enter or Escape ends an edit and the view then removes the field
|
|
50
|
+
* or makes it no longer editable, focus, left on nothing, comes back to the
|
|
51
|
+
* container, so the next key reaches it; give the container a `tabindex`. A double-click on a marked field that is not editable yet asks for it
|
|
52
|
+
* to be (`EditAsked`).
|
|
53
|
+
*
|
|
54
|
+
* It reads `innerText`, never `innerHTML`, so what arrives is text. A field is
|
|
55
|
+
* one line unless it carries `aria-multiline="true"`: in one line, a line
|
|
56
|
+
* break becomes a space. Nothing is reported while an input method composes;
|
|
57
|
+
* the composed text arrives once, when it is done.
|
|
58
|
+
*
|
|
59
|
+
* Enter commits (Shift+Enter breaks a line in a multiline field), and so does
|
|
60
|
+
* leaving the field. Escape cancels, and puts the text the field had when
|
|
61
|
+
* editing began back in the DOM, since a view that stopped redrawing the field
|
|
62
|
+
* while it was edited will not. Where the browser lacks `plaintext-only`, a
|
|
63
|
+
* paste is inserted as text. What an edit means is the parent's `update`.
|
|
64
|
+
*/
|
|
65
|
+
const EditableText = Mount.defineStream("EditableText", {
|
|
66
|
+
messages: [
|
|
67
|
+
TextEdited,
|
|
68
|
+
TextCommitted,
|
|
69
|
+
TextCancelled,
|
|
70
|
+
EditAsked
|
|
71
|
+
],
|
|
72
|
+
args: {
|
|
73
|
+
/** The attribute that marks a field, holding its name. */
|
|
74
|
+
attribute: Schema.String },
|
|
75
|
+
execute: ({ element, attribute }) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
|
|
76
|
+
/** The field being edited: its name, its text when editing began, and the last reported. */
|
|
77
|
+
let current;
|
|
78
|
+
let composingText = false;
|
|
79
|
+
let ending;
|
|
80
|
+
/** The marked field at or above an event's target, inside the container. */
|
|
81
|
+
const markedOf = (target) => {
|
|
82
|
+
if (!(target instanceof Element)) return void 0;
|
|
83
|
+
const field = target.closest(`[${attribute}]`);
|
|
84
|
+
return field instanceof HTMLElement && element.contains(field) ? field : void 0;
|
|
85
|
+
};
|
|
86
|
+
/** The same, while it is editable. */
|
|
87
|
+
const fieldOf = (target) => {
|
|
88
|
+
const field = markedOf(target);
|
|
89
|
+
return field?.isContentEditable === true ? field : void 0;
|
|
90
|
+
};
|
|
91
|
+
/** Begins an edit of `field`, unless one of it is under way: on focus, or typing. */
|
|
92
|
+
const begin = (field) => {
|
|
93
|
+
if (current?.field === field && !current.ended) return current;
|
|
94
|
+
const text = textOf(field);
|
|
95
|
+
current = {
|
|
96
|
+
field,
|
|
97
|
+
name: field.getAttribute(attribute) ?? "",
|
|
98
|
+
initial: text,
|
|
99
|
+
reported: text,
|
|
100
|
+
ended: false
|
|
101
|
+
};
|
|
102
|
+
return current;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The edit of `field`: the one under way or just ended, else one begun
|
|
106
|
+
* now, for a field focused before these listeners came. An ended edit
|
|
107
|
+
* reports nothing more, so the blur that follows a commit or a cancel
|
|
108
|
+
* does not commit again.
|
|
109
|
+
*/
|
|
110
|
+
const editOf = (field) => current?.field === field ? current : begin(field);
|
|
111
|
+
const report = (field) => {
|
|
112
|
+
const edit = editOf(field);
|
|
113
|
+
if (edit.ended) return;
|
|
114
|
+
const text = textOf(field);
|
|
115
|
+
if (text === edit.reported) return;
|
|
116
|
+
edit.reported = text;
|
|
117
|
+
Queue.offerUnsafe(queue, TextEdited.make({
|
|
118
|
+
field: edit.name,
|
|
119
|
+
text
|
|
120
|
+
}));
|
|
121
|
+
};
|
|
122
|
+
const commit = (field) => {
|
|
123
|
+
const edit = editOf(field);
|
|
124
|
+
if (edit.ended) return;
|
|
125
|
+
edit.ended = true;
|
|
126
|
+
Queue.offerUnsafe(queue, TextCommitted.make({
|
|
127
|
+
field: edit.name,
|
|
128
|
+
text: textOf(field)
|
|
129
|
+
}));
|
|
130
|
+
};
|
|
131
|
+
const cancel = (field) => {
|
|
132
|
+
const edit = editOf(field);
|
|
133
|
+
if (edit.ended) return;
|
|
134
|
+
edit.ended = true;
|
|
135
|
+
field.innerText = edit.initial;
|
|
136
|
+
caretAtEnd(field);
|
|
137
|
+
Queue.offerUnsafe(queue, TextCancelled.make({
|
|
138
|
+
field: edit.name,
|
|
139
|
+
initial: edit.initial
|
|
140
|
+
}));
|
|
141
|
+
};
|
|
142
|
+
const listeners = [
|
|
143
|
+
["dblclick", (event) => {
|
|
144
|
+
const field = markedOf(event.target);
|
|
145
|
+
if (field === void 0 || field.isContentEditable) return;
|
|
146
|
+
Queue.offerUnsafe(queue, EditAsked.make({ field: field.getAttribute(attribute) ?? "" }));
|
|
147
|
+
}],
|
|
148
|
+
["focusin", (event) => {
|
|
149
|
+
composingText = false;
|
|
150
|
+
const field = fieldOf(event.target);
|
|
151
|
+
if (field !== void 0) begin(field);
|
|
152
|
+
}],
|
|
153
|
+
["beforeinput", (event) => {
|
|
154
|
+
const field = fieldOf(event.target);
|
|
155
|
+
if (field !== void 0) begin(field);
|
|
156
|
+
}],
|
|
157
|
+
["compositionstart", () => composingText = true],
|
|
158
|
+
["compositionend", (event) => {
|
|
159
|
+
composingText = false;
|
|
160
|
+
const field = fieldOf(event.target);
|
|
161
|
+
if (field !== void 0) report(field);
|
|
162
|
+
}],
|
|
163
|
+
["input", (event) => {
|
|
164
|
+
const field = fieldOf(event.target);
|
|
165
|
+
if (field !== void 0 && !composingText) report(field);
|
|
166
|
+
}],
|
|
167
|
+
["keydown", (event) => {
|
|
168
|
+
const field = fieldOf(event.target);
|
|
169
|
+
const key = event;
|
|
170
|
+
if (field === void 0 || composing(key)) return;
|
|
171
|
+
if (key.key === "Escape") {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
ending = field;
|
|
174
|
+
cancel(field);
|
|
175
|
+
} else if (key.key === "Enter" && !(key.shiftKey && multiline(field))) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
ending = field;
|
|
178
|
+
commit(field);
|
|
179
|
+
}
|
|
180
|
+
}],
|
|
181
|
+
["focusout", (event) => {
|
|
182
|
+
const field = fieldOf(event.target);
|
|
183
|
+
if (field === void 0) return;
|
|
184
|
+
queueMicrotask(() => {
|
|
185
|
+
if (field === ending && field.isConnected && field.isContentEditable) ending = void 0;
|
|
186
|
+
});
|
|
187
|
+
commit(field);
|
|
188
|
+
}],
|
|
189
|
+
["paste", (event) => {
|
|
190
|
+
const field = fieldOf(event.target);
|
|
191
|
+
if (field === void 0 || field.contentEditable === "plaintext-only") return;
|
|
192
|
+
const pasted = event.clipboardData?.getData("text/plain") ?? "";
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
begin(field);
|
|
195
|
+
document.execCommand("insertText", false, pasted);
|
|
196
|
+
}]
|
|
197
|
+
];
|
|
198
|
+
const editable = `[${attribute}][contenteditable]:not([contenteditable="false"])`;
|
|
199
|
+
const becoming = new MutationObserver((records) => {
|
|
200
|
+
if (ending !== void 0 && !(ending.isConnected && ending.isContentEditable)) {
|
|
201
|
+
ending = void 0;
|
|
202
|
+
const focused = document.activeElement;
|
|
203
|
+
if ((focused === null || focused === document.body) && element instanceof HTMLElement) element.focus({ preventScroll: true });
|
|
204
|
+
}
|
|
205
|
+
for (const record of records) {
|
|
206
|
+
const changed = record.type === "attributes" ? [record.target] : Array.from(record.addedNodes);
|
|
207
|
+
for (const node of changed) {
|
|
208
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
209
|
+
const field = node.matches(editable) ? node : node.querySelector(editable);
|
|
210
|
+
if (field instanceof HTMLElement) return caretAtEnd(field);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
for (const [type, listener] of listeners) element.addEventListener(type, listener);
|
|
215
|
+
becoming.observe(element, {
|
|
216
|
+
subtree: true,
|
|
217
|
+
childList: true,
|
|
218
|
+
attributes: true,
|
|
219
|
+
attributeFilter: ["contenteditable"]
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
listeners,
|
|
223
|
+
becoming
|
|
224
|
+
};
|
|
225
|
+
}), ({ listeners, becoming }) => Effect.sync(() => {
|
|
226
|
+
becoming.disconnect();
|
|
227
|
+
for (const [type, listener] of listeners) element.removeEventListener(type, listener);
|
|
228
|
+
})))
|
|
229
|
+
});
|
|
230
|
+
export { EditAsked, EditableText, TextCancelled, TextCommitted, TextEdited, textOf };
|
|
231
|
+
|
|
232
|
+
//# sourceMappingURL=editable-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editable-text.js","names":[],"sources":["../../src/dom/editable-text.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const TextEdited = Schema.TaggedStruct('TextEdited', {\n /** The field, by its marking attribute's value. */\n field: Schema.String,\n text: Schema.String,\n})\nexport type TextEdited = typeof TextEdited.Type\n\nexport const TextCommitted = Schema.TaggedStruct('TextCommitted', {\n field: Schema.String,\n text: Schema.String,\n})\nexport type TextCommitted = typeof TextCommitted.Type\n\nexport const TextCancelled = Schema.TaggedStruct('TextCancelled', {\n field: Schema.String,\n /** The text the field had when editing began, which the DOM shows again. */\n initial: Schema.String,\n})\nexport type TextCancelled = typeof TextCancelled.Type\n\nexport const EditAsked = Schema.TaggedStruct('EditAsked', {\n /** A marked field, not editable yet, that was double-clicked. */\n field: Schema.String,\n})\nexport type EditAsked = typeof EditAsked.Type\n\nexport type TextFact = TextEdited | TextCommitted | TextCancelled | EditAsked\n\n/** Whether a field takes more than one line. */\nconst multiline = (element: HTMLElement) => element.getAttribute('aria-multiline') === 'true'\n\n/**\n * A field's text. A browser ends an edited element with a line break it adds\n * itself, which is not the author's; in one line, the author's breaks are spaces.\n */\nexport const textOf = (element: HTMLElement): string => {\n const text = element.innerText.replace(/\\r\\n?/g, '\\n').replace(/\\n$/, '')\n return multiline(element) ? text : text.replace(/\\n+/g, ' ')\n}\n\n/** Puts the caret at the end of a field, which focuses it too. */\nconst caretAtEnd = (field: HTMLElement) => {\n // Chromium also focuses on the selection below; no standard says a browser must.\n field.focus()\n const caret = document.createRange()\n caret.selectNodeContents(field)\n caret.collapse(false)\n const selection = window.getSelection()\n selection?.removeAllRanges()\n selection?.addRange(caret)\n}\n\n/** Whether a key press belongs to an input method still composing, not to the field. */\nconst composing = (event: KeyboardEvent) => event.isComposing || event.keyCode === 229\n\n/**\n * Text typed into an editable descendant of an element, as a Mount: one set of\n * listeners on the container, like `Targets`. A field is a descendant marked\n * by an attribute whose value names it, while it is `contenteditable`. The\n * view decides which one is; the Mount focuses it as it becomes so (made\n * editable, or drawn editable), with the caret at its end, and at no other\n * time. When Enter or Escape ends an edit and the view then removes the field\n * or makes it no longer editable, focus, left on nothing, comes back to the\n * container, so the next key reaches it; give the container a `tabindex`. A double-click on a marked field that is not editable yet asks for it\n * to be (`EditAsked`).\n *\n * It reads `innerText`, never `innerHTML`, so what arrives is text. A field is\n * one line unless it carries `aria-multiline=\"true\"`: in one line, a line\n * break becomes a space. Nothing is reported while an input method composes;\n * the composed text arrives once, when it is done.\n *\n * Enter commits (Shift+Enter breaks a line in a multiline field), and so does\n * leaving the field. Escape cancels, and puts the text the field had when\n * editing began back in the DOM, since a view that stopped redrawing the field\n * while it was edited will not. Where the browser lacks `plaintext-only`, a\n * paste is inserted as text. What an edit means is the parent's `update`.\n */\nexport const EditableText = Mount.defineStream('EditableText', {\n messages: [TextEdited, TextCommitted, TextCancelled, EditAsked],\n args: {\n /** The attribute that marks a field, holding its name. */\n attribute: Schema.String,\n },\n execute: ({ element, attribute }) =>\n Stream.callback<TextFact>(queue =>\n Effect.acquireRelease(\n Effect.sync(() => {\n /** The field being edited: its name, its text when editing began, and the last reported. */\n let current:\n | {\n readonly field: HTMLElement\n readonly name: string\n readonly initial: string\n reported: string\n ended: boolean\n }\n | undefined\n let composingText = false\n // A field whose edit a key ended: when it leaves with focus on nothing, focus\n // comes back to the container, where the keys that began the edit are.\n let ending: HTMLElement | undefined\n\n /** The marked field at or above an event's target, inside the container. */\n const markedOf = (target: EventTarget | null): HTMLElement | undefined => {\n if (!(target instanceof Element)) return undefined\n const field = target.closest(`[${attribute}]`)\n return field instanceof HTMLElement && element.contains(field) ? field : undefined\n }\n /** The same, while it is editable. */\n const fieldOf = (target: EventTarget | null): HTMLElement | undefined => {\n const field = markedOf(target)\n return field?.isContentEditable === true ? field : undefined\n }\n /** Begins an edit of `field`, unless one of it is under way: on focus, or typing. */\n const begin = (field: HTMLElement) => {\n if (current?.field === field && !current.ended) return current\n const text = textOf(field)\n current = {\n field,\n name: field.getAttribute(attribute) ?? '',\n initial: text,\n reported: text,\n ended: false,\n }\n return current\n }\n /**\n * The edit of `field`: the one under way or just ended, else one begun\n * now, for a field focused before these listeners came. An ended edit\n * reports nothing more, so the blur that follows a commit or a cancel\n * does not commit again.\n */\n const editOf = (field: HTMLElement) => (current?.field === field ? current : begin(field))\n const report = (field: HTMLElement) => {\n const edit = editOf(field)\n if (edit.ended) return\n const text = textOf(field)\n // A browser may follow a composition with an input of the same text.\n if (text === edit.reported) return\n edit.reported = text\n Queue.offerUnsafe(queue, TextEdited.make({ field: edit.name, text }))\n }\n const commit = (field: HTMLElement) => {\n const edit = editOf(field)\n if (edit.ended) return\n edit.ended = true\n Queue.offerUnsafe(queue, TextCommitted.make({ field: edit.name, text: textOf(field) }))\n }\n const cancel = (field: HTMLElement) => {\n const edit = editOf(field)\n if (edit.ended) return\n edit.ended = true\n field.innerText = edit.initial\n // Writing the text moved the caret to its start; typing on goes after it.\n caretAtEnd(field)\n Queue.offerUnsafe(\n queue,\n TextCancelled.make({ field: edit.name, initial: edit.initial }),\n )\n }\n\n const listeners: ReadonlyArray<readonly [string, (event: Event) => void]> = [\n [\n 'dblclick',\n event => {\n const field = markedOf(event.target)\n // In an editable field, a double-click selects a word.\n if (field === undefined || field.isContentEditable) return\n Queue.offerUnsafe(\n queue,\n EditAsked.make({ field: field.getAttribute(attribute) ?? '' }),\n )\n },\n ],\n [\n 'focusin',\n event => {\n // A composition that never ended, in a field left, holds no other field silent.\n composingText = false\n const field = fieldOf(event.target)\n if (field !== undefined) begin(field)\n },\n ],\n // Before the change: a field focused before these listeners came is begun here.\n [\n 'beforeinput',\n event => {\n const field = fieldOf(event.target)\n if (field !== undefined) begin(field)\n },\n ],\n ['compositionstart', () => (composingText = true)],\n [\n 'compositionend',\n event => {\n composingText = false\n const field = fieldOf(event.target)\n if (field !== undefined) report(field)\n },\n ],\n [\n 'input',\n event => {\n const field = fieldOf(event.target)\n if (field !== undefined && !composingText) report(field)\n },\n ],\n [\n 'keydown',\n event => {\n const field = fieldOf(event.target)\n const key = event as KeyboardEvent\n if (field === undefined || composing(key)) return\n if (key.key === 'Escape') {\n event.preventDefault()\n ending = field\n cancel(field)\n } else if (key.key === 'Enter' && !(key.shiftKey && multiline(field))) {\n event.preventDefault()\n ending = field\n commit(field)\n }\n },\n ],\n [\n 'focusout',\n event => {\n const field = fieldOf(event.target)\n if (field === undefined) return\n // Left while still there and editable, asked once the change that may have\n // taken it away is done: the author went elsewhere, and focus is theirs.\n queueMicrotask(() => {\n if (field === ending && field.isConnected && field.isContentEditable)\n ending = undefined\n })\n commit(field)\n },\n ],\n [\n 'paste',\n event => {\n const field = fieldOf(event.target)\n // `plaintext-only` pastes text itself; elsewhere, markup would arrive.\n if (field === undefined || field.contentEditable === 'plaintext-only') return\n const pasted = (event as ClipboardEvent).clipboardData?.getData('text/plain') ?? ''\n event.preventDefault()\n // `insertText` fires no `beforeinput`: begun here, a paste after Enter is an edit.\n begin(field)\n // As typing is, so the browser's undo takes it back; `textOf` makes one line of it.\n document.execCommand('insertText', false, pasted)\n },\n ],\n ]\n // Focuses a field as it becomes editable, and only then: made so, or drawn so.\n // A field editable all along, or another going away, moves no focus.\n const editable = `[${attribute}][contenteditable]:not([contenteditable=\"false\"])`\n const becoming = new MutationObserver(records => {\n if (ending !== undefined && !(ending.isConnected && ending.isContentEditable)) {\n ending = undefined\n const focused = document.activeElement\n if ((focused === null || focused === document.body) && element instanceof HTMLElement)\n element.focus({ preventScroll: true })\n }\n for (const record of records) {\n const changed =\n record.type === 'attributes' ? [record.target] : Array.from(record.addedNodes)\n for (const node of changed) {\n if (!(node instanceof HTMLElement)) continue\n const field = node.matches(editable) ? node : node.querySelector(editable)\n if (field instanceof HTMLElement) return caretAtEnd(field)\n }\n }\n })\n\n for (const [type, listener] of listeners) element.addEventListener(type, listener)\n // A field made editable, drawn editable, or leaving.\n becoming.observe(element, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['contenteditable'],\n })\n return { listeners, becoming }\n }),\n ({ listeners, becoming }) =>\n Effect.sync(() => {\n becoming.disconnect()\n for (const [type, listener] of listeners) element.removeEventListener(type, listener)\n }),\n ),\n ),\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
/** The custom properties `FluidHover` writes on its container for a highlight named `name`. */
|
|
4
|
+
export declare const fluidHovered: (name: string) => {
|
|
5
|
+
readonly x: `--fk-${string}-x`;
|
|
6
|
+
readonly y: `--fk-${string}-y`;
|
|
7
|
+
readonly w: `--fk-${string}-w`;
|
|
8
|
+
readonly h: `--fk-${string}-h`;
|
|
9
|
+
/** `1` while an item is highlighted, `0` once the pointer and focus have left. */
|
|
10
|
+
readonly shown: `--fk-${string}-shown`;
|
|
11
|
+
/**
|
|
12
|
+
* `0` for the frame the highlight appears in, then `1`: a highlight's
|
|
13
|
+
* movement scales its duration by it, so it appears on its item rather
|
|
14
|
+
* than sliding in from where it last was.
|
|
15
|
+
*/
|
|
16
|
+
readonly moving: `--fk-${string}-moving`;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The attribute `FluidHover` sets on its container, to the highlight's name,
|
|
20
|
+
* while it runs: a stylist draws the highlight, and drops each item's own
|
|
21
|
+
* hover, only under it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FLUID_HOVER_ATTRIBUTE = "data-fluid-hover";
|
|
24
|
+
/** The attribute that names, inside an item, the control a press in a gap reaches. */
|
|
25
|
+
export declare const FLUID_PRESS_ATTRIBUTE = "data-fluid-press";
|
|
26
|
+
/** The attribute on the item the highlight is shown on, for what CSS draws beside it. */
|
|
27
|
+
export declare const FLUID_LIT_ATTRIBUTE = "data-fluid-lit";
|
|
28
|
+
/**
|
|
29
|
+
* The attribute `FluidHover` sets on its container, to the highlight's name,
|
|
30
|
+
* while the item that highlight is on is held. A stylist draws the press on
|
|
31
|
+
* the highlight (`data-fluid-held`) rather than on the item, whose own press
|
|
32
|
+
* is hidden under it; a press in a gap marks it too where `gapClick` sends
|
|
33
|
+
* that press to the lit item.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FLUID_HELD_ATTRIBUTE = "data-fluid-held";
|
|
36
|
+
declare const Axis: Schema.Literals<readonly ["x", "y", "xy"]>;
|
|
37
|
+
export type FluidAxis = typeof Axis.Type;
|
|
38
|
+
/**
|
|
39
|
+
* One highlight that follows the pointer across a container's items, as a
|
|
40
|
+
* Mount, with no Message and no Model: which item the pointer is nearest is
|
|
41
|
+
* presentation, known to the DOM and to no transition.
|
|
42
|
+
*
|
|
43
|
+
* The item nearest the pointer is the one highlighted, so the pointer in a gap
|
|
44
|
+
* between two items still points at one, and the highlight moves from item to
|
|
45
|
+
* item instead of blinking off between them. On the `y` axis nearness is read
|
|
46
|
+
* vertically (a list), on `x` horizontally (a strip), on `xy` both (a grid). A
|
|
47
|
+
* disabled item (`aria-disabled="true"`, `data-disabled`, `:disabled`) is never
|
|
48
|
+
* highlighted. An item reached by the keyboard (`:focus-visible`) is
|
|
49
|
+
* highlighted too, and with `focused`, the item that selector finds from a
|
|
50
|
+
* focused element that is no item (a tree's row, from its focused
|
|
51
|
+
* `treeitem`, which also holds the rows beneath it); a touch highlights
|
|
52
|
+
* nothing, since there is no pointer to
|
|
53
|
+
* follow before the press.
|
|
54
|
+
*
|
|
55
|
+
* The container gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, relative
|
|
56
|
+
* to its scroll box like `Measure`'s, and `-shown` and `-moving` (see
|
|
57
|
+
* `fluidHovered`); a view draws the highlight as an absolutely placed child
|
|
58
|
+
* that reads them. With `gapClick`, a click in a gap presses the highlighted
|
|
59
|
+
* item: its descendant marked `data-fluid-press`, or the item itself; with
|
|
60
|
+
* `gapReach` too, only a click within that many pixels of it, as a grid of
|
|
61
|
+
* cards with wide gutters wants (Fluid Functionalism's uses 16). A popup
|
|
62
|
+
* measured before it has a layout box keeps its last complete place, and
|
|
63
|
+
* tries again for three frames, rather than drawing the highlight at zero.
|
|
64
|
+
* The item the highlight is shown on carries `data-fluid-lit`, so a style
|
|
65
|
+
* can change what sits beside it (a table hides the lines around the lit
|
|
66
|
+
* row); its container carries `data-fluid-held` while that item is pressed.
|
|
67
|
+
*
|
|
68
|
+
* `follows` names the item the highlight sits on while the pointer is not
|
|
69
|
+
* moving it: a menu marks the row its keys reach (`[data-active]`), and the
|
|
70
|
+
* highlight goes there whenever that mark moves, so the keys and the pointer
|
|
71
|
+
* light the same one. `from` names the item a fresh highlight grows out of,
|
|
72
|
+
* as a tab's hover leaves the chosen tab. A highlight always fades where it
|
|
73
|
+
* is: once the pointer leaves, nothing should move. It stays only for an item
|
|
74
|
+
* the keyboard reached (`:focus-visible`), not one a click focused.
|
|
75
|
+
*/
|
|
76
|
+
export declare const FluidHover: Mount.MountDefinitionWithArgs<"FluidHover", {
|
|
77
|
+
/** The highlight's name, which names the custom properties. */
|
|
78
|
+
name: Schema.String;
|
|
79
|
+
/** The selector of an item within the container. */
|
|
80
|
+
item: Schema.String;
|
|
81
|
+
axis: Schema.Literals<readonly ["x", "y", "xy"]>;
|
|
82
|
+
gapClick: Schema.Boolean;
|
|
83
|
+
/** How far from the highlighted item, in pixels, a click in a gap still presses it. */
|
|
84
|
+
gapReach: Schema.optional<Schema.Number>;
|
|
85
|
+
/** The selector of the item the highlight sits on while the pointer is not moving it. */
|
|
86
|
+
follows: Schema.optional<Schema.String>;
|
|
87
|
+
/** The selector of the item a fresh highlight grows out of. */
|
|
88
|
+
from: Schema.optional<Schema.String>;
|
|
89
|
+
/** The selector, run from a focused element outside any item, of the item it lights. */
|
|
90
|
+
focused: Schema.optional<Schema.String>;
|
|
91
|
+
}, never>;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=fluid-hover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fluid-hover.d.ts","names":[],"sources":["../../src/dom/fluid-hover.ts"],"mappings":";;;qBAIa,eAAY;WAErB;WACA;WACA;WACA;;WAEA;;;;;;WAMA;;;;;;;qBAQS;;qBAGA;;qBAGA;;;;;;;;qBASA;cAEP,MAAI,OAAA;YACE,mBAAmB,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAyDvB,YAAU,MAAA"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/dom/fluid-hover.ts
|
|
4
|
+
/** The custom properties `FluidHover` writes on its container for a highlight named `name`. */
|
|
5
|
+
const fluidHovered = (name) => ({
|
|
6
|
+
x: `--fk-${name}-x`,
|
|
7
|
+
y: `--fk-${name}-y`,
|
|
8
|
+
w: `--fk-${name}-w`,
|
|
9
|
+
h: `--fk-${name}-h`,
|
|
10
|
+
/** `1` while an item is highlighted, `0` once the pointer and focus have left. */
|
|
11
|
+
shown: `--fk-${name}-shown`,
|
|
12
|
+
/**
|
|
13
|
+
* `0` for the frame the highlight appears in, then `1`: a highlight's
|
|
14
|
+
* movement scales its duration by it, so it appears on its item rather
|
|
15
|
+
* than sliding in from where it last was.
|
|
16
|
+
*/
|
|
17
|
+
moving: `--fk-${name}-moving`
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* The attribute `FluidHover` sets on its container, to the highlight's name,
|
|
21
|
+
* while it runs: a stylist draws the highlight, and drops each item's own
|
|
22
|
+
* hover, only under it.
|
|
23
|
+
*/
|
|
24
|
+
const FLUID_HOVER_ATTRIBUTE = "data-fluid-hover";
|
|
25
|
+
/** The attribute that names, inside an item, the control a press in a gap reaches. */
|
|
26
|
+
const FLUID_PRESS_ATTRIBUTE = "data-fluid-press";
|
|
27
|
+
/** The attribute on the item the highlight is shown on, for what CSS draws beside it. */
|
|
28
|
+
const FLUID_LIT_ATTRIBUTE = "data-fluid-lit";
|
|
29
|
+
/**
|
|
30
|
+
* The attribute `FluidHover` sets on its container, to the highlight's name,
|
|
31
|
+
* while the item that highlight is on is held. A stylist draws the press on
|
|
32
|
+
* the highlight (`data-fluid-held`) rather than on the item, whose own press
|
|
33
|
+
* is hidden under it; a press in a gap marks it too where `gapClick` sends
|
|
34
|
+
* that press to the lit item.
|
|
35
|
+
*/
|
|
36
|
+
const FLUID_HELD_ATTRIBUTE = "data-fluid-held";
|
|
37
|
+
const Axis = Schema.Literals([
|
|
38
|
+
"x",
|
|
39
|
+
"y",
|
|
40
|
+
"xy"
|
|
41
|
+
]);
|
|
42
|
+
/** How far a point is from a rectangle along each axis: zero inside it. */
|
|
43
|
+
const gap = (low, high, at) => Math.max(low - at, 0, at - high);
|
|
44
|
+
const away = {
|
|
45
|
+
x: (dx, dy) => dx * 1e6 + dy,
|
|
46
|
+
y: (dx, dy) => dy * 1e6 + dx,
|
|
47
|
+
xy: Math.hypot
|
|
48
|
+
};
|
|
49
|
+
const distance = (rect, x, y, axis) => away[axis](gap(rect.left, rect.right, x), gap(rect.top, rect.bottom, y));
|
|
50
|
+
const isDisabled = (item) => item.matches("[aria-disabled=\"true\"], [data-disabled]:not([data-disabled=\"false\"]), :disabled");
|
|
51
|
+
/**
|
|
52
|
+
* One highlight that follows the pointer across a container's items, as a
|
|
53
|
+
* Mount, with no Message and no Model: which item the pointer is nearest is
|
|
54
|
+
* presentation, known to the DOM and to no transition.
|
|
55
|
+
*
|
|
56
|
+
* The item nearest the pointer is the one highlighted, so the pointer in a gap
|
|
57
|
+
* between two items still points at one, and the highlight moves from item to
|
|
58
|
+
* item instead of blinking off between them. On the `y` axis nearness is read
|
|
59
|
+
* vertically (a list), on `x` horizontally (a strip), on `xy` both (a grid). A
|
|
60
|
+
* disabled item (`aria-disabled="true"`, `data-disabled`, `:disabled`) is never
|
|
61
|
+
* highlighted. An item reached by the keyboard (`:focus-visible`) is
|
|
62
|
+
* highlighted too, and with `focused`, the item that selector finds from a
|
|
63
|
+
* focused element that is no item (a tree's row, from its focused
|
|
64
|
+
* `treeitem`, which also holds the rows beneath it); a touch highlights
|
|
65
|
+
* nothing, since there is no pointer to
|
|
66
|
+
* follow before the press.
|
|
67
|
+
*
|
|
68
|
+
* The container gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, relative
|
|
69
|
+
* to its scroll box like `Measure`'s, and `-shown` and `-moving` (see
|
|
70
|
+
* `fluidHovered`); a view draws the highlight as an absolutely placed child
|
|
71
|
+
* that reads them. With `gapClick`, a click in a gap presses the highlighted
|
|
72
|
+
* item: its descendant marked `data-fluid-press`, or the item itself; with
|
|
73
|
+
* `gapReach` too, only a click within that many pixels of it, as a grid of
|
|
74
|
+
* cards with wide gutters wants (Fluid Functionalism's uses 16). A popup
|
|
75
|
+
* measured before it has a layout box keeps its last complete place, and
|
|
76
|
+
* tries again for three frames, rather than drawing the highlight at zero.
|
|
77
|
+
* The item the highlight is shown on carries `data-fluid-lit`, so a style
|
|
78
|
+
* can change what sits beside it (a table hides the lines around the lit
|
|
79
|
+
* row); its container carries `data-fluid-held` while that item is pressed.
|
|
80
|
+
*
|
|
81
|
+
* `follows` names the item the highlight sits on while the pointer is not
|
|
82
|
+
* moving it: a menu marks the row its keys reach (`[data-active]`), and the
|
|
83
|
+
* highlight goes there whenever that mark moves, so the keys and the pointer
|
|
84
|
+
* light the same one. `from` names the item a fresh highlight grows out of,
|
|
85
|
+
* as a tab's hover leaves the chosen tab. A highlight always fades where it
|
|
86
|
+
* is: once the pointer leaves, nothing should move. It stays only for an item
|
|
87
|
+
* the keyboard reached (`:focus-visible`), not one a click focused.
|
|
88
|
+
*/
|
|
89
|
+
const FluidHover = Mount.defineStream("FluidHover", {
|
|
90
|
+
messages: [Schema.Never],
|
|
91
|
+
args: {
|
|
92
|
+
/** The highlight's name, which names the custom properties. */
|
|
93
|
+
name: Schema.String,
|
|
94
|
+
/** The selector of an item within the container. */
|
|
95
|
+
item: Schema.String,
|
|
96
|
+
axis: Axis,
|
|
97
|
+
gapClick: Schema.Boolean,
|
|
98
|
+
/** How far from the highlighted item, in pixels, a click in a gap still presses it. */
|
|
99
|
+
gapReach: Schema.optional(Schema.Number),
|
|
100
|
+
/** The selector of the item the highlight sits on while the pointer is not moving it. */
|
|
101
|
+
follows: Schema.optional(Schema.String),
|
|
102
|
+
/** The selector of the item a fresh highlight grows out of. */
|
|
103
|
+
from: Schema.optional(Schema.String),
|
|
104
|
+
/** The selector, run from a focused element outside any item, of the item it lights. */
|
|
105
|
+
focused: Schema.optional(Schema.String)
|
|
106
|
+
},
|
|
107
|
+
execute: ({ element, name, item: selector, axis, gapClick, gapReach, follows, from, focused }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
108
|
+
const container = element;
|
|
109
|
+
const property = fluidHovered(name);
|
|
110
|
+
let active = null;
|
|
111
|
+
let followed = null;
|
|
112
|
+
let pointer = null;
|
|
113
|
+
let frame;
|
|
114
|
+
const origin = () => from === void 0 ? null : container.querySelector(from);
|
|
115
|
+
let retries = 0;
|
|
116
|
+
const place = (at = active) => {
|
|
117
|
+
if (at === null) return;
|
|
118
|
+
const box = container.getBoundingClientRect();
|
|
119
|
+
const rect = at.getBoundingClientRect();
|
|
120
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
121
|
+
if (retries < 3) {
|
|
122
|
+
retries += 1;
|
|
123
|
+
requestAnimationFrame(() => place(at));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
retries = 0;
|
|
128
|
+
const x = rect.left - box.left - container.clientLeft + container.scrollLeft;
|
|
129
|
+
const y = rect.top - box.top - container.clientTop + container.scrollTop;
|
|
130
|
+
container.style.setProperty(property.x, `${x}px`);
|
|
131
|
+
container.style.setProperty(property.y, `${y}px`);
|
|
132
|
+
container.style.setProperty(property.w, `${rect.width}px`);
|
|
133
|
+
container.style.setProperty(property.h, `${rect.height}px`);
|
|
134
|
+
};
|
|
135
|
+
/** Marks `item` as the lit one, and nothing else. */
|
|
136
|
+
const light = (item) => {
|
|
137
|
+
container.querySelectorAll(`[${FLUID_LIT_ATTRIBUTE}]`).forEach((each) => each !== item && each.removeAttribute("data-fluid-lit"));
|
|
138
|
+
item?.setAttribute(FLUID_LIT_ATTRIBUTE, "");
|
|
139
|
+
};
|
|
140
|
+
const show = (next) => {
|
|
141
|
+
const appearing = container.style.getPropertyValue(property.shown) !== "1";
|
|
142
|
+
active = next;
|
|
143
|
+
light(next);
|
|
144
|
+
const grows = appearing ? origin() : null;
|
|
145
|
+
if (appearing) container.style.setProperty(property.moving, "0");
|
|
146
|
+
if (grows === null) {
|
|
147
|
+
place();
|
|
148
|
+
container.style.setProperty(property.shown, "1");
|
|
149
|
+
} else {
|
|
150
|
+
place(grows);
|
|
151
|
+
requestAnimationFrame(() => {
|
|
152
|
+
if (active !== next) return;
|
|
153
|
+
place();
|
|
154
|
+
container.style.setProperty(property.shown, "1");
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (appearing) requestAnimationFrame(() => container.style.setProperty(property.moving, "1"));
|
|
158
|
+
};
|
|
159
|
+
const hide = () => {
|
|
160
|
+
container.style.setProperty(property.shown, "0");
|
|
161
|
+
container.removeAttribute(FLUID_HELD_ATTRIBUTE);
|
|
162
|
+
light(null);
|
|
163
|
+
};
|
|
164
|
+
/** Whether the keyboard has an item focused, which the highlight then stays on. */
|
|
165
|
+
const keyed = () => {
|
|
166
|
+
const focused = document.activeElement;
|
|
167
|
+
return focused !== null && container.contains(focused) && focused.matches(":focus-visible");
|
|
168
|
+
};
|
|
169
|
+
const nearest = (x, y) => {
|
|
170
|
+
let best = null;
|
|
171
|
+
let bestDistance = Infinity;
|
|
172
|
+
for (const each of Array.from(container.querySelectorAll(selector))) {
|
|
173
|
+
if (isDisabled(each)) continue;
|
|
174
|
+
const far = distance(each.getBoundingClientRect(), x, y, axis);
|
|
175
|
+
if (far < bestDistance) {
|
|
176
|
+
best = each;
|
|
177
|
+
bestDistance = far;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return best;
|
|
181
|
+
};
|
|
182
|
+
const follow = () => {
|
|
183
|
+
frame = void 0;
|
|
184
|
+
if (pointer === null) return;
|
|
185
|
+
const next = nearest(pointer.x, pointer.y);
|
|
186
|
+
if (next === null) hide();
|
|
187
|
+
else if (next !== active || container.style.getPropertyValue(property.shown) !== "1") show(next);
|
|
188
|
+
};
|
|
189
|
+
const move = (event) => {
|
|
190
|
+
if (event.pointerType === "touch") return;
|
|
191
|
+
pointer = {
|
|
192
|
+
x: event.clientX,
|
|
193
|
+
y: event.clientY
|
|
194
|
+
};
|
|
195
|
+
if (frame === void 0) frame = requestAnimationFrame(follow);
|
|
196
|
+
};
|
|
197
|
+
const leave = () => {
|
|
198
|
+
pointer = null;
|
|
199
|
+
release();
|
|
200
|
+
if (!keyed()) hide();
|
|
201
|
+
};
|
|
202
|
+
const focus = (event) => {
|
|
203
|
+
if (!(event.target instanceof Element) || !event.target.matches(":focus-visible")) return;
|
|
204
|
+
const owner = event.target.closest(selector) ?? (focused === void 0 ? null : event.target.querySelector(focused));
|
|
205
|
+
if (owner !== null && container.contains(owner) && !isDisabled(owner)) show(owner);
|
|
206
|
+
};
|
|
207
|
+
const blur = (event) => {
|
|
208
|
+
const to = event.relatedTarget;
|
|
209
|
+
if (pointer === null && !(to instanceof Node && container.contains(to))) hide();
|
|
210
|
+
};
|
|
211
|
+
const reachOf = (item, event) => gapReach === void 0 || distance(item.getBoundingClientRect(), event.clientX, event.clientY, "xy") <= gapReach;
|
|
212
|
+
/** Whether a click in a gap presses the lit item, within `gapReach` of it. */
|
|
213
|
+
const reaches = (item, event) => gapClick && reachOf(item, event);
|
|
214
|
+
const click = (event) => {
|
|
215
|
+
if (active === null) return;
|
|
216
|
+
if (container.style.getPropertyValue(property.shown) !== "1") return;
|
|
217
|
+
const target = event.target;
|
|
218
|
+
if (!(target instanceof Element) || target.closest(selector) !== null) return;
|
|
219
|
+
if (!reaches(active, event)) return;
|
|
220
|
+
const press = active.querySelector(`[data-fluid-press]`) ?? active;
|
|
221
|
+
if (press instanceof HTMLElement) press.click();
|
|
222
|
+
};
|
|
223
|
+
const soon = () => {
|
|
224
|
+
if (active !== null) requestAnimationFrame(() => place());
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Marks the container as held where the press is the lit item's: the
|
|
228
|
+
* item itself, or a gap a click there reaches through (`gapClick`). A
|
|
229
|
+
* press on another item is that item's, once the pointer has lit it.
|
|
230
|
+
*/
|
|
231
|
+
const press = (event) => {
|
|
232
|
+
if (event.button !== 0 || active === null) return;
|
|
233
|
+
if (container.style.getPropertyValue(property.shown) !== "1") return;
|
|
234
|
+
const target = event.target;
|
|
235
|
+
if (!(target instanceof Element)) return;
|
|
236
|
+
const inItem = target.closest(selector) !== null;
|
|
237
|
+
if (inItem && !active.contains(target)) return;
|
|
238
|
+
if (!inItem && !reaches(active, event)) return;
|
|
239
|
+
container.setAttribute(FLUID_HELD_ATTRIBUTE, name);
|
|
240
|
+
};
|
|
241
|
+
const release = () => container.removeAttribute(FLUID_HELD_ATTRIBUTE);
|
|
242
|
+
/** The marked item moved: the highlight goes with it, or fades if it is gone. */
|
|
243
|
+
const marked = () => {
|
|
244
|
+
if (follows === void 0) return;
|
|
245
|
+
const next = container.querySelector(follows);
|
|
246
|
+
if (next === followed) return;
|
|
247
|
+
followed = next;
|
|
248
|
+
if (next !== null && !isDisabled(next)) show(next);
|
|
249
|
+
else if (pointer === null && !keyed()) hide();
|
|
250
|
+
};
|
|
251
|
+
const sizes = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(soon);
|
|
252
|
+
const marks = follows === void 0 ? void 0 : new MutationObserver(marked);
|
|
253
|
+
sizes?.observe(container);
|
|
254
|
+
marks?.observe(container, {
|
|
255
|
+
subtree: true,
|
|
256
|
+
childList: true,
|
|
257
|
+
attributes: true
|
|
258
|
+
});
|
|
259
|
+
container.addEventListener("pointermove", move, { passive: true });
|
|
260
|
+
container.addEventListener("pointerdown", press);
|
|
261
|
+
container.addEventListener("pointerleave", leave);
|
|
262
|
+
window.addEventListener("pointerup", release);
|
|
263
|
+
window.addEventListener("pointercancel", release);
|
|
264
|
+
container.addEventListener("focusin", focus);
|
|
265
|
+
container.addEventListener("focusout", blur);
|
|
266
|
+
container.addEventListener("click", click);
|
|
267
|
+
container.addEventListener("scroll", soon, { passive: true });
|
|
268
|
+
window.addEventListener("resize", soon);
|
|
269
|
+
container.style.setProperty(property.shown, "0");
|
|
270
|
+
container.setAttribute(FLUID_HOVER_ATTRIBUTE, name);
|
|
271
|
+
marked();
|
|
272
|
+
return () => {
|
|
273
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
274
|
+
sizes?.disconnect();
|
|
275
|
+
marks?.disconnect();
|
|
276
|
+
container.removeAttribute(FLUID_HOVER_ATTRIBUTE);
|
|
277
|
+
container.removeAttribute(FLUID_HELD_ATTRIBUTE);
|
|
278
|
+
light(null);
|
|
279
|
+
container.removeEventListener("pointermove", move);
|
|
280
|
+
container.removeEventListener("pointerdown", press);
|
|
281
|
+
container.removeEventListener("pointerleave", leave);
|
|
282
|
+
window.removeEventListener("pointerup", release);
|
|
283
|
+
window.removeEventListener("pointercancel", release);
|
|
284
|
+
container.removeEventListener("focusin", focus);
|
|
285
|
+
container.removeEventListener("focusout", blur);
|
|
286
|
+
container.removeEventListener("click", click);
|
|
287
|
+
container.removeEventListener("scroll", soon);
|
|
288
|
+
window.removeEventListener("resize", soon);
|
|
289
|
+
};
|
|
290
|
+
}), (stop) => Effect.sync(stop)))
|
|
291
|
+
});
|
|
292
|
+
export { FLUID_HELD_ATTRIBUTE, FLUID_HOVER_ATTRIBUTE, FLUID_LIT_ATTRIBUTE, FLUID_PRESS_ATTRIBUTE, FluidHover, fluidHovered };
|
|
293
|
+
|
|
294
|
+
//# sourceMappingURL=fluid-hover.js.map
|