Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-lexical-ujdd17/deployments/2895
marcin mikołajczak 2022-12-01 21:32:55 +01:00
rodzic f6169b9cf0
commit 31f340282f
18 zmienionych plików z 1348 dodań i 45 usunięć

Wyświetl plik

@ -260,7 +260,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
<Textarea
ref={this.setTextarea}
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
className={classNames('hidden transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
'min-h-[40px]': condensed,
'min-h-[100px]': !condensed,
})}

Wyświetl plik

@ -35,7 +35,8 @@
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
}
[data-markup] code {
[data-markup] code,
[data-markup] pre {
@apply cursor-text font-mono;
}

Wyświetl plik

@ -25,6 +25,7 @@ import ReplyIndicatorContainer from '../containers/reply-indicator-container';
import ScheduleFormContainer from '../containers/schedule-form-container';
import UploadButtonContainer from '../containers/upload-button-container';
import WarningContainer from '../containers/warning-container';
import ComposeEditor from '../editor';
import { countableText } from '../util/counter';
import EmojiPickerDropdown from './emoji-picker/emoji-picker-dropdown';
@ -41,13 +42,14 @@ import UploadForm from './upload-form';
import VisualCharacterCounter from './visual-character-counter';
import Warning from './warning';
import type { EditorState } from 'lexical';
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic...' },
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
@ -88,6 +90,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const formRef = useRef(null);
const spoilerTextRef = useRef<AutosuggestInput>(null);
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
const editorStateRef = useRef<string>(null);
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
dispatch(changeCompose(id, e.target.value));
@ -134,11 +137,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
};
const handleSubmit = (e?: React.FormEvent<Element>) => {
if (text !== autosuggestTextareaRef.current?.textarea?.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
dispatch(changeCompose(id, autosuggestTextareaRef.current!.textarea!.value));
}
// editorStateRef.current
console.log(editorStateRef.current);
dispatch(changeCompose(id, editorStateRef.current!));
// if (text !== autosuggestTextareaRef.current?.textarea?.value) {
// // Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// // Update the state to match the current text
// dispatch(changeCompose(id, autosuggestTextareaRef.current!.textarea!.value));
// }
// Submit disabled:
const fulltext = [spoilerText, countableText(text)].join('');
@ -292,6 +298,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{!shouldCondense && <ReplyMentions composeId={id} />}
<ComposeEditor ref={editorStateRef} />
<AutosuggestTextarea
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
placeholder={intl.formatMessage(hasPoll ? messages.pollPlaceholder : messages.placeholder)}

Wyświetl plik

@ -1,6 +1,7 @@
import classNames from 'clsx';
import React from 'react';
import { HStack } from 'soapbox/components/ui';
import { useCompose } from 'soapbox/hooks';
import Upload from './upload';
@ -14,19 +15,16 @@ interface IUploadForm {
const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
const mediaIds = useCompose(composeId).media_attachments.map((item: AttachmentEntity) => item.id);
const classes = classNames('compose-form__uploads-wrapper', {
'contains-media': mediaIds.size !== 0,
});
return (
<div className='compose-form__upload-wrapper'>
<div className='overflow-hidden'>
<UploadProgress composeId={composeId} />
<div className={classes}>
<HStack wrap className={classNames(mediaIds.size !== 0 && 'p-1')}>
{mediaIds.map((id: string) => (
<Upload id={id} key={id} composeId={composeId} />
))}
</div>
</HStack>
</div>
);
};

Wyświetl plik

@ -0,0 +1,72 @@
import {
$convertToMarkdownString,
TRANSFORMERS,
} from '@lexical/markdown';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import nodes from './nodes';
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
// import type { EditorState } from 'lexical';
const initialConfig = {
namespace: 'ComposeForm',
onError: console.error,
nodes,
};
const ComposeEditor = React.forwardRef<string, any>((_, editorStateRef) => {
const [floatingAnchorElem, setFloatingAnchorElem] =
useState<HTMLDivElement | null>(null);
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
if (_floatingAnchorElem !== null) {
setFloatingAnchorElem(_floatingAnchorElem);
}
};
return (
<LexicalComposer initialConfig={initialConfig}>
<div className='relative' data-markup>
<RichTextPlugin
contentEditable={
<div className='editor' ref={onRef}>
<ContentEditable className='outline-none py-2 min-h-[100px]' />
</div>
}
placeholder={(
<div className='absolute top-2 text-gray-600 dark:placeholder:text-gray-600 pointer-events-none select-none'>
<FormattedMessage id='compose_form.placeholder' defaultMessage="What's on your mind" />
</div>
)}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={(_, editor) => {
editor.update(() => {
if (editorStateRef) (editorStateRef as any).current = $convertToMarkdownString(TRANSFORMERS);
});
}}
/>
<HistoryPlugin />
<LinkPlugin />
{floatingAnchorElem ? (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
</>
) : ''}
</div>
</LexicalComposer>
);
});
export default ComposeEditor;

