INIT
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
export { default as TextSelectionLayer } from "./text-selection-layer.svelte";
|
||||
export type { TextSelectionLayerProps } from "./types.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default as TextSelectionLayer } from "./text-selection-layer.svelte";
|
||||
Generated
Vendored
+26
@@ -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?.()}
|
||||
Generated
Vendored
+4
@@ -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;
|
||||
+33
@@ -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;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+14
@@ -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 {};
|
||||
Generated
Vendored
+79
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user