facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

Bug: Font size highlight removed on input #5690

Open dlarroder opened 2 months ago

dlarroder commented 2 months ago

Lexical version: v0.13.1

Steps To Reproduce

  1. Highlight a any text you want to resize
  2. Click the font size input (see that the current highlight gets removed)
  3. Change the input, e.g 50
  4. Press enter, the text still resizes even though the highlight is removed

https://github.com/facebook/lexical/assets/52998821/917fdfbd-4e9f-4acf-8e0e-6380f26d3df5

The current behavior

When trying to resize using the font size input, the highlight gets removed and still does the resizing on enter

The expected behavior

Maybe this is hard but can we keep the current highlight of the text if we have control of it when we change the font size via the input? It's the same behavior when we change the sizes via the +/- buttons

AliakseiPaseishviliSyntheticabio commented 2 weeks ago

I think this is important feature update. I am trying to implement saving of highlight on my own, but I am faced with bugs everytime I fix previous things: First of all $patchStyleText is doing focus on editor everytime it is called. I tried to do additional highlight on Editor blur, but it is just doing fucus back. Then I added $setSelection(null) to remove focus from editor, but in this case I can't do normal font-size change that is written in playground.

  const setHighlightData = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setHighlightDataCallback(selection);
      $patchStyleText(selection, { 'background-color': 'red' });
      $setSelection(null);
    }
  }, []);

 editor.registerCommand(
          BLUR_COMMAND,
          () => {
            setHighlightData();
            return false;
          },
          COMMAND_PRIORITY_LOW,
        );
AliakseiPaseishviliSyntheticabio commented 2 weeks ago

So, how I made it work closely to google-docs:

I added HighlightPlugin:

import { useLayoutEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical';

import { useHighlightPluginContext } from './context';

export const HighlightPlugin = () => {
  const [editor] = useLexicalComposerContext();
  const { setHighlightData, cleanHighlightData } = useHighlightPluginContext();

  useLayoutEffect(
    () =>
      mergeRegister(
        editor.registerCommand(
          FOCUS_COMMAND,
          () => {
            cleanHighlightData();
            return false;
          },
          COMMAND_PRIORITY_LOW,
        ),
        editor.registerCommand(
          BLUR_COMMAND,
          () => {
            setHighlightData();
            return false;
          },
          COMMAND_PRIORITY_LOW,
        ),
      ),
    [editor, cleanHighlightData, setHighlightData],
  );

  return null;
};

I added HighlightProvider with hook inside of it:

import { useCallback, useState } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $patchStyleText } from '@lexical/selection';
import {
  $getSelection,
  $isRangeSelection,
  $setSelection,
  RangeSelection,
} from 'lexical';

export const useHighlightData = () => {
  const [editor] = useLexicalComposerContext();
  const [highlightedData, setHighlightDataCallback] = useState<
    RangeSelection | undefined
  >();

  const setHighlightData = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setHighlightDataCallback(selection);
      $patchStyleText(selection, { 'background-color': '#ACCEF7' });
      $setSelection(null);
    }
  }, []);

  const cleanHighlightData = useCallback(() => {
    editor.update(() => {
      if (editor.isEditable() && highlightedData) {
        $patchStyleText(highlightedData.clone(), {
          'background-color': 'transparent',
        });
        setHighlightDataCallback(undefined);
      }
    });
  }, [editor, highlightedData]);

  return {
    highlightedData,
    setHighlightData,
    cleanHighlightData,
  };
};

After it I modified fontSize hook from playground:

import { KeyboardEvent, useCallback, useEffect, useState } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
  $getSelectionStyleValueForProperty,
  $patchStyleText,
} from '@lexical/selection';
import {
  $getSelection,
  $isRangeSelection,
  $setSelection,
  COMMAND_PRIORITY_CRITICAL,
  SELECTION_CHANGE_COMMAND,
} from 'lexical';

import {
  DEFAULT_FONT_SIZE,
  MAX_ALLOWED_FONT_SIZE,
  MIN_ALLOWED_FONT_SIZE,
  PX,
} from '../constants';
import { useHighlightPluginContext } from '../plugins/HighlightPlugin/context';
import { UPDATE_FONT_SIZE_TYPE } from '../types';
import { calculateNextFontSize } from '../utils/calculate-font-size';

