Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*
*/

import {selectionAlwaysOnDisplay} from '@lexical/utils';
import {
type MarkSelectionColors,
selectionAlwaysOnDisplay,
} from '@lexical/utils';
import {defineExtension, safeCast} from 'lexical';

import {namedSignals} from './namedSignals';
Expand All @@ -15,6 +18,7 @@ import {effect} from './signals';
export interface SelectionAlwaysOnDisplayConfig {
disabled: boolean;
onReposition: undefined | ((node: readonly HTMLElement[]) => void);
colors: undefined | MarkSelectionColors;
}

/**
Expand All @@ -25,6 +29,7 @@ export const SelectionAlwaysOnDisplayExtension =
/* @__PURE__ */ defineExtension({
build: (editor, config, state) => namedSignals(config),
config: /* @__PURE__ */ safeCast<SelectionAlwaysOnDisplayConfig>({
colors: undefined,
disabled: false,
onReposition: undefined,
}),
Expand All @@ -33,7 +38,11 @@ export const SelectionAlwaysOnDisplayExtension =
const stores = state.getOutput();
return effect(() => {
if (!stores.disabled.value) {
return selectionAlwaysOnDisplay(editor, stores.onReposition.value);
return selectionAlwaysOnDisplay(
editor,
stores.onReposition.value,
stores.colors.value,
);
}
});
},
Expand Down
8 changes: 8 additions & 0 deletions packages/lexical-utils/flow/LexicalUtils.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,16 @@ declare export function $findMatchingParent(
findFn: (LexicalNode) => boolean,
): LexicalNode | null;
declare export function mergeRegister(...func: (() => void)[]): () => void;
export type MarkSelectionColors = {
background?: string,
};
declare export function createDefaultOnReposition(
colors?: MarkSelectionColors,
): (nodes: HTMLElement[]) => void;
declare export function markSelection(
editor: LexicalEditor,
onReposition?: (node: HTMLElement[]) => void,
colors?: MarkSelectionColors,
): () => void;
declare export function positionNodeOnRange(
editor: LexicalEditor,
Expand All @@ -98,6 +105,7 @@ declare export function positionNodeOnRange(
declare export function selectionAlwaysOnDisplay(
editor: LexicalEditor,
onReposition?: (node: HTMLElement[]) => void,
colors?: MarkSelectionColors,
): () => void;
declare export function $getNearestBlockElementAncestorOrThrow(
startNode: LexicalNode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {createDefaultOnReposition} from '@lexical/utils';
import {describe, expect, it} from 'vitest';

describe('createDefaultOnReposition', () => {
it('defaults the rect background to the system Highlight color', () => {
const defaultNode = document.createElement('div');
createDefaultOnReposition()([defaultNode]);
const explicitNode = document.createElement('div');
createDefaultOnReposition({background: 'Highlight'})([explicitNode]);
expect(defaultNode.style.background).toBe(explicitNode.style.background);
});

it('applies a custom background color when one is given', () => {
const node = document.createElement('div');
createDefaultOnReposition({background: 'rgb(0, 100, 200)'})([node]);
expect(node.style.background).toBe('rgb(0, 100, 200)');
});

it('keeps the margin and padding offsets regardless of color', () => {
const node = document.createElement('div');
createDefaultOnReposition({background: 'Highlight'})([node]);
expect(node.style.marginTop).toBe('-1.5px');
expect(node.style.paddingTop).toBe('4px');
expect(node.style.paddingBottom).toBe('0px');
});
});
6 changes: 5 additions & 1 deletion packages/lexical-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ import {
ValueOrUpdater,
} from 'lexical';

export {default as markSelection} from './markSelection';
export {
createDefaultOnReposition,
default as markSelection,
type MarkSelectionColors,
} from './markSelection';
export {default as positionNodeOnRange} from './positionNodeOnRange';
export {default as selectionAlwaysOnDisplay} from './selectionAlwaysOnDisplay';
export {
Expand Down
66 changes: 42 additions & 24 deletions packages/lexical-utils/src/markSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,46 @@ function $rangeFromPoints(
return range;
}

function defaultOnReposition(domNodes: readonly HTMLElement[]): void {
for (const domNode of domNodes) {
const domNodeStyle = domNode.style;
export interface MarkSelectionColors {
/**
* CSS color for the selection rect background. Defaults to the system
* `'Highlight'` color, which preserves the previous behavior.
*/
background?: string;
}

if (domNodeStyle.background !== 'Highlight') {
domNodeStyle.background = 'Highlight';
}
if (domNodeStyle.color !== 'HighlightText') {
domNodeStyle.color = 'HighlightText';
}
if (domNodeStyle.marginTop !== px(-1.5)) {
domNodeStyle.marginTop = px(-1.5);
}
if (domNodeStyle.paddingTop !== px(4)) {
domNodeStyle.paddingTop = px(4);
}
if (domNodeStyle.paddingBottom !== px(0)) {
domNodeStyle.paddingBottom = px(0);
/**
* Build the default `onReposition` styler used by {@link markSelection},
* optionally overriding the selection rect color. Exposed so a consumer that
* passes its own `onReposition` can reuse the default rect styling with a
* different color, instead of re-deriving the margin and padding offsets.
*/
export function createDefaultOnReposition(
colors?: MarkSelectionColors,
): (domNodes: readonly HTMLElement[]) => void {
const background =
colors && colors.background != null ? colors.background : 'Highlight';
return domNodes => {
for (const domNode of domNodes) {
const domNodeStyle = domNode.style;

if (domNodeStyle.background !== background) {
domNodeStyle.background = background;
}
if (domNodeStyle.color !== 'HighlightText') {
domNodeStyle.color = 'HighlightText';
}
if (domNodeStyle.marginTop !== px(-1.5)) {
domNodeStyle.marginTop = px(-1.5);
}
if (domNodeStyle.paddingTop !== px(4)) {
domNodeStyle.paddingTop = px(4);
}
if (domNodeStyle.paddingBottom !== px(0)) {
domNodeStyle.paddingBottom = px(0);
}
}
}
};
}

/**
Expand All @@ -97,8 +117,10 @@ function defaultOnReposition(domNodes: readonly HTMLElement[]): void {
*/
export default function markSelection(
editor: LexicalEditor,
onReposition: (node: readonly HTMLElement[]) => void = defaultOnReposition,
onReposition?: (node: readonly HTMLElement[]) => void,
colors?: MarkSelectionColors,
): () => void {
const reposition = onReposition ?? createDefaultOnReposition(colors);
let previousAnchorNode: null | TextNode | ElementNode = null;
let previousAnchorNodeDOM: null | HTMLElement = null;
let previousAnchorOffset: null | number = null;
Expand Down Expand Up @@ -154,11 +176,7 @@ export default function markSelection(
currentEndNodeDOM,
);
removeRangeListener();
removeRangeListener = positionNodeOnRange(
editor,
range,
onReposition,
);
removeRangeListener = positionNodeOnRange(editor, range, reposition);
}
previousAnchorNode = currentStartNode;
previousAnchorNodeDOM = currentStartNodeDOM;
Expand Down
5 changes: 3 additions & 2 deletions packages/lexical-utils/src/selectionAlwaysOnDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

import {LexicalEditor} from 'lexical';

import markSelection from './markSelection';
import markSelection, {type MarkSelectionColors} from './markSelection';

export default function selectionAlwaysOnDisplay(
editor: LexicalEditor,
onReposition?: (node: readonly HTMLElement[]) => void,
colors?: MarkSelectionColors,
): () => void {
let removeSelectionMark: (() => void) | null = null;

Expand All @@ -33,7 +34,7 @@ export default function selectionAlwaysOnDisplay(
}
} else {
if (removeSelectionMark === null) {
removeSelectionMark = markSelection(editor, onReposition);
removeSelectionMark = markSelection(editor, onReposition, colors);
}
}
};
Expand Down