Allow creating horizontal lines
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -34,6 +34,7 @@ const LINK_MATCHERS = [
|
||||
|
||||
import { useNodes } from './nodes';
|
||||
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
||||
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
||||
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
||||
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
|
||||
import MentionPlugin from './plugins/mention-plugin';
|
||||
@ -188,6 +189,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||
{features.richText && <ListPlugin />}
|
||||
{features.richText && floatingAnchorElem && (
|
||||
<>
|
||||
<FloatingBlockTypeToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
|
||||
</>
|
||||
|
||||
@ -0,0 +1,211 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $createHorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||
import { mergeRegister } from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
DEPRECATED_$isGridSelection,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
|
||||
|
||||
import { ToolbarButton } from './floating-text-format-toolbar-plugin';
|
||||
|
||||
const BlockTypeFloatingToolbar = ({
|
||||
editor,
|
||||
anchorElem,
|
||||
}: {
|
||||
editor: LexicalEditor
|
||||
anchorElem: HTMLElement
|
||||
}): JSX.Element => {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateBlockTypeFloatingToolbar = 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.anchorNode?.textContent &&
|
||||
rootElement !== null &&
|
||||
rootElement.contains(nativeSelection.anchorNode)
|
||||
) {
|
||||
setFloatingElemPosition((nativeSelection.focusNode as HTMLParagraphElement)?.getBoundingClientRect(), popupCharStylesEditorElem, anchorElem);
|
||||
}
|
||||
}, [editor, anchorElem]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElem = anchorElem.parentElement;
|
||||
|
||||
const update = () => {
|
||||
editor.getEditorState().read(() => {
|
||||
updateBlockTypeFloatingToolbar();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.addEventListener('scroll', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.removeEventListener('scroll', update);
|
||||
}
|
||||
};
|
||||
}, [editor, updateBlockTypeFloatingToolbar, anchorElem]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
updateBlockTypeFloatingToolbar();
|
||||
});
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateBlockTypeFloatingToolbar();
|
||||
});
|
||||
}),
|
||||
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateBlockTypeFloatingToolbar();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
);
|
||||
}, [editor, updateBlockTypeFloatingToolbar]);
|
||||
|
||||
const createHorizontalLine = () => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
||||
const selectionNode = selection.anchor.getNode();
|
||||
selectionNode.replace($createHorizontalRuleNode());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={popupCharStylesEditorRef}
|
||||
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'
|
||||
>
|
||||
{editor.isEditable() && (
|
||||
<>
|
||||
<ToolbarButton
|
||||
onClick={createHorizontalLine}
|
||||
aria-label='Insert horizontal line'
|
||||
icon={require('@tabler/icons/line-dashed.svg')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useFloatingBlockTypeToolbar = (
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null => {
|
||||
const [isEmptyBlock, setIsEmptyBlock] = 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))
|
||||
) {
|
||||
setIsEmptyBlock(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
|
||||
setIsEmptyBlock(anchorNode.getType() === 'paragraph' && anchorNode.getTextContentSize() === 0);
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updatePopup();
|
||||
}),
|
||||
editor.registerRootListener(() => {
|
||||
if (editor.getRootElement() === null) {
|
||||
setIsEmptyBlock(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
if (!isEmptyBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<BlockTypeFloatingToolbar
|
||||
editor={editor}
|
||||
anchorElem={anchorElem}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
};
|
||||
|
||||
const FloatingBlockTypeToolbarPlugin = ({
|
||||
anchorElem = document.body,
|
||||
}: {
|
||||
anchorElem?: HTMLElement
|
||||
}): JSX.Element | null => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return useFloatingBlockTypeToolbar(editor, anchorElem);
|
||||
};
|
||||
|
||||
export default FloatingBlockTypeToolbarPlugin;
|
||||
@ -86,7 +86,7 @@ interface IToolbarButton extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
icon: string
|
||||
}
|
||||
|
||||
const ToolbarButton: React.FC<IToolbarButton> = ({ active, icon, ...props }) => (
|
||||
export 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',
|
||||
@ -125,10 +125,7 @@ const BlockTypeDropdown = ({ editor, anchorElem, blockType, icon }: {
|
||||
if (blockType !== headingSize) {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (
|
||||
$isRangeSelection(selection) ||
|
||||
DEPRECATED_$isGridSelection(selection)
|
||||
) {
|
||||
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
||||
}
|
||||
});
|
||||
@ -155,10 +152,7 @@ const BlockTypeDropdown = ({ editor, anchorElem, blockType, icon }: {
|
||||
if (blockType !== 'quote') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (
|
||||
$isRangeSelection(selection) ||
|
||||
DEPRECATED_$isGridSelection(selection)
|
||||
) {
|
||||
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createQuoteNode());
|
||||
}
|
||||
});
|
||||
@ -170,10 +164,7 @@ const BlockTypeDropdown = ({ editor, anchorElem, blockType, icon }: {
|
||||
editor.update(() => {
|
||||
let selection = $getSelection();
|
||||
|
||||
if (
|
||||
$isRangeSelection(selection) ||
|
||||
DEPRECATED_$isGridSelection(selection)
|
||||
) {
|
||||
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
||||
if (selection.isCollapsed()) {
|
||||
$setBlocksType(selection, () => $createCodeNode());
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user