feat: add ability to convert selection in Super to bulleted or numbered list

This commit is contained in:
Mo
2022-12-07 11:14:03 -06:00
parent 2fa3fec9ab
commit be4cc4e605
6 changed files with 132 additions and 24 deletions

View File

@@ -11,7 +11,11 @@ import './index.css';
import {$isCodeHighlightNode} from '@lexical/code';
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
mergeRegister,
$findMatchingParent,
$getNearestNodeOfType,
} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
@@ -20,7 +24,17 @@ import {
FORMAT_TEXT_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
$isRootOrShadowRoot,
COMMAND_PRIORITY_CRITICAL,
} from 'lexical';
import {$isHeadingNode} from '@lexical/rich-text';
import {
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
INSERT_ORDERED_LIST_COMMAND,
} from '@lexical/list';
import {useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
@@ -36,9 +50,26 @@ import {
LinkIcon,
SuperscriptIcon,
SubscriptIcon,
ListBulleted,
ListNumbered,
} from '@standardnotes/icons';
import {IconComponent} from '../../Theme/IconComponent';
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',
};
const IconSize = 15;
function TextFormatFloatingToolbar({
@@ -52,6 +83,8 @@ function TextFormatFloatingToolbar({
isStrikethrough,
isSubscript,
isSuperscript,
isBulletedList,
isNumberedList,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
@@ -63,6 +96,8 @@ function TextFormatFloatingToolbar({
isSubscript: boolean;
isSuperscript: boolean;
isUnderline: boolean;
isBulletedList: boolean;
isNumberedList: boolean;
}): JSX.Element {
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
@@ -74,6 +109,22 @@ function TextFormatFloatingToolbar({
}
}, [editor, isLink]);
const formatBulletList = useCallback(() => {
if (!isBulletedList) {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
}, [isBulletedList]);
const formatNumberedList = useCallback(() => {
if (!isNumberedList) {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
}, [isNumberedList]);
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection();
@@ -226,6 +277,22 @@ function TextFormatFloatingToolbar({
<LinkIcon />
</IconComponent>
</button>
<button
onClick={formatBulletList}
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
aria-label="Insert bulleted list">
<IconComponent size={IconSize}>
<ListBulleted />
</IconComponent>
</button>
<button
onClick={formatNumberedList}
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
aria-label="Insert numbered list">
<IconComponent size={IconSize}>
<ListNumbered />
</IconComponent>
</button>
</>
)}
</div>
@@ -236,6 +303,7 @@ function useFloatingTextFormatToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null {
const [activeEditor, setActiveEditor] = useState(editor);
const [isText, setIsText] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
@@ -245,6 +313,8 @@ function useFloatingTextFormatToolbar(
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const [isCode, setIsCode] = useState(false);
const [blockType, setBlockType] =
useState<keyof typeof blockTypeToBlockName>('paragraph');
const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
@@ -270,6 +340,42 @@ function useFloatingTextFormatToolbar(
return;
}
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);
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);
}
}
}
const node = getSelectedNode(selection);
// Update text format
@@ -298,14 +404,19 @@ function useFloatingTextFormatToolbar(
setIsText(false);
}
});
}, [editor]);
}, [editor, activeEditor]);
useEffect(() => {
document.addEventListener('selectionchange', updatePopup);
return () => {
document.removeEventListener('selectionchange', updatePopup);
};
}, [updatePopup]);
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
setActiveEditor(newEditor);
updatePopup();
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, updatePopup]);
useEffect(() => {
return mergeRegister(
@@ -336,6 +447,8 @@ function useFloatingTextFormatToolbar(
isSuperscript={isSuperscript}
isUnderline={isUnderline}
isCode={isCode}
isBulletedList={blockType === 'bullet'}
isNumberedList={blockType === 'number'}
/>,
anchorElem,
);