2023-01-15 13:49:33 -08:00
|
|
|
/*
|
|
|
|
MIT License
|
2022-12-01 12:32:55 -08:00
|
|
|
|
2023-01-15 13:49:33 -08:00
|
|
|
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
|
|
|
|
|
|
This source code is licensed under the MIT license found in the
|
|
|
|
LICENSE file in the /app/soapbox/features/compose/editor directory.
|
|
|
|
*/
|
2022-12-01 12:32:55 -08:00
|
|
|
|
2023-03-18 11:20:43 -07:00
|
|
|
import { $createCodeNode, $isCodeHighlightNode } from '@lexical/code';
|
2022-12-01 12:32:55 -08:00
|
|
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
2023-03-18 11:20:43 -07:00
|
|
|
import {
|
|
|
|
$isListNode,
|
|
|
|
INSERT_ORDERED_LIST_COMMAND,
|
|
|
|
INSERT_UNORDERED_LIST_COMMAND,
|
|
|
|
ListNode,
|
|
|
|
REMOVE_LIST_COMMAND,
|
|
|
|
} from '@lexical/list';
|
2022-12-01 12:32:55 -08:00
|
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
|
|
import {
|
2023-03-18 11:20:43 -07:00
|
|
|
$createHeadingNode,
|
|
|
|
$createQuoteNode,
|
|
|
|
$isHeadingNode,
|
|
|
|
HeadingTagType,
|
|
|
|
} from '@lexical/rich-text';
|
|
|
|
import {
|
|
|
|
$setBlocksType,
|
|
|
|
} from '@lexical/selection';
|
|
|
|
import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
|
|
|
|
import clsx from 'clsx';
|
|
|
|
import {
|
|
|
|
$createParagraphNode,
|
2022-12-01 12:32:55 -08:00
|
|
|
$getSelection,
|
|
|
|
$isRangeSelection,
|
2023-03-18 11:20:43 -07:00
|
|
|
$isRootOrShadowRoot,
|
2022-12-01 12:32:55 -08:00
|
|
|
$isTextNode,
|
|
|
|
COMMAND_PRIORITY_LOW,
|
2023-03-18 11:20:43 -07:00
|
|
|
DEPRECATED_$isGridSelection,
|
2022-12-01 12:32:55 -08:00
|
|
|
FORMAT_TEXT_COMMAND,
|
|
|
|
LexicalEditor,
|
|
|
|
SELECTION_CHANGE_COMMAND,
|
|
|
|
} from 'lexical';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import * as React from 'react';
|
|
|
|
import { createPortal } from 'react-dom';
|
|
|
|
|
|
|
|
import { Icon } from 'soapbox/components/ui';
|
|
|
|
|
|
|
|
import { getDOMRangeRect } from '../utils/get-dom-range-rect';
|
|
|
|
import { getSelectedNode } from '../utils/get-selected-node';
|
|
|
|
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
|
|
|
|
|
2023-03-18 11:20:43 -07:00
|
|
|
const blockTypeToIcon = {
|
|
|
|
bullet: require('@tabler/icons/list.svg'),
|
|
|
|
check: require('@tabler/icons/list-check.svg'),
|
|
|
|
code: require('@tabler/icons/code.svg'),
|
|
|
|
h1: require('@tabler/icons/h-1.svg'),
|
|
|
|
h2: require('@tabler/icons/h-2.svg'),
|
|
|
|
h3: require('@tabler/icons/h-3.svg'),
|
|
|
|
h4: require('@tabler/icons/h-4.svg'),
|
|
|
|
h5: require('@tabler/icons/h-5.svg'),
|
|
|
|
h6: require('@tabler/icons/h-6.svg'),
|
|
|
|
number: require('@tabler/icons/list-numbers.svg'),
|
|
|
|
paragraph: require('@tabler/icons/align-left.svg'),
|
|
|
|
quote: require('@tabler/icons/blockquote.svg'),
|
|
|
|
};
|
|
|
|
|
|
|
|
const blockTypeToBlockName = {
|
|
|
|
bullet: 'Bulleted List',
|
|
|
|
check: 'Check List',
|
|
|
|
code: 'Code Block',
|
|
|
|
h1: 'Heading 1',
|
|
|
|
h2: 'Heading 2',
|
|
|
|
h3: 'Heading 3',
|
|
|
|
h4: 'Heading 4',
|
|
|
|
h5: 'Heading 5',
|
|
|
|
h6: 'Heading 6',
|
|
|
|
number: 'Numbered List',
|
|
|
|
paragraph: 'Normal',
|
|
|
|
quote: 'Quote',
|
|
|
|
};
|
|
|
|
|
2023-03-28 08:42:16 -07:00
|
|
|
interface IToolbarButton extends React.HTMLAttributes<HTMLButtonElement> {
|
|
|
|
active?: boolean
|
|
|
|
icon: string
|
|
|
|
}
|
|
|
|
|
|
|
|
const ToolbarButton: React.FC<IToolbarButton> = ({ active, icon, ...props }) => (
|
|
|
|
<button
|
|
|
|
className={clsx(
|
|
|
|
'flex cursor-pointer rounded-lg border-0 bg-none p-1 align-middle hover:bg-gray-100 disabled:cursor-not-allowed disabled:hover:bg-none hover:dark:bg-primary-700',
|
|
|
|
{ 'bg-gray-100/30 dark:bg-gray-800/30': active },
|
|
|
|
)}
|
|
|
|
type='button'
|
|
|
|
{...props}
|
|
|
|
>
|
|
|
|
<Icon className='h-5 w-5' src={icon} />
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
|
2023-03-18 11:20:43 -07:00
|
|
|
const BlockTypeDropdown = ({ editor, anchorElem, blockType, icon }: {
|
|
|
|
editor: LexicalEditor
|
|
|
|
anchorElem: HTMLElement
|
|
|
|
blockType: keyof typeof blockTypeToBlockName
|
|
|
|
icon: string
|
|
|
|
}) => {
|
|
|
|
const [showDropDown, setShowDropDown] = useState(false);
|
|
|
|
|
|
|
|
const formatParagraph = () => {
|
|
|
|
editor.update(() => {
|
|
|
|
const selection = $getSelection();
|
|
|
|
if (
|
|
|
|
$isRangeSelection(selection) ||
|
|
|
|
DEPRECATED_$isGridSelection(selection)
|
|
|
|
) {
|
|
|
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatHeading = (headingSize: HeadingTagType) => {
|
|
|
|
if (blockType !== headingSize) {
|
|
|
|
editor.update(() => {
|
|
|
|
const selection = $getSelection();
|
|
|
|
if (
|
|
|
|
$isRangeSelection(selection) ||
|
|
|
|
DEPRECATED_$isGridSelection(selection)
|
|
|
|
) {
|
|
|
|
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatBulletList = () => {
|
|
|
|
if (blockType !== 'bullet') {
|
|
|
|
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
|
|
} else {
|
|
|
|
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatNumberedList = () => {
|
|
|
|
if (blockType !== 'number') {
|
|
|
|
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
|
|
} else {
|
|
|
|
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatQuote = () => {
|
|
|
|
if (blockType !== 'quote') {
|
|
|
|
editor.update(() => {
|
|
|
|
const selection = $getSelection();
|
|
|
|
if (
|
|
|
|
$isRangeSelection(selection) ||
|
|
|
|
DEPRECATED_$isGridSelection(selection)
|
|
|
|
) {
|
|
|
|
$setBlocksType(selection, () => $createQuoteNode());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatCode = () => {
|
|
|
|
if (blockType !== 'code') {
|
|
|
|
editor.update(() => {
|
|
|
|
let selection = $getSelection();
|
|
|
|
|
|
|
|
if (
|
|
|
|
$isRangeSelection(selection) ||
|
|
|
|
DEPRECATED_$isGridSelection(selection)
|
|
|
|
) {
|
|
|
|
if (selection.isCollapsed()) {
|
|
|
|
$setBlocksType(selection, () => $createCodeNode());
|
|
|
|
} else {
|
|
|
|
const textContent = selection.getTextContent();
|
|
|
|
const codeNode = $createCodeNode();
|
|
|
|
selection.insertNodes([codeNode]);
|
|
|
|
selection = $getSelection();
|
|
|
|
if ($isRangeSelection(selection))
|
|
|
|
selection.insertRawText(textContent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<button
|
|
|
|
onClick={() => setShowDropDown(!showDropDown)}
|
2023-03-28 08:42:16 -07:00
|
|
|
className='relative flex cursor-pointer rounded-lg border-0 bg-none p-1 align-middle hover:bg-gray-100 disabled:cursor-not-allowed disabled:hover:bg-none hover:dark:bg-primary-700'
|
2023-03-18 11:20:43 -07:00
|
|
|
aria-label=''
|
|
|
|
type='button'
|
|
|
|
>
|
|
|
|
<Icon src={icon} />
|
|
|
|
<Icon src={require('@tabler/icons/chevron-down.svg')} className='-bottom-2 h-4 w-4' />
|
|
|
|
{showDropDown && (
|
2023-03-28 08:42:16 -07:00
|
|
|
<div
|
2023-04-05 14:45:48 -07:00
|
|
|
className='absolute left-0 top-9 z-10 flex h-[38px] gap-0.5 rounded-lg bg-white p-1 shadow-lg transition-[opacity] dark:bg-gray-900'
|
2023-03-28 08:42:16 -07:00
|
|
|
>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={formatParagraph}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'paragraph'}
|
|
|
|
icon={blockTypeToIcon.paragraph}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={() => formatHeading('h1')}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'h1'}
|
|
|
|
icon={blockTypeToIcon.h1}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={() => formatHeading('h2')}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'h2'}
|
|
|
|
icon={blockTypeToIcon.h2}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={() => formatHeading('h3')}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'h3'}
|
|
|
|
icon={blockTypeToIcon.h3}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={formatBulletList}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'bullet'}
|
|
|
|
icon={blockTypeToIcon.bullet}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={formatNumberedList}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'number'}
|
|
|
|
icon={blockTypeToIcon.number}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={formatQuote}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'quote'}
|
|
|
|
icon={blockTypeToIcon.quote}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2023-03-18 11:20:43 -07:00
|
|
|
onClick={formatCode}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={blockType === 'code'}
|
|
|
|
icon={blockTypeToIcon.code}
|
|
|
|
/>
|
2023-03-18 11:20:43 -07:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-12-01 12:32:55 -08:00
|
|
|
const TextFormatFloatingToolbar = ({
|
|
|
|
editor,
|
|
|
|
anchorElem,
|
2023-03-18 11:20:43 -07:00
|
|
|
blockType,
|
2022-12-01 12:32:55 -08:00
|
|
|
isLink,
|
|
|
|
isBold,
|
|
|
|
isItalic,
|
|
|
|
isUnderline,
|
|
|
|
isCode,
|
|
|
|
isStrikethrough,
|
|
|
|
}: {
|
2023-03-02 10:42:31 -08:00
|
|
|
editor: LexicalEditor
|
|
|
|
anchorElem: HTMLElement
|
2023-03-18 11:20:43 -07:00
|
|
|
blockType: keyof typeof blockTypeToBlockName
|
2023-03-02 10:42:31 -08:00
|
|
|
isBold: boolean
|
|
|
|
isCode: boolean
|
|
|
|
isItalic: boolean
|
|
|
|
isLink: boolean
|
|
|
|
isStrikethrough: boolean
|
|
|
|
isUnderline: boolean
|
2022-12-01 12:32:55 -08:00
|
|
|
}): JSX.Element => {
|
|
|
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
const insertLink = useCallback(() => {
|
|
|
|
if (!isLink) {
|
|
|
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
|
|
|
} else {
|
|
|
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
|
|
}
|
|
|
|
}, [editor, isLink]);
|
|
|
|
|
|
|
|
const updateTextFormatFloatingToolbar = useCallback(() => {
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
|
|
|
|
const nativeSelection = window.getSelection();
|
|
|
|
|
|
|
|
if (popupCharStylesEditorElem === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
if (
|
|
|
|
selection !== null &&
|
|
|
|
nativeSelection !== null &&
|
|
|
|
!nativeSelection.isCollapsed &&
|
|
|
|
rootElement !== null &&
|
|
|
|
rootElement.contains(nativeSelection.anchorNode)
|
|
|
|
) {
|
|
|
|
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
|
|
|
|
|
|
|
|
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem);
|
|
|
|
}
|
|
|
|
}, [editor, anchorElem]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const scrollerElem = anchorElem.parentElement;
|
|
|
|
|
|
|
|
const update = () => {
|
|
|
|
editor.getEditorState().read(() => {
|
|
|
|
updateTextFormatFloatingToolbar();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('resize', update);
|
|
|
|
if (scrollerElem) {
|
|
|
|
scrollerElem.addEventListener('scroll', update);
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener('resize', update);
|
|
|
|
if (scrollerElem) {
|
|
|
|
scrollerElem.removeEventListener('scroll', update);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
editor.getEditorState().read(() => {
|
|
|
|
updateTextFormatFloatingToolbar();
|
|
|
|
});
|
2023-03-17 14:05:59 -07:00
|
|
|
|
2022-12-01 12:32:55 -08:00
|
|
|
return mergeRegister(
|
|
|
|
editor.registerUpdateListener(({ editorState }) => {
|
|
|
|
editorState.read(() => {
|
|
|
|
updateTextFormatFloatingToolbar();
|
|
|
|
});
|
|
|
|
}),
|
|
|
|
|
|
|
|
editor.registerCommand(
|
|
|
|
SELECTION_CHANGE_COMMAND,
|
|
|
|
() => {
|
|
|
|
updateTextFormatFloatingToolbar();
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
COMMAND_PRIORITY_LOW,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}, [editor, updateTextFormatFloatingToolbar]);
|
|
|
|
|
|
|
|
return (
|
2023-03-28 08:42:16 -07:00
|
|
|
<div
|
|
|
|
ref={popupCharStylesEditorRef}
|
2023-04-05 14:45:48 -07:00
|
|
|
className='absolute left-0 top-0 z-10 flex h-[38px] gap-0.5 rounded-lg bg-white p-1 opacity-0 shadow-lg transition-[opacity] dark:bg-gray-900'
|
2023-03-28 08:42:16 -07:00
|
|
|
>
|
2022-12-01 12:32:55 -08:00
|
|
|
{editor.isEditable() && (
|
|
|
|
<>
|
2023-03-18 11:20:43 -07:00
|
|
|
<BlockTypeDropdown
|
|
|
|
editor={editor}
|
|
|
|
anchorElem={anchorElem}
|
|
|
|
blockType={blockType}
|
|
|
|
icon={blockTypeToIcon[blockType]}
|
|
|
|
/>
|
2023-03-28 08:42:16 -07:00
|
|
|
<ToolbarButton
|
2023-03-17 14:05:59 -07:00
|
|
|
onClick={() => {
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
|
|
}}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isBold}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Format text as bold'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/bold.svg')}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2022-12-01 12:32:55 -08:00
|
|
|
onClick={() => {
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
|
|
}}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isItalic}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Format text as italics'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/italic.svg')}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2022-12-01 12:32:55 -08:00
|
|
|
onClick={() => {
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
|
|
}}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isUnderline}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Format text to underlined'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/underline.svg')}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2022-12-01 12:32:55 -08:00
|
|
|
onClick={() => {
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
|
|
}}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isStrikethrough}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Format text with a strikethrough'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/strikethrough.svg')}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2022-12-01 12:32:55 -08:00
|
|
|
onClick={() => {
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
|
|
}}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isCode}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Insert code block'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/code.svg')}
|
|
|
|
/>
|
|
|
|
<ToolbarButton
|
2022-12-01 12:32:55 -08:00
|
|
|
onClick={insertLink}
|
2023-03-28 08:42:16 -07:00
|
|
|
active={isLink}
|
2022-12-01 12:32:55 -08:00
|
|
|
aria-label='Insert link'
|
2023-03-28 08:42:16 -07:00
|
|
|
icon={require('@tabler/icons/link.svg')}
|
|
|
|
/>
|
2022-12-01 12:32:55 -08:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const useFloatingTextFormatToolbar = (
|
|
|
|
editor: LexicalEditor,
|
|
|
|
anchorElem: HTMLElement,
|
|
|
|
): JSX.Element | null => {
|
2023-03-18 11:20:43 -07:00
|
|
|
const [blockType, setBlockType] =
|
|
|
|
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
2022-12-01 12:32:55 -08:00
|
|
|
const [isText, setIsText] = useState(false);
|
|
|
|
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 updatePopup = useCallback(() => {
|
|
|
|
editor.getEditorState().read(() => {
|
|
|
|
// Should not to pop up the floating toolbar when using IME input
|
|
|
|
if (editor.isComposing()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const selection = $getSelection();
|
|
|
|
const nativeSelection = window.getSelection();
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
|
|
|
|
if (
|
|
|
|
nativeSelection !== null &&
|
|
|
|
(!$isRangeSelection(selection) ||
|
|
|
|
rootElement === null ||
|
|
|
|
!rootElement.contains(nativeSelection.anchorNode))
|
|
|
|
) {
|
|
|
|
setIsText(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$isRangeSelection(selection)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-18 11:20:43 -07:00
|
|
|
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 = editor.getElementByKey(elementKey);
|
|
|
|
|
2022-12-01 12:32:55 -08:00
|
|
|
const node = getSelectedNode(selection);
|
|
|
|
|
|
|
|
// Update text format
|
|
|
|
setIsBold(selection.hasFormat('bold'));
|
|
|
|
setIsItalic(selection.hasFormat('italic'));
|
|
|
|
setIsUnderline(selection.hasFormat('underline'));
|
|
|
|
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
|
|
|
setIsCode(selection.hasFormat('code'));
|
|
|
|
|
2023-03-18 11:20:43 -07:00
|
|
|
if (elementDOM !== null) {
|
|
|
|
if ($isListNode(element)) {
|
|
|
|
const parentList = $getNearestNodeOfType<ListNode>(
|
|
|
|
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 as keyof typeof blockTypeToBlockName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-01 12:32:55 -08:00
|
|
|
// Update links
|
|
|
|
const parent = node.getParent();
|
|
|
|
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
|
|
setIsLink(true);
|
|
|
|
} else {
|
|
|
|
setIsLink(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!$isCodeHighlightNode(selection.anchor.getNode()) &&
|
|
|
|
selection.getTextContent() !== ''
|
|
|
|
) {
|
|
|
|
setIsText($isTextNode(node));
|
|
|
|
} else {
|
|
|
|
setIsText(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, [editor]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
document.addEventListener('selectionchange', updatePopup);
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('selectionchange', updatePopup);
|
|
|
|
};
|
|
|
|
}, [updatePopup]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
return mergeRegister(
|
|
|
|
editor.registerUpdateListener(() => {
|
|
|
|
updatePopup();
|
|
|
|
}),
|
|
|
|
editor.registerRootListener(() => {
|
|
|
|
if (editor.getRootElement() === null) {
|
|
|
|
setIsText(false);
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}, [editor, updatePopup]);
|
|
|
|
|
|
|
|
if (!isText || isLink) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return createPortal(
|
|
|
|
<TextFormatFloatingToolbar
|
|
|
|
editor={editor}
|
|
|
|
anchorElem={anchorElem}
|
2023-03-18 11:20:43 -07:00
|
|
|
blockType={blockType}
|
2022-12-01 12:32:55 -08:00
|
|
|
isLink={isLink}
|
|
|
|
isBold={isBold}
|
|
|
|
isItalic={isItalic}
|
|
|
|
isStrikethrough={isStrikethrough}
|
|
|
|
isUnderline={isUnderline}
|
|
|
|
isCode={isCode}
|
|
|
|
/>,
|
|
|
|
anchorElem,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const FloatingTextFormatToolbarPlugin = ({
|
|
|
|
anchorElem = document.body,
|
|
|
|
}: {
|
2023-03-02 10:42:31 -08:00
|
|
|
anchorElem?: HTMLElement
|
2022-12-01 12:32:55 -08:00
|
|
|
}): JSX.Element | null => {
|
|
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
return useFloatingTextFormatToolbar(editor, anchorElem);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default FloatingTextFormatToolbarPlugin;
|