facebook / facebook/lexical

Bug: $getSelectionStyleValueForProperty(selection, 'font-size', '16px') doesn't work as expected

Open
#4,011 3 comments 0 reactions 0 assignees View on GitHub
all-platforms-bug core
Dominant language
TypeScript
Stars
23.9k
Forks
2.2k
Avg merge
1d 14h
Merged PRs (30d)
55

Description

Lexical version: 0.8.0

## Steps To Reproduce

1. Copy Markdown plugin coding
2. Integrate a font-size change from the playground to the Toolbarplugin
3. Change the font-size, $getSelectionStyleValueForProperty(selection, 'font-size', '16px') do not return the correct value of the actual font-size, but the font-size is changed correctly, so I just can not update the toolbar ui correctly

Link to code example:

> import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
> import { useCallback, useEffect, useRef, useState } from "react";
> import {
> SELECTION_CHANGE_COMMAND,
> FORMAT_TEXT_COMMAND,
> $getSelection,
> $isRangeSelection,
> $createParagraphNode,
> COMMAND_PRIORITY_CRITICAL,
> } from "lexical";
> import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
> import {
> $wrapNodes,
> $isAtNodeEnd,
> $getSelectionStyleValueForProperty,
> $patchStyleText
> } from "@lexical/selection";
> import { $getNearestNodeOfType, mergeRegister, $findMatchingParent } from "@lexical/utils";
> import {
> INSERT_ORDERED_LIST_COMMAND,
> INSERT_UNORDERED_LIST_COMMAND,
> REMOVE_LIST_COMMAND,
> $isListNode,
> ListNode
> } from "@lexical/list";
> import { createPortal } from "react-dom";
> import {
> $createHeadingNode,
> $createQuoteNode,
> $isHeadingNode
> } from "@lexical/rich-text";
> import {
> $createCodeNode,
> $isCodeNode,
> getDefaultCodeLanguage,
> getCodeLanguages
> } from "@lexical/code";
> import DropDown, { DropDownItem } from "@/Components/DropDown";
> import ColorPicker from "@/Components/ColorPicker";
>
> const LowPriority = 1;
>
> const supportedBlockTypes = new Set([
> "paragraph",
> "quote",
> "h1",
> "h2",
> "h3",
> "h4",
> "h5",
> "h6",
> "ul",
> "ol"
> ]);
>
> const blockTypeToBlockName = {
> h1: "40px Heading",
> h2: "30px Heading",
> h3: "23.4px Heading",
> h4: "20px Heading",
> h5: "16.6px Heading",
> h6: "13.4px Heading",
> ol: "Numbered List",
> paragraph: "Normal",
> quote: "Quote",
> ul: "Bulleted List"
> };
>
> const FONT_FAMILY_OPTIONS = [
> ['Arial', 'Arial'],
> ['Courier New', 'Courier New'],
> ['Georgia', 'Georgia'],
> ['Times New Roman', 'Times New Roman'],
> ['Trebuchet MS', 'Trebuchet MS'],
> ['Verdana', 'Verdana'],
> ];
>
> const FONT_SIZE_OPTIONS = [
> ['10px', '10px'],
> ['12px', '12px'],
> ['14px', '14px'],
> ['16px', '16px'],
> ['18px', '18px'],
> ['20px', '20px'],
> ['22px', '22px'],
> ['24px', '24px'],
> ['26px', '26px'],
> ['28px', '28px'],
> ['30px', '30px'],
> ];
>
> function Divider() {
> return

;
> }
>
> function positionEditorElement(editor, rect) {
> if (rect === null) {
> editor.style.opacity = "0";
> editor.style.top = "-1000px";
> editor.style.left = "-1000px";
> } else {
> editor.style.opacity = "1";
> editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
> editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
> }px`;
> }
> }
>
> function FloatingLinkEditor({ editor }) {
> const editorRef = useRef(null);
> const inputRef = useRef(null);
> const mouseDownRef = useRef(false);
> const [linkUrl, setLinkUrl] = useState("");
> const [isEditMode, setEditMode] = useState(false);
> const [lastSelection, setLastSelection] = useState(null);
>
> const updateLinkEditor = useCallback(() => {
> const selection = $getSelection();
> if ($isRangeSelection(selection)) {
> const node = getSelectedNode(selection);
> const parent = node.getParent();
> if ($isLinkNode(parent)) {
> setLinkUrl(parent.getURL());
> } else if ($isLinkNode(node)) {
> setLinkUrl(node.getURL());
> } else {
> setLinkUrl("");
> }
> }
> const editorElem = editorRef.current;
> const nativeSelection = window.getSelection();
> const activeElement = document.activeElement;
>
> if (editorElem === null) {
> return;
> }
>
> const rootElement = editor.getRootElement();
> if (
> selection !== null &&
> !nativeSelection.isCollapsed &&
> rootElement !== null &&
> rootElement.contains(nativeSelection.anchorNode)
> ) {
> const domRange = nativeSelection.getRangeAt(0);
> let rect;
> if (nativeSelection.anchorNode === rootElement) {
> let inner = rootElement;
> while (inner.firstElementChild != null) {
> inner = inner.firstElementChild;
> }
> rect = inner.getBoundingClientRect();
> } else {
> rect = domRange.getBoundingClientRect();
> }
>
> if (!mouseDownRef.current) {
> positionEditorElement(editorElem, rect);
> }
> setLastSelection(selection);
> } else if (!activeElement || activeElement.className !== "link-input") {
> positionEditorElement(editorElem, null);
> setLastSelection(null);
> setEditMode(false);
> setLinkUrl("");
> }
>
> return true;
> }, [editor]);
>
> useEffect(() => {
> return mergeRegister(
> editor.registerUpdateListener(({ editorState }) => {
> editorState.read(() => {
> updateLinkEditor();
> });
> }),
>
> editor.registerCommand(
> SELECTION_CHANGE_COMMAND,
> () => {
> updateLinkEditor();
> return true;
> },
> LowPriority
> )
> );
> }, [editor, updateLinkEditor]);
>
> useEffect(() => {
> editor.getEditorState().read(() => {
> updateLinkEditor();
> });
> }, [editor, updateLinkEditor]);
>
> useEffect(() => {
> if (isEditMode && inputRef.current) {
> inputRef.current.focus();
> }
> }, [isEditMode]);
>
> return (
>

> {isEditMode ? (
> ref={inputRef}
> className="link-input"
> value={linkUrl}
> onChange={(event) => {
> setLinkUrl(event.target.value);
> }}
> onKeyDown={(event) => {
> if (event.key === "Enter") {
> event.preventDefault();
> if (lastSelection !== null) {
> if (linkUrl !== "") {
> editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
> }
> setEditMode(false);
> }
> } else if (event.key === "Escape") {
> event.preventDefault();
> setEditMode(false);
> }
> }}
> />
> ) : (
> <>
>

>
> {linkUrl}
>

>
className="link-edit"
> role="button"
> tabIndex={0}
> onMouseDown={(event) => event.preventDefault()}
> onClick={() => {
> setEditMode(true);
> }}
> />
>

>
> )}
>

> );
> }
>
> function dropDownActiveClass(active) {
> if (active) return 'active dropdown-item-active';
> else return '';
> }
>
> function getSelectedNode(selection) {
> const anchor = selection.anchor;
> const focus = selection.focus;
> const anchorNode = selection.anchor.getNode();
> const focusNode = selection.focus.getNode();
> if (anchorNode === focusNode) {
> return anchorNode;
> }
> const isBackward = selection.isBackward();
> if (isBackward) {
> return $isAtNodeEnd(focus) ? anchorNode : focusNode;
> } else {
> return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
> }
> }
>
> function BlockOptionsDropdownList({
> editor,
> blockType,
> toolbarRef,
> setShowBlockOptionsDropDown
> }) {
> const dropDownRef = useRef(null);
>
> useEffect(() => {
> const toolbar = toolbarRef.current;
> const dropDown = dropDownRef.current;
>
> if (toolbar !== null && dropDown !== null) {
> const { top, left } = toolbar.getBoundingClientRect();
> dropDown.style.top = `${top + 40}px`;
> dropDown.style.left = `${left}px`;
> }
> }, [dropDownRef, toolbarRef]);
>
> useEffect(() => {
> const dropDown = dropDownRef.current;
> const toolbar = toolbarRef.current;
>
> if (dropDown !== null && toolbar !== null) {
> const handle = (event) => {
> const target = event.target;
>
> if (!dropDown.contains(target) && !toolbar.contains(target)) {
> setShowBlockOptionsDropDown(false);
> }
> };
> document.addEventListener("click", handle);
>
> return () => {
> document.removeEventListener("click", handle);
> };
> }
> }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
>
> const formatParagraph = () => {
> if (blockType !== "paragraph") {
> editor.update(() => {
> const selection = $getSelection();
>
> if ($isRangeSelection(selection)) {
> $wrapNodes(selection, () => $createParagraphNode());
> }
> });
> }
> setShowBlockOptionsDropDown(false);
> };
>
> const formatHeading = (size) => {
> if (blockType !== size) {
> editor.update(() => {
> const selection = $getSelection();
>
> if ($isRangeSelection(selection)) {
> $wrapNodes(selection, () => $createHeadingNode(size));
> }
> });
> }
> setShowBlockOptionsDropDown(false);
> };
>
> /*
> const formatSmallHeading = () => {
> if (blockType !== "h2") {
> editor.update(() => {
> const selection = $getSelection();
>
> if ($isRangeSelection(selection)) {
> $wrapNodes(selection, () => $createHeadingNode("h2"));
> }
> });
> }
> setShowBlockOptionsDropDown(false);
> };
> */
>
> const formatBulletList = () => {
> if (blockType !== "ul") {
> editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
> } else {
> editor.dispatchCommand(REMOVE_LIST_COMMAND);
> }
> setShowBlockOptionsDropDown(false);
> };
>
> const formatNumberedList = () => {
> if (blockType !== "ol") {
> editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
> } else {
> editor.dispatchCommand(REMOVE_LIST_COMMAND);
> }
> setShowBlockOptionsDropDown(false);
> };
>
> const formatQuote = () => {
> if (blockType !== "quote") {
> editor.update(() => {
> const selection = $getSelection();
>
> if ($isRangeSelection(selection)) {
> $wrapNodes(selection, () => $createQuoteNode());
> }
> });
> }
> setShowBlockOptionsDropDown(false);
> };
>
> return (
>

>
>
> Normal
> {blockType === "paragraph" && }
>

> formatHeading("h1")}>
>
> 40px Heading
> {blockType === "h1" && }
>

> formatHeading("h2")}>
>
> 30px Heading
> {blockType === "h2" && }
>

> formatHeading("h3")}>
>
> 23.4px Heading
> {blockType === "h3" && }
>

> formatHeading("h4")}>
>
> 20px Heading
> {blockType === "h4" && }
>

> formatHeading("h5")}>
>
> 16.6px Heading
> {blockType === "h5" && }
>

> formatHeading("h6")}>
>
> 13.4px Heading
> {blockType === "h6" && }
>

>
>
> Bullet List
> {blockType === "ul" && }
>

>
>
> Numbered List
> {blockType === "ol" && }
>

>
>
> Quote
> {blockType === "quote" && }
>

>

> );
> }
>
> function FontDropDown({
> editor,
> value,
> style,
> disabled = false,
> }) {
> const handleClick = useCallback(
> (option) => {
> editor.update(() => {
> const selection = $getSelection();
> if ($isRangeSelection(selection)) {
> $patchStyleText(selection, {
> [style]: option,
> });
> }
> });
> },
> [editor, style],
> );
>
> const buttonAriaLabel =
> style === 'font-family'
> ? 'Formatting options for font family'
> : 'Formatting options for font size';
>
> return (
> disabled={disabled}
> buttonClassName={'toolbar-item ' + style}
> buttonLabel={value}
> buttonAriaLabel={buttonAriaLabel}>
> {(style === 'font-family' ? FONT_FAMILY_OPTIONS : FONT_SIZE_OPTIONS).map(
> ([option, text]) => (
> className={`item ${dropDownActiveClass(value === option)} ${style === 'font-size' ? 'fontsize-item' : ''
> }`}
> onClick={() => handleClick(option)}
> key={option}>
> {text}
>
> ),
> )}
>
> );
> }
>
> export default function ToolbarPlugin() {
> const [editor] = useLexicalComposerContext();
> const [activeEditor, setActiveEditor] = useState(editor);
> const toolbarRef = useRef(null);
> const [blockType, setBlockType] = useState("paragraph");
> const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(
> false
> );
> const [codeLanguage, setCodeLanguage] = useState("");
> const [isLink, setIsLink] = useState(false);
> const [isBold, setIsBold] = useState(false);
> const [isItalic, setIsItalic] = useState(false);
> const [isUnderline, setIsUnderline] = useState(false);
> const [isStrikethrough, setIsStrikethrough] = useState(false);
> const [isCode, setIsCode] = useState(false);
> const [isEditable, setIsEditable] = useState(() => editor.isEditable());
> const [fontSize, setFontSize] = useState('16px');
> const [fontFamily, setFontFamily] = useState('Arial');
> const [fontColor, setFontColor] = useState('#000');
> const [bgColor, setBgColor] = useState('#fff');
>
>
>
> const updateToolbar = useCallback(() => {
> const selection = $getSelection();
> if ($isRangeSelection(selection)) {
> const anchorNode = selection.anchor.getNode();
> const element =
> anchorNode.getKey() === "root"
> ? anchorNode
> : anchorNode.getTopLevelElementOrThrow();
> const elementKey = element.getKey();
> const elementDOM = activeEditor.getElementByKey(elementKey);
> if (elementDOM !== null) {
> if ($isListNode(element)) {
> const parentList = $getNearestNodeOfType(anchorNode, ListNode);
> const type = parentList ? parentList.getTag() : element.getTag();
> setBlockType(type);
> } else {
> const type = $isHeadingNode(element)
> ? element.getTag()
> : element.getType();
> setBlockType(type);
> if ($isCodeNode(element)) {
> setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
> }
> }
> }
> // Update text format
> setIsBold(selection.hasFormat("bold"));
> setIsItalic(selection.hasFormat("italic"));
> setIsUnderline(selection.hasFormat("underline"));
> setIsStrikethrough(selection.hasFormat("strikethrough"));
> setIsCode(selection.hasFormat("code"));
>
> // Update links
> const node = getSelectedNode(selection);
> const parent = node.getParent();
> if ($isLinkNode(parent) || $isLinkNode(node)) {
> setIsLink(true);
> } else {
> setIsLink(false);
> }
> setFontSize(
> $getSelectionStyleValueForProperty(selection, 'font-size', '16px'),
> );
> setFontColor(
> $getSelectionStyleValueForProperty(selection, 'color', '#000'),
> );
> setBgColor(
> $getSelectionStyleValueForProperty(
> selection,
> 'background-color',
> '#fff',
> ),
> );
> setFontFamily(
> $getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'),
> );
> }
> }, [activeEditor]);
>
>
> /*
> const updateToolbar = useCallback(() => {
> const selection = $getSelection();
> if ($isRangeSelection(selection)) {
> const anchorNode = selection.anchor.getNode();
> let element =
> anchorNode.getKey() === 'root'
> ? anchorNode
> : $findMatchingParent(anchorNode, (e) => {
> const parent = e.getParent();
> return parent !== null && $isRootOrShadowRoot(parent);
> });
>
> if (element === null) {
> element = anchorNode.getTopLevelElementOrThrow();
> }
>
> const elementKey = element.getKey();
> const elementDOM = activeEditor.getElementByKey(elementKey);
>
> // Update text format
> setIsBold(selection.hasFormat('bold'));
> setIsItalic(selection.hasFormat('italic'));
> setIsUnderline(selection.hasFormat('underline'));
> setIsStrikethrough(selection.hasFormat('strikethrough'));
> setIsCode(selection.hasFormat('code'));
>
> // Update links
> const node = getSelectedNode(selection);
> const parent = node.getParent();
> if ($isLinkNode(parent) || $isLinkNode(node)) {
> setIsLink(true);
> } else {
> setIsLink(false);
> }
>
> if (elementDOM !== null) {
> if ($isListNode(element)) {
> const parentList = $getNearestNodeOfType(
> anchorNode,
> ListNode,
> );
> const type = parentList
> ? parentList.getListType()
> : element.getListType();
> setBlockType(type);
> } else {
> const type = $isHeadingNode(element)
> ? element.getTag()
> : element.getType();
> if (type in blockTypeToBlockName) {
> setBlockType(type);
> }
>
> if ($isCodeNode(element)) {
> const language =
> element.getLanguage();
> setCodeLanguage(
> language ? CODE_LANGUAGE_MAP[language] || language : '',
> );
> return;
> }
>
> }
> }
> // Handle buttons
> setFontSize(
> $getSelectionStyleValueForProperty(selection, 'font-size', '16px'),
> );
> setFontColor(
> $getSelectionStyleValueForProperty(selection, 'color', '#000'),
> );
> setBgColor(
> $getSelectionStyleValueForProperty(
> selection,
> 'background-color',
> '#fff',
> ),
> );
> setFontFamily(
> $getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'),
> );
> }
> }, [activeEditor]);
> */
>
> useEffect(() => {
> return editor.registerCommand(
> SELECTION_CHANGE_COMMAND,
> (_payload, newEditor) => {
> updateToolbar();
> setActiveEditor(newEditor);
> return false;
> },
> COMMAND_PRIORITY_CRITICAL,
> );
> }, [editor, updateToolbar]);
>
> useEffect(() => {
> return mergeRegister(
> editor.registerEditableListener((editable) => {
> setIsEditable(editable);
> }),
> activeEditor.registerUpdateListener(({ editorState }) => {
> editorState.read(() => {
> updateToolbar();
> });
> }),
> );
> }, [activeEditor, editor, updateToolbar]);
>
> /*
> useEffect(() => {
> return mergeRegister(
> editor.registerUpdateListener(({ editorState }) => {
> editorState.read(() => {
> updateToolbar();
> });
> }),
> editor.registerCommand(
> SELECTION_CHANGE_COMMAND,
> (_payload, newEditor) => {
> updateToolbar();
> return false;
> },
> LowPriority
> )
> );
> }, [editor, updateToolbar]);
> */
>
> const applyStyleText = useCallback(
> (styles) => {
> activeEditor.update(() => {
> const selection = $getSelection();
> if ($isRangeSelection(selection)) {
> $patchStyleText(selection, styles);
> }
> });
> },
> [activeEditor],
> );
>
> const onFontColorSelect = useCallback(
> (value) => {
> applyStyleText({ color: value });
> },
> [applyStyleText],
> );
>
> const onBgColorSelect = useCallback(
> (value) => {
> applyStyleText({ 'background-color': value });
> },
> [applyStyleText],
> );
>
> const insertLink = useCallback(() => {
> if (!isLink) {
> editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
> } else {
> editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
> }
> }, [editor, isLink]);
>
> return (
>

> {supportedBlockTypes.has(blockType) && (
> <>
> className="toolbar-item block-controls"
> onClick={() =>
> setShowBlockOptionsDropDown(!showBlockOptionsDropDown)
> }
> aria-label="Formatting Options"
> >
>
> {blockTypeToBlockName[blockType]}
>
>

> {showBlockOptionsDropDown &&
> createPortal(
> editor={editor}
> blockType={blockType}
> toolbarRef={toolbarRef}
> setShowBlockOptionsDropDown={setShowBlockOptionsDropDown}
> />,
> document.body
> )}
>
>
> )}
> disabled={!isEditable}
> style={'font-family'}
> value={fontFamily}
> editor={editor}
> />
>
> disabled={!isEditable}
> style={'font-size'}
> value={fontSize}
> editor={editor}
> />
>
> disabled={!isEditable}
> buttonClassName="toolbar-item color-picker"
> buttonAriaLabel="Formatting text color"
> buttonIconClassName="icon font-color"
> color={fontColor}
> onChange={onFontColorSelect}
> title="text color"
> />
>
> disabled={!isEditable}
> buttonClassName="toolbar-item color-picker"
> buttonAriaLabel="Formatting background color"
> buttonIconClassName="icon bg-color"
> color={bgColor}
> onChange={onBgColorSelect}
> title="bg color"
> />
>
>

> onClick={() => {
> editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
> }}
> className={"toolbar-item spaced " + (isBold ? "active" : "")}
> aria-label="Format Bold"
> >
>
>

> onClick={() => {
> editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
> }}
> className={"toolbar-item spaced " + (isItalic ? "active" : "")}
> aria-label="Format Italics"
> >
>
>

> onClick={() => {
> editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
> }}
> className={"toolbar-item spaced " + (isUnderline ? "active" : "")}
> aria-label="Format Underline"
> >
>
>

> onClick={() => {
> editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
> }}
> className={
> "toolbar-item spaced " + (isStrikethrough ? "active" : "")
> }
> aria-label="Format Strikethrough"
> >
>
>

> onClick={() => {
> editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
> }}
> className={"toolbar-item spaced " + (isCode ? "active" : "")}
> aria-label="Insert Code"
> >
>
>

> onClick={insertLink}
> className={"toolbar-item spaced " + (isLink ? "active" : "")}
> aria-label="Insert Link"
> >
>
>

> {isLink &&
> createPortal(, document.body)}
>

> );
> }

## The current behavior
The font-size ui do not show the correct font-size of the current node
## The expected behavior
The font-size ui should display correct font-size of the current node

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.