Wyświetl plik

@ -0,0 +1,74 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { CodeHighlightNode, CodeNode } from '@lexical/code';
// import { HashtagNode } from '@lexical/hashtag';
import { AutoLinkNode, LinkNode } from '@lexical/link';
// import { ListItemNode, ListNode } from '@lexical/list';
// import { MarkNode } from '@lexical/mark';
// import { OverflowNode } from '@lexical/overflow';
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
// import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
// import { CollapsibleContainerNode } from '../plugins/CollapsiblePlugin/CollapsibleContainerNode';
// import { CollapsibleContentNode } from '../plugins/CollapsiblePlugin/CollapsibleContentNode';
// import { CollapsibleTitleNode } from '../plugins/CollapsiblePlugin/CollapsibleTitleNode';
// import { AutocompleteNode } from './AutocompleteNode';
// import { EmojiNode } from './EmojiNode';
// import { EquationNode } from './EquationNode';
// import { ExcalidrawNode } from './ExcalidrawNode';
// import { FigmaNode } from './FigmaNode';
// import { ImageNode } from './ImageNode';
// import { KeywordNode } from './KeywordNode';
// import { MentionNode } from './MentionNode';
// import { PollNode } from './PollNode';
// import { StickyNode } from './StickyNode';
// import { TableNode as NewTableNode } from './TableNode';
// import { TweetNode } from './TweetNode';
// import { YouTubeNode } from './YouTubeNode';
import type { Klass, LexicalNode } from 'lexical';
const PlaygroundNodes: Array<Klass<LexicalNode>> = [
HeadingNode,
// ListNode,
// ListItemNode,
QuoteNode,
CodeNode,
// NewTableNode,
// TableNode,
// TableCellNode,
// TableRowNode,
// HashtagNode,
CodeHighlightNode,
AutoLinkNode,
LinkNode,
// OverflowNode,
// PollNode,
// StickyNode,
// ImageNode,
// MentionNode,
// EmojiNode,
// ExcalidrawNode,
// EquationNode,
// AutocompleteNode,
// KeywordNode,
HorizontalRuleNode,
// TweetNode,
// YouTubeNode,
// FigmaNode,
// MarkNode,
// CollapsibleContainerNode,
// CollapsibleContentNode,
// CollapsibleTitleNode,
];
export default PlaygroundNodes;

Wyświetl plik

