This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
@@ -0,0 +1,2 @@
export { default as TextSelectionLayer } from "./text-selection-layer.svelte";
export type { TextSelectionLayerProps } from "./types.js";
@@ -0,0 +1 @@
export { default as TextSelectionLayer } from "./text-selection-layer.svelte";
@@ -0,0 +1,26 @@
<script lang="ts">
import { boxWith } from "svelte-toolbelt";
import type { TextSelectionLayerImplProps } from "./types.js";
import { TextSelectionLayerState } from "./use-text-selection-layer.svelte.js";
import { noop } from "../../../internal/noop.js";
let {
preventOverflowTextSelection = true,
onPointerDown = noop,
onPointerUp = noop,
id,
children,
enabled,
ref,
}: TextSelectionLayerImplProps = $props();
TextSelectionLayerState.create({
id: boxWith(() => id),
onPointerDown: boxWith(() => onPointerDown),
onPointerUp: boxWith(() => onPointerUp),
enabled: boxWith(() => enabled && preventOverflowTextSelection),
ref,
});
</script>
{@render children?.()}
@@ -0,0 +1,4 @@
import type { TextSelectionLayerImplProps } from "./types.js";
declare const TextSelectionLayer: import("svelte").Component<TextSelectionLayerImplProps, {}, "">;
type TextSelectionLayer = ReturnType<typeof TextSelectionLayer>;
export default TextSelectionLayer;
@@ -0,0 +1,33 @@
import type { Snippet } from "svelte";
import type { ReadableBox } from "svelte-toolbelt";
export type PointerHandler = (e: PointerEvent) => void;
export type TextSelectionLayerProps = {
/**
* Passing `true` will prevent the overflow of text selection
* outside the element, provided the element is the top layer.
* @defaultValue `true`
*/
preventOverflowTextSelection?: boolean;
};
export type TextSelectionLayerImplProps = {
/**
* Whether the layer is enabled. Currently, we determine this with the
* `presence` returned from the `presence` layer.
*/
enabled: boolean;
/**
* DOM ID of the node.
*/
id: string;
children?: Snippet;
/**
* Callback fired when pointerdown event triggers. Call `preventDefault()`
* to disable the prevention of text-selection overflow.
*/
onPointerDown?: PointerHandler;
/**
* Callback fired when pointerup event triggers.
*/
onPointerUp?: PointerHandler;
ref: ReadableBox<HTMLElement | null>;
} & TextSelectionLayerProps;
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,14 @@
import { DOMContext, type ReadableBoxedValues } from "svelte-toolbelt";
import type { TextSelectionLayerImplProps } from "./types.js";
interface TextSelectionLayerStateOpts extends ReadableBoxedValues<Required<Omit<TextSelectionLayerImplProps, "children" | "preventOverflowTextSelection" | "ref">> & {
ref: HTMLElement | null;
}> {
}
export declare class TextSelectionLayerState {
#private;
static create(opts: TextSelectionLayerStateOpts): TextSelectionLayerState;
readonly opts: TextSelectionLayerStateOpts;
readonly domContext: DOMContext;
constructor(opts: TextSelectionLayerStateOpts);
}
export {};
@@ -0,0 +1,79 @@
import { DOMContext, composeHandlers, contains, executeCallbacks, } from "svelte-toolbelt";
import { watch } from "runed";
import { on } from "svelte/events";
import { noop } from "../../../internal/noop.js";
import { isHTMLElement } from "../../../internal/is.js";
globalThis.bitsTextSelectionLayers ??= new Map();
export class TextSelectionLayerState {
static create(opts) {
return new TextSelectionLayerState(opts);
}
opts;
domContext;
#unsubSelectionLock = noop;
constructor(opts) {
this.opts = opts;
this.domContext = new DOMContext(opts.ref);
let unsubEvents = noop;
watch(() => this.opts.enabled.current, (isEnabled) => {
if (isEnabled) {
globalThis.bitsTextSelectionLayers.set(this, this.opts.enabled);
unsubEvents();
unsubEvents = this.#addEventListeners();
}
return () => {
unsubEvents();
this.#resetSelectionLock();
globalThis.bitsTextSelectionLayers.delete(this);
};
});
}
#addEventListeners() {
return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.#pointerdown), on(this.domContext.getDocument(), "pointerup", composeHandlers(this.#resetSelectionLock, this.opts.onPointerUp.current)));
}
#pointerdown = (e) => {
const node = this.opts.ref.current;
const target = e.target;
if (!isHTMLElement(node) || !isHTMLElement(target) || !this.opts.enabled.current)
return;
/**
* We only lock user-selection overflow if layer is the top most layer and
* pointerdown occurred inside the node. You are still allowed to select text
* outside the node provided pointerdown occurs outside the node.
*/
if (!isHighestLayer(this) || !contains(node, target))
return;
this.opts.onPointerDown.current(e);
if (e.defaultPrevented)
return;
this.#unsubSelectionLock = preventTextSelectionOverflow(node, this.domContext.getDocument().body);
};
#resetSelectionLock = () => {
this.#unsubSelectionLock();
this.#unsubSelectionLock = noop;
};
}
const getUserSelect = (node) => node.style.userSelect || node.style.webkitUserSelect;
function preventTextSelectionOverflow(node, body) {
const originalBodyUserSelect = getUserSelect(body);
const originalNodeUserSelect = getUserSelect(node);
setUserSelect(body, "none");
setUserSelect(node, "text");
return () => {
setUserSelect(body, originalBodyUserSelect);
setUserSelect(node, originalNodeUserSelect);
};
}
function setUserSelect(node, value) {
node.style.userSelect = value;
node.style.webkitUserSelect = value;
}
function isHighestLayer(instance) {
const layersArr = [...globalThis.bitsTextSelectionLayers];
if (!layersArr.length)
return false;
const highestLayer = layersArr.at(-1);
if (!highestLayer)
return false;
return highestLayer[0] === instance;
}