diff --git a/package.json b/package.json index b68f5c5ad..3883b2450 100644 --- a/package.json +++ b/package.json @@ -50,17 +50,12 @@ "@fontsource/inter": "^5.0.0", "@fontsource/roboto-mono": "^5.0.0", "@gamestdio/websocket": "^0.3.2", - "@lexical/clipboard": "^0.11.3", - "@lexical/code": "^0.12.0", - "@lexical/hashtag": "^0.11.3", - "@lexical/html": "^0.11.3", - "@lexical/link": "^0.11.3", - "@lexical/list": "^0.11.3", - "@lexical/react": "^0.11.3", - "@lexical/rich-text": "^0.11.3", - "@lexical/selection": "^0.11.3", - "@lexical/table": "^0.11.3", - "@lexical/utils": "^0.11.3", + "@lexical/clipboard": "^0.12.2", + "@lexical/hashtag": "^0.12.2", + "@lexical/link": "^0.12.2", + "@lexical/react": "^0.12.2", + "@lexical/selection": "^0.12.2", + "@lexical/utils": "^0.12.2", "@popperjs/core": "^2.11.5", "@reach/combobox": "^0.18.0", "@reach/menu-button": "^0.18.0", @@ -123,7 +118,7 @@ "intl-messageformat-parser": "^6.0.0", "intl-pluralrules": "^2.0.0", "leaflet": "^1.8.0", - "lexical": "^0.11.3", + "lexical": "^0.12.2", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash": "^4.7.11", diff --git a/src/features/compose/editor/nodes/image-component.tsx b/src/features/compose/editor/nodes/image-component.tsx deleted file mode 100644 index 0bc917686..000000000 --- a/src/features/compose/editor/nodes/image-component.tsx +++ /dev/null @@ -1,359 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. - */ - -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'; -import { mergeRegister } from '@lexical/utils'; -import clsx from 'clsx'; -import { List as ImmutableList } from 'immutable'; -import { - $getNodeByKey, - $getSelection, - $isNodeSelection, - $setSelection, - CLICK_COMMAND, - COMMAND_PRIORITY_LOW, - DRAGSTART_COMMAND, - KEY_BACKSPACE_COMMAND, - KEY_DELETE_COMMAND, - KEY_ENTER_COMMAND, - KEY_ESCAPE_COMMAND, - SELECTION_CHANGE_COMMAND, -} from 'lexical'; -import * as React from 'react'; -import { Suspense, useCallback, useEffect, useRef, useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; - -import { openModal } from 'soapbox/actions/modals'; -import { HStack, IconButton } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; -import { normalizeAttachment } from 'soapbox/normalizers'; - -import { $isImageNode } from './image-node'; - -import type { - GridSelection, - LexicalEditor, - NodeKey, - NodeSelection, - RangeSelection, -} from 'lexical'; - -const messages = defineMessages({ - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, -}); - -const imageCache = new Set(); - -const useSuspenseImage = (src: string) => { - if (!imageCache.has(src)) { - throw new Promise((resolve) => { - const img = new Image(); - img.src = src; - img.onload = () => { - imageCache.add(src); - resolve(null); - }; - }); - } -}; - -const LazyImage = ({ - altText, - className, - imageRef, - src, -}: { - altText: string - className: string | null - imageRef: {current: null | HTMLImageElement} - src: string -}): JSX.Element => { - useSuspenseImage(src); - return ( - {altText} - ); -}; - -const ImageComponent = ({ - src, - altText, - nodeKey, -}: { - altText: string - nodeKey: NodeKey - src: string -}): JSX.Element => { - const intl = useIntl(); - const dispatch = useAppDispatch(); - - const imageRef = useRef(null); - const buttonRef = useRef(null); - const [isSelected, setSelected, clearSelection] = - useLexicalNodeSelection(nodeKey); - const [editor] = useLexicalComposerContext(); - const [selection, setSelection] = useState< - RangeSelection | NodeSelection | GridSelection | null - >(null); - const activeEditorRef = useRef(null); - - const [hovered, setHovered] = useState(false); - const [focused, setFocused] = useState(false); - const [dirtyDescription, setDirtyDescription] = useState(null); - - const deleteNode = useCallback( - () => { - editor.update(() => { - const node = $getNodeByKey(nodeKey); - if ($isImageNode(node)) { - node.remove(); - } - }); - }, - [nodeKey], - ); - - const previewImage = () => { - const image = normalizeAttachment({ - type: 'image', - url: src, - altText, - }); - - dispatch(openModal('MEDIA', { media: ImmutableList.of(image), index: 0 })); - }; - - const onDelete = useCallback( - (payload: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { - const event: KeyboardEvent = payload; - event.preventDefault(); - deleteNode(); - } - return false; - }, - [isSelected, nodeKey], - ); - - const onEnter = useCallback( - (event: KeyboardEvent) => { - const latestSelection = $getSelection(); - const buttonElem = buttonRef.current; - if (isSelected && $isNodeSelection(latestSelection) && latestSelection.getNodes().length === 1) { - if (buttonElem !== null && buttonElem !== document.activeElement) { - event.preventDefault(); - buttonElem.focus(); - return true; - } - } - return false; - }, - [isSelected], - ); - - const onEscape = useCallback( - (event: KeyboardEvent) => { - if (buttonRef.current === event.target) { - $setSelection(null); - editor.update(() => { - setSelected(true); - const parentRootElement = editor.getRootElement(); - if (parentRootElement !== null) { - parentRootElement.focus(); - } - }); - return true; - } - return false; - }, - [editor, setSelected], - ); - - const handleKeyDown: React.KeyboardEventHandler = (e) => { - if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { - handleInputBlur(); - } - }; - - const handleInputBlur = () => { - setFocused(false); - - if (dirtyDescription !== null) { - editor.update(() => { - const node = $getNodeByKey(nodeKey); - if ($isImageNode(node)) { - node.setAltText(dirtyDescription); - } - - setDirtyDescription(null); - }); - } - }; - - const handleInputChange: React.ChangeEventHandler = e => { - setDirtyDescription(e.target.value); - }; - - const handleMouseEnter = () => { - setHovered(true); - }; - - const handleMouseLeave = () => { - setHovered(false); - }; - - const handleInputFocus = () => { - setFocused(true); - }; - - const handleClick = () => { - setFocused(true); - }; - - useEffect(() => { - let isMounted = true; - const unregister = mergeRegister( - editor.registerUpdateListener(({ editorState }) => { - if (isMounted) { - setSelection(editorState.read(() => $getSelection())); - } - }), - editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_, activeEditor) => { - activeEditorRef.current = activeEditor; - return false; - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - CLICK_COMMAND, - (payload) => { - const event = payload; - - if (event.target === imageRef.current) { - if (event.shiftKey) { - setSelected(!isSelected); - } else { - clearSelection(); - setSelected(true); - } - return true; - } - - return false; - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - DRAGSTART_COMMAND, - (event) => { - if (event.target === imageRef.current) { - // TODO This is just a temporary workaround for FF to behave like other browsers. - // Ideally, this handles drag & drop too (and all browsers). - event.preventDefault(); - return true; - } - return false; - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - KEY_DELETE_COMMAND, - onDelete, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - KEY_BACKSPACE_COMMAND, - onDelete, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_LOW), - editor.registerCommand( - KEY_ESCAPE_COMMAND, - onEscape, - COMMAND_PRIORITY_LOW, - ), - ); - return () => { - isMounted = false; - unregister(); - }; - }, [ - clearSelection, - editor, - isSelected, - nodeKey, - onDelete, - onEnter, - onEscape, - setSelected, - ]); - - const active = hovered || focused; - const description = dirtyDescription || (dirtyDescription !== '' && altText) || ''; - const draggable = isSelected && $isNodeSelection(selection); - - return ( - - <> -
- - - - - -
-