@ -0,0 +1,276 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $findMatchingParent, mergeRegister } from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_LOW,
GridSelection,
LexicalEditor,
NodeSelection,
RangeSelection,
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 { getSelectedNode } from '../utils/get-selected-node';
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
import { sanitizeUrl } from '../utils/url';
const FloatingLinkEditor = ({
editor,
anchorElem,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
}): JSX.Element => {
const editorRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [linkUrl, setLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState<
RangeSelection | GridSelection | NodeSelection | null
>(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 !== null &&
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 as HTMLElement;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
setFloatingElemPosition(rect, editorElem, anchorElem);
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== 'link-input') {
if (rootElement !== null) {
setFloatingElemPosition(null, editorElem, anchorElem);
}
setLastSelection(null);
setEditMode(false);
setLinkUrl('');
}
return true;
}, [anchorElem, editor]);
useEffect(() => {
const scrollerElem = anchorElem.parentElement;
const update = () => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
};
window.addEventListener('resize', update);
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update);
}
};
}, [anchorElem.parentElement, editor, updateLinkEditor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className='link-editor'>
<div className='link-input'>
{isEditMode ? (
<>
<input
ref={inputRef}
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,
sanitizeUrl(linkUrl),
);
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
setEditMode(false);
}
} else if (event.key === 'Escape') {
event.preventDefault();
setEditMode(false);
}
}}
/>
<div
className='link-edit'
role='button'
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}}
>
<Icon className='h-5 w-5' src={require('@tabler/icons/x.svg')} />
</div>
</>
) : (
<>
<a href={linkUrl} target='_blank' rel='noopener noreferrer'>
{linkUrl}
</a>
<div
className='link-edit'
role='button'
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setEditMode(true);
}}
>
<Icon className='h-5 w-5' src={require('@tabler/icons/pencil.svg')} />
</div>
</>
)}
</div>
</div>
);
};
const useFloatingLinkEditorToolbar = (
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null => {
const [activeEditor, setActiveEditor] = useState(editor);
const [isLink, setIsLink] = useState(false);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const linkParent = $findMatchingParent(node, $isLinkNode);
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode);
// We don't want this menu to open for auto links.
if (linkParent != null && autoLinkParent == null) {
setIsLink(true);
} else {
setIsLink(false);
}
}
}, []);
useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
updateToolbar();
setActiveEditor(newEditor);
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, updateToolbar]);
return isLink
? createPortal(
<FloatingLinkEditor editor={activeEditor} anchorElem={anchorElem} />,
anchorElem,
)
: null;
};
const FloatingLinkEditorPlugin = ({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
return useFloatingLinkEditorToolbar(editor, anchorElem);
};
export default FloatingLinkEditorPlugin;

Wyświetl plik

@ -0,0 +1,311 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// 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 {
$getSelection,
$isRangeSelection,
$isTextNode,
COMMAND_PRIORITY_LOW,
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';
const TextFormatFloatingToolbar = ({
editor,
anchorElem,
isLink,
isBold,
isItalic,
isUnderline,
isCode,
isStrikethrough,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
isBold: boolean;
isCode: boolean;
isItalic: boolean;
isLink: boolean;
isStrikethrough: boolean;
isUnderline: boolean;
}): 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();
});
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateTextFormatFloatingToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateTextFormatFloatingToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, updateTextFormatFloatingToolbar]);
return (
<div ref={popupCharStylesEditorRef} className='floating-text-format-popup'>
{editor.isEditable() && (
<>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={'popup-item spaced ' + (isBold ? 'active' : '')}
aria-label='Format text as bold'
type='button'
>
<Icon src={require('@tabler/icons/bold.svg')} />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
aria-label='Format text as italics'
type='button'
>
<Icon src={require('@tabler/icons/italic.svg')} />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
aria-label='Format text to underlined'
type='button'
>
<Icon src={require('@tabler/icons/underline.svg')} />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
aria-label='Format text with a strikethrough'
type='button'
>
<Icon src={require('@tabler/icons/strikethrough.svg')} />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
}}
className={'popup-item spaced ' + (isCode ? 'active' : '')}
aria-label='Insert code block'
type='button'
>
<Icon src={require('@tabler/icons/code.svg')} />
</button>
<button
onClick={insertLink}
className={'popup-item spaced ' + (isLink ? 'active' : '')}
aria-label='Insert link'
type='button'
>
<Icon src={require('@tabler/icons/link.svg')} />
</button>
</>
)}
</div>
);
};
const useFloatingTextFormatToolbar = (
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null => {
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;
}
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'));
// 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}
isLink={isLink}
isBold={isBold}
isItalic={isItalic}
isStrikethrough={isStrikethrough}
isUnderline={isUnderline}
isCode={isCode}
/>,
anchorElem,
);
};
const FloatingTextFormatToolbarPlugin = ({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
return useFloatingTextFormatToolbar(editor, anchorElem);
};
export default FloatingTextFormatToolbarPlugin;

Wyświetl plik

@ -0,0 +1,18 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import * as React from 'react';
import { validateUrl } from '../utils/url';
const LinkPlugin = (): JSX.Element => {
return <LexicalLinkPlugin validateUrl={validateUrl} />;
};
export default LinkPlugin;

Wyświetl plik

@ -0,0 +1,28 @@
/* eslint-disable eqeqeq */
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export const getDOMRangeRect = (
nativeSelection: Selection,
rootElement: HTMLElement,
): DOMRect => {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild as HTMLElement;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
return rect;
};

Wyświetl plik

@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $isAtNodeEnd } from '@lexical/selection';
import { ElementNode, RangeSelection, TextNode } from 'lexical';
export const getSelectedNode = (
selection: RangeSelection,
): TextNode | ElementNode => {
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;
}
};