const useFontSizeInLexical = (setFontSize: (value: string) => void) => {
  const [editor] = useLexicalComposerContext();

  const $updateFontSize = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      const currentFontSize = $getSelectionStyleValueForProperty(
        selection,
        'font-size',
        `${DEFAULT_FONT_SIZE}${PX}`,
      );

      setFontSize(currentFontSize.replaceAll(PX, ''));
    }
  }, [setFontSize]);

  useEffect(() => {
    return editor.registerCommand(
      SELECTION_CHANGE_COMMAND,
      () => {
        $updateFontSize();
        return false;
      },
      COMMAND_PRIORITY_CRITICAL,
    );
  }, [editor, $updateFontSize]);

  useEffect(() => {
    const registerListener = editor.registerUpdateListener(
      ({ editorState }) => {
        editorState.read(() => {
          $updateFontSize();
        });
      },
    );

    return () => {
      registerListener();
    };
  }, [$updateFontSize, editor]);
};

export const useFontSize = () => {
  const [editor] = useLexicalComposerContext();
  const [fontSize, setFontSize] = useState<string>(
    DEFAULT_FONT_SIZE.toString(),
  );
  const { highlightedData } = useHighlightPluginContext();

  useFontSizeInLexical(setFontSize);

  const updateFontSizeInSelection = useCallback(
    (newFontSize: string | null, updateType: UPDATE_FONT_SIZE_TYPE | null) => {
      const getNextFontSize = (prevFontSize: string | null): string => {
        if (!prevFontSize) {
          prevFontSize = `${DEFAULT_FONT_SIZE}${PX}`;
        }
        prevFontSize = prevFontSize.slice(0, -2);
        const nextFontSize = calculateNextFontSize(
          Number(prevFontSize),
          updateType,
        );
        return `${nextFontSize}${PX}`;
      };

      editor.update(() => {
        if (editor.isEditable()) {
          const oldSelection = highlightedData?.clone();

          if (oldSelection) {
            $patchStyleText(oldSelection, {
              'font-size': newFontSize || getNextFontSize,
            });
            $setSelection(oldSelection);
          }
        }
      });
    },
    [editor, highlightedData],
  );

  const handleKeyPress = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      const inputValueNumber = Number(fontSize);

      if (['e', 'E', '+', '-'].includes(e.key) || isNaN(inputValueNumber)) {
        e.preventDefault();
        setFontSize('');
        return;
      }

      if (e.key === 'Enter') {
        e.preventDefault();

        let updatedFontSize = inputValueNumber;
        if (inputValueNumber > MAX_ALLOWED_FONT_SIZE) {
          updatedFontSize = MAX_ALLOWED_FONT_SIZE;
        } else if (inputValueNumber < MIN_ALLOWED_FONT_SIZE) {
          updatedFontSize = MIN_ALLOWED_FONT_SIZE;
        }
        setFontSize(String(updatedFontSize));
        updateFontSizeInSelection(`${updatedFontSize}${PX}`, null);
      }
    },
    [fontSize, updateFontSizeInSelection],
  );

  const handleButtonClick = useCallback(
    (updateType: UPDATE_FONT_SIZE_TYPE) => {
      if (fontSize !== '') {
        const nextFontSize = calculateNextFontSize(
          Number(fontSize),
          updateType,
        );
        updateFontSizeInSelection(`${nextFontSize}${PX}`, null);
      } else {
        updateFontSizeInSelection(null, updateType);
      }
    },
    [fontSize, updateFontSizeInSelection],
  );

  return {
    handleButtonClick,
    handleKeyPress,
    fontSize,
    setFontSize,
  };
};

But I am facing with bug in historyPlugin, I can't do normal undo/redo functionality if I used highlight of code.

AliakseiPaseishviliSyntheticabio commented 2 weeks ago

https://github.com/facebook/lexical/assets/142044988/9e5fe3b5-9834-4873-a94d-82d3c269c0cf

So you can see that history doesn't work correct in video