Wyświetl plik

@ -0,0 +1,46 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const VERTICAL_GAP = 10;
const HORIZONTAL_OFFSET = 5;
export const setFloatingElemPosition = (
targetRect: ClientRect | null,
floatingElem: HTMLElement,
anchorElem: HTMLElement,
verticalGap: number = VERTICAL_GAP,
horizontalOffset: number = HORIZONTAL_OFFSET,
): void => {
const scrollerElem = anchorElem.parentElement;
if (targetRect === null || !scrollerElem) {
floatingElem.style.opacity = '0';
floatingElem.style.transform = 'translate(-10000px, -10000px)';
return;
}
const floatingElemRect = floatingElem.getBoundingClientRect();
const anchorElementRect = anchorElem.getBoundingClientRect();
const editorScrollerRect = scrollerElem.getBoundingClientRect();
let top = targetRect.top - floatingElemRect.height - verticalGap;
let left = targetRect.left - horizontalOffset;
if (top < editorScrollerRect.top) {
top += floatingElemRect.height + targetRect.height + verticalGap * 2;
}
if (left + floatingElemRect.width > editorScrollerRect.right) {
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
}
top -= anchorElementRect.top;
left -= anchorElementRect.left;
floatingElem.style.opacity = '1';
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
};

Wyświetl plik

@ -0,0 +1,34 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export const sanitizeUrl = (url: string): string => {
/** A pattern that matches safe URLs. */
const SAFE_URL_PATTERN =
/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
/** A pattern that matches safe data URLs. */
const DATA_URL_PATTERN =
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
url = String(url).trim();
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
return 'https://';
};
// Source: https://stackoverflow.com/a/8234912/2013580
const urlRegExp = new RegExp(
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/,
);
export const validateUrl = (url: string): boolean => {
// TODO Fix UI for link insertion; it should never default to an invalid URL such as https://.
// Maybe show a dialog where they user can type the URL before inserting it.
return url === 'https://' || urlRegExp.test(url);
};

Wyświetl plik

@ -128,31 +128,31 @@ const messages = defineMessages({
});
const keyMap = {
help: '?',
new: 'n',
search: 's',
forceNew: 'option+n',
reply: 'r',
favourite: 'f',
react: 'e',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
openProfile: 'p',
moveDown: ['down', 'j'],
moveUp: ['up', 'k'],
back: 'backspace',
goToHome: 'g h',
goToNotifications: 'g n',
goToFavourites: 'g f',
goToPinned: 'g p',
goToProfile: 'g u',
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'a',
// help: '?',
// new: 'n',
// search: 's',
// forceNew: 'option+n',
// reply: 'r',
// favourite: 'f',
// react: 'e',
// boost: 'b',
// mention: 'm',
// open: ['enter', 'o'],
// openProfile: 'p',
// moveDown: ['down', 'j'],
// moveUp: ['up', 'k'],
// back: 'backspace',
// goToHome: 'g h',
// goToNotifications: 'g n',
// goToFavourites: 'g f',
// goToPinned: 'g p',
// goToProfile: 'g u',
// goToBlocked: 'g b',
// goToMuted: 'g m',
// goToRequests: 'g r',
// toggleHidden: 'x',
// toggleSensitive: 'h',
// openMedia: 'a',
};
const SwitchingColumnsArea: React.FC = ({ children }) => {

Wyświetl plik

@ -196,3 +196,247 @@
@apply block shadow-md;
}
}
.floating-text-format-popup {
display: flex;
background: #fff;
padding: 4px;
vertical-align: middle;
position: absolute;
top: 0;
left: 0;
z-index: 10;
opacity: 0;
background-color: #fff;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 8px;
transition: opacity 0.5s;
height: 38px;
}
.floating-text-format-popup button.popup-item {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 5px;
cursor: pointer;
vertical-align: middle;
}
.floating-text-format-popup button.popup-item:disabled {
cursor: not-allowed;
}
.floating-text-format-popup button.popup-item.spaced {
margin-right: 2px;
}
.floating-text-format-popup button.popup-item i.format {
background-size: contain;
height: 18px;
width: 18px;
margin-top: 2px;
vertical-align: -0.25em;
display: flex;
opacity: 0.6;
}
.floating-text-format-popup button.popup-item:disabled i.format {
opacity: 0.2;
}
.floating-text-format-popup button.popup-item.active {
background-color: rgba(223, 232, 250, 0.3);
}
.floating-text-format-popup button.popup-item.active i {
opacity: 1;
}
.floating-text-format-popup button.popup-item svg {
@apply h-5 w-5;
}
.floating-text-format-popup .popup-item:hover:not([disabled]) {
background-color: #eee;
}
.floating-text-format-popup select.popup-item {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 8px;
vertical-align: middle;
-webkit-appearance: none;
-moz-appearance: none;
width: 70px;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
}
.floating-text-format-popup select.code-language {
text-transform: capitalize;
width: 130px;
}
.floating-text-format-popup .popup-item .text {
display: flex;
line-height: 20px;
vertical-align: middle;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.floating-text-format-popup .popup-item .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
.floating-text-format-popup i.chevron-down {
margin-top: 3px;
width: 16px;
height: 16px;
display: flex;
user-select: none;
}
.floating-text-format-popup i.chevron-down.inside {
width: 16px;
height: 16px;
display: flex;
margin-left: -25px;
margin-top: 11px;
margin-right: 10px;
pointer-events: none;
}
.floating-text-format-popup .divider {
width: 1px;
background-color: #eee;
margin: 0 4px;
}
.link-editor {
position: absolute;
top: 0;
left: 0;
z-index: 10;
max-width: 400px;
width: 100%;
opacity: 0;
background-color: #fff;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 8px;
transition: opacity 0.5s;
will-change: transform;
}
.link-editor .button {
width: 20px;
height: 20px;
display: inline-block;
padding: 6px;
border-radius: 8px;
cursor: pointer;
margin: 0 2px;
}
.link-editor .button.hovered {
width: 20px;
height: 20px;
display: inline-block;
background-color: #eee;
}
.link-editor .button i,
.actions i {
background-size: contain;
display: inline-block;
height: 20px;
width: 20px;
vertical-align: -0.25em;
}
.link-editor .button.active,
.toolbar .button.active {
background-color: rgb(223, 232, 250);
}
.link-editor .link-input {
display: block;
width: calc(100% - 24px);
box-sizing: border-box;
margin: 8px 12px;
padding: 8px 12px;
border-radius: 15px;
background-color: #eee;
font-size: 15px;
color: rgb(5, 5, 5);
border: 0;
outline: 0;
position: relative;
font-family: inherit;
}
.link-editor .link-input input {
width: 100%;
margin: -8px -12px;
padding: 8px 12px;
background: transparent;
font-size: 15px;
color: rgb(5, 5, 5);
border: 0;
outline: 0;
}
.link-editor div.link-edit {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
position: absolute;
right: 0;
top: 0;
bottom: 0;
cursor: pointer;
}
.link-editor .link-input a {
color: rgb(33, 111, 219);
text-decoration: none;
display: block;
white-space: nowrap;
overflow: hidden;
margin-right: 30px;
text-overflow: ellipsis;
}
.link-editor .link-input a:hover {
text-decoration: underline;
}
.link-editor .font-size-wrapper,
.link-editor .font-family-wrapper {
display: flex;
margin: 0 4px;
}
.link-editor select {
padding: 6px;
border: none;
background-color: rgba(0, 0, 0, 0.075);
border-radius: 4px;
}

Wyświetl plik

@ -24,11 +24,6 @@ select {
$no-columns-breakpoint: 600px;
code {
font-family: var(--font-monospace), monospace;
font-weight: 400;
}
.form-container {
max-width: 400px;
padding: 20px;

Wyświetl plik

@ -54,6 +54,13 @@
"@gamestdio/websocket": "^0.3.2",
"@jest/globals": "^28.1.2",
"@lcdp/offline-plugin": "^5.1.0",
"@lexical/code": "^0.6.4",
"@lexical/link": "^0.6.4",
"@lexical/markdown": "^0.6.4",
"@lexical/react": "^0.6.4",
"@lexical/rich-text": "^0.6.4",
"@lexical/selection": "^0.6.4",
"@lexical/utils": "^0.6.4",
"@metamask/providers": "^9.0.0",
"@popperjs/core": "^2.11.5",
"@reach/menu-button": "^0.18.0",
@ -138,6 +145,7 @@
"intl-pluralrules": "^1.3.1",
"is-nan": "^1.2.1",
"jsdoc": "~3.6.7",
"lexical": "^0.6.4",
"libphonenumber-js": "^1.10.8",
"line-awesome": "^1.3.0",
"localforage": "^1.10.0",

165
yarn.lock
Wyświetl plik

@ -1869,6 +1869,159 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lexical/clipboard@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.6.4.tgz#fdbe763bd0fde6f08f1f6fc2cad40d9977127941"
integrity sha512-pbJmbR1B9d3l4Ey/NrNfvLibA9CzGBamf3HanDtSEcXTDwKoSkUoPDgY9worUYdz9vnQlefntIRrOGjaDbFOxA==
dependencies:
"@lexical/html" "0.6.4"
"@lexical/list" "0.6.4"
"@lexical/selection" "0.6.4"
"@lexical/utils" "0.6.4"
"@lexical/code@0.6.4", "@lexical/code@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.6.4.tgz#9cdbcaf395248f8e194e3c3edde4c17f8511dd0f"
integrity sha512-fsGLvY6BgLSuKRn4/xqbZMnJSrY3u1b2htk7AKVOBxSeEENjLClMkdomVvbWF6CkGMX1c1wkP93MmeOe9VmF/g==
dependencies:
"@lexical/utils" "0.6.4"
prismjs "^1.27.0"
"@lexical/dragon@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.6.4.tgz#a69aeaf5ab89187cf2ff3d1ac92631c91a0dec0c"
integrity sha512-6M7rtmZck1CxHsKCpUaSUNIzU0zDfus77RWEKbdBAdvFROqQoOKmBJnBRjlghqDPpi5lz8pL+70eXfcfJepwlw==
"@lexical/hashtag@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.6.4.tgz#3d670b1c933db254a6fa027c798da0e1fa06829d"
integrity sha512-xB2/Es9PsDdtEHtcOxZk5AtqSEAkzbgadSJC7iYFssc+ltX1ZGOLqXHREZCwL8c++kUnuwr+tALL7iRuLpVouA==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/history@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.6.4.tgz#52ec268b3769663d6fd8d573018ef0a67f96d18e"
integrity sha512-NdFxuwNdnFFihHsawewy1tQTW3M0ubzxmkAHZNYnEnjTcF77NxetM24jdUbNS84aLckCJnc4uM84/B0N6P3H2Q==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/html@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.6.4.tgz#d9e26779f5dc816289643ebac2beb3f42fe47d97"
integrity sha512-NVW70XFd9Ekfe4t/FwZc2EuT2axC8wYxNQ3NI9FXnqAWvLP3F6caDUa0Qn58Gdyx8QQGKRa6GL0BFEXqaD3cxw==
dependencies:
"@lexical/selection" "0.6.4"
"@lexical/link@0.6.4", "@lexical/link@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.6.4.tgz#e7c1ab092d8281cc4ccf0d371e407b3aed4a2e7d"
integrity sha512-4ergNX/GOKyRFUIf2daflB1LzU96UAi4mGH3QdkvfDBo+a4eFj7GYXCf98ktilhvGlC7El4cgLVCq/BoidjS/w==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/list@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.6.4.tgz#ad1f89401bc3104130baffa260c9607240f1d30a"
integrity sha512-2WjQTABK4Zckup0YXp/UpAuCul4Ay8yTTUNEhsPpZegeYyglURugx5uYsh811+wN6acUZwFhkYRyyzxU9cDPsg==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/mark@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.6.4.tgz#d9c9d284a6a3f236290023b39f68647756c659cb"
integrity sha512-2Hpc21i2VmAHilnAevbuUlQijiRd6KVoIEJldx2+oK0vsXQnYnAF4T/RdZO3BStPVdSW3KnIa2TNqsmMJHqG2g==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/markdown@0.6.4", "@lexical/markdown@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.6.4.tgz#884a91028ee303e3446b817c3d8b284c9038b808"
integrity sha512-9kg+BsP4ePCztrK7UYW8a+8ad1/h/OLziJkMZkl3YAkfhJudkHoj4ljCTJZcLuXHtVXmvLZhyGZktitcJImnOg==
dependencies:
"@lexical/code" "0.6.4"
"@lexical/link" "0.6.4"
"@lexical/list" "0.6.4"
"@lexical/rich-text" "0.6.4"
"@lexical/text" "0.6.4"
"@lexical/utils" "0.6.4"
"@lexical/offset@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.6.4.tgz#06fdf49e8e18135e82c9925c6b168e7e012a5420"
integrity sha512-IdB1GL5yRRDQPOedXG4zT7opdjHGx+7DqLKOQwcSMWo/XprnJxIq8zpFew3XCp/nBKp+hyUsdf98vmEaG3XctQ==
"@lexical/overflow@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.6.4.tgz#0f0139a54916d3ff8e0a6fadb55804a45d2afa5e"
integrity sha512-Mdcpi2PyWTaDpEfTBAGSEX+5E0v/okSTLoIg1eDO3Q3VgGEkr5++FrJH8rVGIl6KIJeAa1WVM0vcu/W4O6y5ng==
"@lexical/plain-text@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.6.4.tgz#003e8f712d49e117d55a8add31516277757b384a"
integrity sha512-VB1zKqyef+3Vs8SVwob1HvCGRRfn4bypyKUyk93kk6LQDpjiGC6Go2OyuPX0EuJeFnQBYmYd3Bi205k/nVjfZA==
"@lexical/react@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.6.4.tgz#80e524701ec83797fd66b89d1bc04c63afc0d9fc"
integrity sha512-KAbNF/H5TzwCBFe5zuxz9ZSxj6a9ol8a2JkhACriO4HqWM0mURsxrXU8g3hySFpp4W7tdjpGSWsHLTdnaovHzw==
dependencies:
"@lexical/clipboard" "0.6.4"
"@lexical/code" "0.6.4"
"@lexical/dragon" "0.6.4"
"@lexical/hashtag" "0.6.4"
"@lexical/history" "0.6.4"
"@lexical/link" "0.6.4"
"@lexical/list" "0.6.4"
"@lexical/mark" "0.6.4"
"@lexical/markdown" "0.6.4"
"@lexical/overflow" "0.6.4"
"@lexical/plain-text" "0.6.4"
"@lexical/rich-text" "0.6.4"
"@lexical/selection" "0.6.4"
"@lexical/table" "0.6.4"
"@lexical/text" "0.6.4"
"@lexical/utils" "0.6.4"
"@lexical/yjs" "0.6.4"
react-error-boundary "^3.1.4"
"@lexical/rich-text@0.6.4", "@lexical/rich-text@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.6.4.tgz#d00e2621d0113ed842178f505618060cde2f2b0f"
integrity sha512-GUTAEUPmSKzL1kldvdHqM9IgiAJC1qfMeDQFyUS2xwWKQnid0nVeUZXNxyBwxZLyOcyDkx5dXp9YiEO6X4x+TQ==
"@lexical/selection@0.6.4", "@lexical/selection@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.6.4.tgz#2a3c8537c1e9e8bf492ccd6fbaafcfb02fea231a"
integrity sha512-dmrIQCQJOKARS7VRyE9WEKRaqP8SG9Xtzm8Bsk6+P0up1yWzlUvww+2INKr0bUUeQmI7DJxo5PX68qcoLeTAUg==
"@lexical/table@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.6.4.tgz#a07b642899e40c5981ab81b6ac541944bfef19ed"
integrity sha512-PjmRd5w5Gy4ht4i3IpQag/bsOi5V3/Fd0RQaD2gFaTFCh49NsYWrJcV6K1tH7xpO4PDJFibC5At2EiVKwhMyFA==
dependencies:
"@lexical/utils" "0.6.4"
"@lexical/text@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.6.4.tgz#49267e7a9395720b6361ca12631e0370c118c03a"
integrity sha512-gCANONCi3J2zf+yxv2CPEj2rsxpUBgQuR4TymGjsoVFsHqjRc3qHF5lNlbpWjPL5bJDSHFJySwn4/P20GNWggg==
"@lexical/utils@0.6.4", "@lexical/utils@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.6.4.tgz#7e1d79dd112efbcc048088714a3dc8e403b5aee5"
integrity sha512-JPFC+V65Eu+YTfThzV+LEGWj/QXre91Yq6c+P3VMWNfck4ZC1Enc6v7ntP9UI7FYFyM5NfwvWTzjUGEOYGajwg==
dependencies:
"@lexical/list" "0.6.4"
"@lexical/table" "0.6.4"
"@lexical/yjs@0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.6.4.tgz#0773a7a7abbd3d32bb16e7ff84d9f89ee2284996"
integrity sha512-QlhB/gIB+/3d9nP5zgnR8oJZ++14vw1VxgjBjXs1pW+32ho4ET4Wa+E0OHuVSwozEhJsC4wT1Q1C38oX+yRHdQ==
dependencies:
"@lexical/offset" "0.6.4"
"@mdn/browser-compat-data@^3.3.14":
version "3.3.14"
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28"
@ -7962,6 +8115,11 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lexical@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.6.4.tgz#00f5070a967fd1410aaa7d4c928f6e61af73c604"
integrity sha512-QBJEowv2FRkanu9V4HxCiR/V0rECt98zUeHsaYcgUCphrxbZseQgk8AO5UbHJMgAD4iQ0CCKZWbiqbv7Z8tYnw==
li@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b"
@ -9618,6 +9776,11 @@ prettyjson@^1.2.1:
colors "1.4.0"
minimist "^1.2.0"
prismjs@^1.27.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -9794,7 +9957,7 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-error-boundary@^3.1.0:
react-error-boundary@^3.1.0, react-error-boundary@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==