From 26951e84773608f2c442ba8598932031d59407dd Mon Sep 17 00:00:00 2001 From: shixiaohua Date: Tue, 27 Feb 2024 16:54:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../context/SharedHistoryContext/index.jsx | 19 + .../Note/Hlexical/createWebsocketProvider.js | 33 + .../Note/Hlexical/nodes/EmojiNode/index.jsx | 79 ++ .../nodes/ImageNode/ImageComponent/index.jsx | 270 +++++++ .../nodes/ImageNode/ImageComponent/index.less | 0 .../Note/Hlexical/nodes/ImageNode/index.jsx | 42 +- .../Note/Hlexical/nodes/KeywordNode/index.jsx | 56 ++ .../Note/Hlexical/nodes/MentionNode/index.jsx | 111 +++ .../Hlexical/plugins/EmojisPlugin/index.jsx | 69 ++ .../Hlexical/plugins/ImagesPlugin/index.jsx | 28 +- .../plugins/Input/ContentEditable/index.jsx | 10 + .../plugins/Input/ContentEditable/index.less | 15 + .../Note/Hlexical/plugins/Input/FileInput.jsx | 21 +- .../plugins/Input/ImageResizer/index.jsx | 285 ++++++++ .../plugins/Input/Placeholder/index.jsx | 11 + .../plugins/Input/Placeholder/index.less | 19 + .../Hlexical/plugins/KeywordsPlugin/index.jsx | 45 ++ .../Hlexical/plugins/LinkPlugin/index.jsx | 8 + .../Hlexical/plugins/MentionsPlugin/index.jsx | 682 ++++++++++++++++++ .../Note/Hlexical/plugins/SaveFilePlugin.js | 15 +- .../Note/Hlexical/plugins/ToolbarPlugin.js | 2 +- .../Hlexical/plugins/TreeViewPlugin/index.jsx | 19 + src/pages/Note/Hlexical/utils/url/index.jsx | 27 + 23 files changed, 1813 insertions(+), 53 deletions(-) create mode 100644 src/pages/Note/Hlexical/context/SharedHistoryContext/index.jsx create mode 100644 src/pages/Note/Hlexical/createWebsocketProvider.js create mode 100644 src/pages/Note/Hlexical/nodes/EmojiNode/index.jsx create mode 100644 src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.jsx delete mode 100644 src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less create mode 100644 src/pages/Note/Hlexical/nodes/KeywordNode/index.jsx create mode 100644 src/pages/Note/Hlexical/nodes/MentionNode/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/EmojisPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.less create mode 100644 src/pages/Note/Hlexical/plugins/Input/ImageResizer/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/Placeholder/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/Input/Placeholder/index.less create mode 100644 src/pages/Note/Hlexical/plugins/KeywordsPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/LinkPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/MentionsPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/plugins/TreeViewPlugin/index.jsx create mode 100644 src/pages/Note/Hlexical/utils/url/index.jsx diff --git a/src/pages/Note/Hlexical/context/SharedHistoryContext/index.jsx b/src/pages/Note/Hlexical/context/SharedHistoryContext/index.jsx new file mode 100644 index 0000000..a790a0d --- /dev/null +++ b/src/pages/Note/Hlexical/context/SharedHistoryContext/index.jsx @@ -0,0 +1,19 @@ +import {createEmptyHistoryState} from '@lexical/react/LexicalHistoryPlugin'; +import * as React from 'react'; +import {createContext, ReactNode, useContext, useMemo} from 'react'; + +const Context = createContext({}); + +export const SharedHistoryContext = ({ + children, +}) => { + const historyContext = useMemo( + () => ({historyState: createEmptyHistoryState()}), + [], + ); + return {children}; +}; + +export const useSharedHistoryContext = () => { + return useContext(Context); +}; diff --git a/src/pages/Note/Hlexical/createWebsocketProvider.js b/src/pages/Note/Hlexical/createWebsocketProvider.js new file mode 100644 index 0000000..823495f --- /dev/null +++ b/src/pages/Note/Hlexical/createWebsocketProvider.js @@ -0,0 +1,33 @@ +// import {WebsocketProvider} from 'y-websocket'; +// import {Doc} from 'yjs'; +// +// const url = new URL(window.location.href); +// const params = new URLSearchParams(url.search); +// const WEBSOCKET_ENDPOINT = +// params.get('collabEndpoint') || 'ws://localhost:1234'; +// const WEBSOCKET_SLUG = 'playground'; +// const WEBSOCKET_ID = params.get('collabId') || '0'; +// +// export function createWebsocketProvider( +// id, +// yjsDocMap, +// ) { +// let doc = yjsDocMap.get(id); +// +// if (doc === undefined) { +// doc = new Doc(); +// yjsDocMap.set(id, doc); +// } else { +// doc.load(); +// } +// +// // @ts-ignore +// return new WebsocketProvider( +// WEBSOCKET_ENDPOINT, +// WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id, +// doc, +// { +// connect: false, +// }, +// ); +// } diff --git a/src/pages/Note/Hlexical/nodes/EmojiNode/index.jsx b/src/pages/Note/Hlexical/nodes/EmojiNode/index.jsx new file mode 100644 index 0000000..b99ff52 --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/EmojiNode/index.jsx @@ -0,0 +1,79 @@ +import {$applyNodeReplacement, TextNode} from 'lexical'; + +export class EmojiNode extends TextNode { + __className; + + static getType() { + return 'emoji'; + } + + static clone(node) { + return new EmojiNode(node.__className, node.__text, node.__key); + } + + constructor(className, text, key) { + super(text, key); + this.__className = className; +} + +createDOM(config) { + const dom = document.createElement('span'); + const inner = super.createDOM(config); + dom.className = this.__className; + inner.className = 'emoji-inner'; + dom.appendChild(inner); + return dom; +} + +updateDOM( + prevNode, + dom, + config, +) { + const inner = dom.firstChild; + if (inner === null) { + return true; + } + super.updateDOM(prevNode, inner , config); + return false; +} + +static importJSON(serializedNode) { + const node = $createEmojiNode( + serializedNode.className, + serializedNode.text, + ); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; +} + +exportJSON() { + return { + ...super.exportJSON(), + className: this.getClassName(), + type: 'emoji', + }; +} + +getClassName() { + const self = this.getLatest(); + return self.__className; +} +} + +export function $isEmojiNode( + node, +) { + return node instanceof EmojiNode; +} + +export function $createEmojiNode( + className, + emojiText, +) { + const node = new EmojiNode(className, emojiText).setMode('token'); + return $applyNodeReplacement(node); +} diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.jsx b/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.jsx new file mode 100644 index 0000000..1d2787e --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.jsx @@ -0,0 +1,270 @@ +import '../index.less'; +import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin'; +import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext'; +import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; +import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin'; +import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; +import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer'; +import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; +import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; +import {mergeRegister} from '@lexical/utils'; +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 {createWebsocketProvider} from '../../../createWebsocketProvider'; + +import {useSharedHistoryContext} from '../../../context/SharedHistoryContext'; +import EmojisPlugin from '../../../plugins/EmojisPlugin'; +import KeywordsPlugin from '../../../plugins/KeywordsPlugin'; +import LinkPlugin from '../../../plugins/LinkPlugin'; + +// import MentionsPlugin from '../../../plugins/MentionsPlugin'; +import TreeViewPlugin from '../../../plugins/TreeViewPlugin'; +import ContentEditable from '../../../plugins/Input/ContentEditable'; +import ImageResizer from '../../../plugins/Input/ImageResizer'; +import Placeholder from '../../../plugins/Input/Placeholder'; + +import {$isImageNode} from '../index'; + +const imageCache = new Set(); + +function useSuspenseImage(src) { + if (!imageCache.has(src)) { + throw new Promise((resolve) => { + const img = new Image(); + img.src = src; + img.onload = () => { + imageCache.add(src); + resolve(null); + }; + }); + } +} + +function LazyImage({altText, className, imageRef, src, width, height, maxWidth,}) { + useSuspenseImage(src); + return ({altText}); +} + +export default function ImageComponent({ + src, + altText, + nodeKey, + width, + height, + maxWidth, + resizable, + showCaption, + caption, + captionsEnabled, + }) { + const imageRef = useRef(null); + const buttonRef = useRef(null); + const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey); + const [isResizing, setIsResizing] = useState(false); + const {isCollabActive} = useCollaborationContext(); + const [editor] = useLexicalComposerContext(); + const [selection, setSelection] = useState(null); + const activeEditorRef = useRef(null); + + const onDelete = useCallback((payload) => { + if (isSelected && $isNodeSelection($getSelection())) { + const event = payload; + event.preventDefault(); + const node = $getNodeByKey(nodeKey); + if ($isImageNode(node)) { + node.remove(); + } + } + return false; + }, [isSelected, nodeKey],); + + const onEnter = useCallback((event) => { + const latestSelection = $getSelection(); + const buttonElem = buttonRef.current; + if (isSelected && $isNodeSelection(latestSelection) && latestSelection.getNodes().length === 1) { + if (showCaption) { + // Move focus into nested editor + $setSelection(null); + event.preventDefault(); + caption.focus(); + return true; + } else if (buttonElem !== null && buttonElem !== document.activeElement) { + event.preventDefault(); + buttonElem.focus(); + return true; + } + } + return false; + }, [caption, isSelected, showCaption],); + + const onEscape = useCallback((event) => { + if (activeEditorRef.current === caption || buttonRef.current === event.target) { + $setSelection(null); + editor.update(() => { + setSelected(true); + const parentRootElement = editor.getRootElement(); + if (parentRootElement !== null) { + parentRootElement.focus(); + } + }); + return true; + } + return false; + }, [caption, editor, setSelected],); + + 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 (isResizing) { + return true; + } + 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, isResizing, isSelected, nodeKey, onDelete, onEnter, onEscape, setSelected,]); + + const setShowCaption = () => { + editor.update(() => { + const node = $getNodeByKey(nodeKey); + if ($isImageNode(node)) { + node.setShowCaption(true); + } + }); + }; + + const onResizeEnd = (nextWidth, nextHeight,) => { + // Delay hiding the resize bars for click case + setTimeout(() => { + setIsResizing(false); + }, 200); + + editor.update(() => { + const node = $getNodeByKey(nodeKey); + if ($isImageNode(node)) { + node.setWidthAndHeight(nextWidth, nextHeight); + } + }); + }; + + const onResizeStart = () => { + setIsResizing(true); + }; + + const {historyState} = useSharedHistoryContext(); + + const draggable = isSelected && $isNodeSelection(selection) && !isResizing; + const isFocused = isSelected || isResizing; + return ( + <> +
+ +
+ {showCaption && (
+ + + {/**/} + + + + + {isCollabActive ? () : ()} + } + placeholder={ + Enter a caption... + } + ErrorBoundary={LexicalErrorBoundary} + /> + {showNestedEditorTreeView === true ? : null} + +
)} + {resizable && $isNodeSelection(selection) && isFocused && ()} + +
); +} diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less b/src/pages/Note/Hlexical/nodes/ImageNode/ImageComponent/index.less deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx b/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx index fce57a4..73b9fa6 100644 --- a/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx +++ b/src/pages/Note/Hlexical/nodes/ImageNode/index.jsx @@ -2,9 +2,9 @@ import {$applyNodeReplacement, createEditor, DecoratorNode} from 'lexical'; import * as React from 'react'; import {Suspense} from 'react'; -// const ImageComponent = React.lazy( -// () => import('../ImageComponent'), -// ); +const ImageComponent = React.lazy( + () => import('./ImageComponent'), +); function convertImageElement(domNode) { if (domNode instanceof HTMLImageElement) { @@ -153,24 +153,24 @@ export class ImageNode extends DecoratorNode { return this.__altText; } - // decorate() { - // return ( - // - // - // - // ); - // } + decorate() { + return ( + + + + ); + } } export function $createImageNode({ diff --git a/src/pages/Note/Hlexical/nodes/KeywordNode/index.jsx b/src/pages/Note/Hlexical/nodes/KeywordNode/index.jsx new file mode 100644 index 0000000..e77114f --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/KeywordNode/index.jsx @@ -0,0 +1,56 @@ +import {TextNode} from 'lexical'; + + +export class KeywordNode extends TextNode { + static getType() { + return 'keyword'; + } + + static clone(node) { + return new KeywordNode(node.__text, node.__key); + } + + static importJSON(serializedNode) { + const node = $createKeywordNode(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + exportJSON() { + return { + ...super.exportJSON(), + type: 'keyword', + version: 1, + }; + } + + createDOM(config) { + const dom = super.createDOM(config); + dom.style.cursor = 'default'; + dom.className = 'keyword'; + return dom; + } + + canInsertTextBefore() { + return false; + } + + canInsertTextAfter() { + return false; + } + + isTextEntity() { + return true; + } +} + +export function $createKeywordNode(keyword) { + return new KeywordNode(keyword); +} + +export function $isKeywordNode(node) { + return node instanceof KeywordNode; +} diff --git a/src/pages/Note/Hlexical/nodes/MentionNode/index.jsx b/src/pages/Note/Hlexical/nodes/MentionNode/index.jsx new file mode 100644 index 0000000..748ff9c --- /dev/null +++ b/src/pages/Note/Hlexical/nodes/MentionNode/index.jsx @@ -0,0 +1,111 @@ +import {Spread} from 'lexical'; + +import { + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + LexicalNode, + NodeKey, + SerializedTextNode, + $applyNodeReplacement, + TextNode, +} from 'lexical'; + + +function convertMentionElement(domNode,) { + const textContent = domNode.textContent; + + if (textContent !== null) { + const node = $createMentionNode(textContent); + return { + node, + }; + } + + return null; +} + +const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)'; + +export class MentionNode extends TextNode { + __mention; + + static getType() { + return 'mention'; + } + + static clone(node) { + return new MentionNode(node.__mention, node.__text, node.__key); + } + + static importJSON(serializedNode) { + const node = $createMentionNode(serializedNode.mentionName); + node.setTextContent(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + constructor(mentionName, text, key) { + super(text ?? mentionName, key); + this.__mention = mentionName; + } + + exportJSON() { + return { + ...super.exportJSON(), mentionName: this.__mention, type: 'mention', version: 1, + }; + } + + createDOM(config) { + const dom = super.createDOM(config); + dom.style.cssText = mentionStyle; + dom.className = 'mention'; + return dom; + } + + exportDOM() { + const element = document.createElement('span'); + element.setAttribute('data-lexical-mention', 'true'); + element.textContent = this.__text; + return {element}; + } + + static importDOM() { + return { + span: (domNode) => { + if (!domNode.hasAttribute('data-lexical-mention')) { + return null; + } + return { + conversion: convertMentionElement, priority: 1, + }; + }, + }; + } + + isTextEntity() { + return true; + } + + canInsertTextBefore() { + return false; + } + + canInsertTextAfter() { + return false; + } +} + +export function $createMentionNode(mentionName) { + const mentionNode = new MentionNode(mentionName); + mentionNode.setMode('segmented').toggleDirectionless(); + return $applyNodeReplacement(mentionNode); +} + +export function $isMentionNode(node,) { + return node instanceof MentionNode; +} diff --git a/src/pages/Note/Hlexical/plugins/EmojisPlugin/index.jsx b/src/pages/Note/Hlexical/plugins/EmojisPlugin/index.jsx new file mode 100644 index 0000000..4948623 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/EmojisPlugin/index.jsx @@ -0,0 +1,69 @@ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {TextNode} from 'lexical'; +import {useEffect} from 'react'; + +import {$createEmojiNode, EmojiNode} from '../../nodes/EmojiNode'; + +const emojis = new Map([ + [':)', ['emoji happysmile', '🙂']], + [':D', ['emoji veryhappysmile', '😀']], + [':(', ['emoji unhappysmile', '🙁']], + ['<3', ['emoji heart', '❤']], + ['🙂', ['emoji happysmile', '🙂']], + ['😀', ['emoji veryhappysmile', '😀']], + ['🙁', ['emoji unhappysmile', '🙁']], + ['❤', ['emoji heart', '❤']], +]); + +function findAndTransformEmoji(node) { + const text = node.getTextContent(); + + for (let i = 0; i < text.length; i++) { + const emojiData = emojis.get(text[i]) || emojis.get(text.slice(i, i + 2)); + + if (emojiData !== undefined) { + const [emojiStyle, emojiText] = emojiData; + let targetNode; + + if (i === 0) { + [targetNode] = node.splitText(i + 2); + } else { + [, targetNode] = node.splitText(i, i + 2); + } + + const emojiNode = $createEmojiNode(emojiStyle, emojiText); + targetNode.replace(emojiNode); + return emojiNode; + } + } + + return null; +} + +function textNodeTransform(node) { + let targetNode = node; + + while (targetNode !== null) { + if (!targetNode.isSimpleText()) { + return; + } + + targetNode = findAndTransformEmoji(targetNode); + } +} + +function useEmojis(editor) { + useEffect(() => { + if (!editor.hasNodes([EmojiNode])) { + throw new Error('EmojisPlugin: EmojiNode not registered on editor'); + } + + return editor.registerNodeTransform(TextNode, textNodeTransform); + }, [editor]); +} + +export default function EmojisPlugin(){ + const [editor] = useLexicalComposerContext(); + useEmojis(editor); + return null; +} diff --git a/src/pages/Note/Hlexical/plugins/ImagesPlugin/index.jsx b/src/pages/Note/Hlexical/plugins/ImagesPlugin/index.jsx index 1fbe1ee..24d4a61 100644 --- a/src/pages/Note/Hlexical/plugins/ImagesPlugin/index.jsx +++ b/src/pages/Note/Hlexical/plugins/ImagesPlugin/index.jsx @@ -97,14 +97,14 @@ export function InsertImageUploadedDialogBody({ return ( <> {!mode && ( - )} diff --git a/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.jsx b/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.jsx new file mode 100644 index 0000000..18a67fc --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.jsx @@ -0,0 +1,10 @@ +import './index.less'; + +import {ContentEditable} from '@lexical/react/LexicalContentEditable'; +import * as React from 'react'; + +export default function LexicalContentEditable({ + className, + }) { + return ; +} diff --git a/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.less b/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.less new file mode 100644 index 0000000..c5b09e7 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/ContentEditable/index.less @@ -0,0 +1,15 @@ +.ContentEditable__root { + border: 0; + font-size: 15px; + display: block; + position: relative; + outline: 0; + padding: 8px 28px 40px; + min-height: 150px; +} +@media (max-width: 1025px) { + .ContentEditable__root { + padding-left: 8px; + padding-right: 8px; + } +} diff --git a/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx b/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx index fcabecd..a4a821a 100644 --- a/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx +++ b/src/pages/Note/Hlexical/plugins/Input/FileInput.jsx @@ -1,6 +1,7 @@ import './index.less'; - +import { UploadOutlined } from '@ant-design/icons'; import * as React from 'react'; +import {Button, Upload} from "antd"; export default function FileInput({ accept, @@ -8,16 +9,34 @@ export default function FileInput({ onChange, 'data-test-id': dataTestId, }) { + + const props = { + beforeUpload: (file) => { + const isPNG = file.type === 'image/png'; + if (!isPNG) { + message.error(`${file.name} is not a png file`); + } + return isPNG || Upload.LIST_IGNORE; + }, + onChange: (info) => { + console.log(info.fileList); + }, + }; + return (
onChange(e.target.files)} data-test-id={dataTestId} /> + {/**/} + {/* */} + {/**/}
); } diff --git a/src/pages/Note/Hlexical/plugins/Input/ImageResizer/index.jsx b/src/pages/Note/Hlexical/plugins/Input/ImageResizer/index.jsx new file mode 100644 index 0000000..754deb8 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/ImageResizer/index.jsx @@ -0,0 +1,285 @@ +import {LexicalEditor} from 'lexical'; + +import * as React from 'react'; +import {useRef} from 'react'; + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + +const Direction = { + east: 1 << 0, + north: 1 << 3, + south: 1 << 1, + west: 1 << 2, +}; + +export default function ImageResizer({ + onResizeStart, + onResizeEnd, + buttonRef, + imageRef, + maxWidth, + editor, + showCaption, + setShowCaption, + captionsEnabled, + }){ + const controlWrapperRef = useRef(null); + const userSelect = useRef({ + priority: '', + value: 'default', + }); + const positioningRef = useRef({ + currentHeight: 0, + currentWidth: 0, + direction: 0, + isResizing: false, + ratio: 0, + startHeight: 0, + startWidth: 0, + startX: 0, + startY: 0, + }); + const editorRootElement = editor.getRootElement(); + // Find max width, accounting for editor padding. + const maxWidthContainer = maxWidth + ? maxWidth + : editorRootElement !== null + ? editorRootElement.getBoundingClientRect().width - 20 + : 100; + const maxHeightContainer = + editorRootElement !== null + ? editorRootElement.getBoundingClientRect().height - 20 + : 100; + + const minWidth = 100; + const minHeight = 100; + + const setStartCursor = (direction) => { + const ew = direction === Direction.east || direction === Direction.west; + const ns = direction === Direction.north || direction === Direction.south; + const nwse = + (direction & Direction.north && direction & Direction.west) || + (direction & Direction.south && direction & Direction.east); + + const cursorDir = ew ? 'ew' : ns ? 'ns' : nwse ? 'nwse' : 'nesw'; + + if (editorRootElement !== null) { + editorRootElement.style.setProperty( + 'cursor', + `${cursorDir}-resize`, + 'important', + ); + } + if (document.body !== null) { + document.body.style.setProperty( + 'cursor', + `${cursorDir}-resize`, + 'important', + ); + userSelect.current.value = document.body.style.getPropertyValue( + '-webkit-user-select', + ); + userSelect.current.priority = document.body.style.getPropertyPriority( + '-webkit-user-select', + ); + document.body.style.setProperty( + '-webkit-user-select', + `none`, + 'important', + ); + } + }; + + const setEndCursor = () => { + if (editorRootElement !== null) { + editorRootElement.style.setProperty('cursor', 'text'); + } + if (document.body !== null) { + document.body.style.setProperty('cursor', 'default'); + document.body.style.setProperty( + '-webkit-user-select', + userSelect.current.value, + userSelect.current.priority, + ); + } + }; + + const handlePointerDown = ( + event, + direction, + ) => { + if (!editor.isEditable()) { + return; + } + + const image = imageRef.current; + const controlWrapper = controlWrapperRef.current; + + if (image !== null && controlWrapper !== null) { + event.preventDefault(); + const {width, height} = image.getBoundingClientRect(); + const positioning = positioningRef.current; + positioning.startWidth = width; + positioning.startHeight = height; + positioning.ratio = width / height; + positioning.currentWidth = width; + positioning.currentHeight = height; + positioning.startX = event.clientX; + positioning.startY = event.clientY; + positioning.isResizing = true; + positioning.direction = direction; + + setStartCursor(direction); + onResizeStart(); + + controlWrapper.classList.add('image-control-wrapper--resizing'); + image.style.height = `${height}px`; + image.style.width = `${width}px`; + + document.addEventListener('pointermove', handlePointerMove); + document.addEventListener('pointerup', handlePointerUp); + } + }; + const handlePointerMove = (event) => { + const image = imageRef.current; + const positioning = positioningRef.current; + + const isHorizontal = + positioning.direction & (Direction.east | Direction.west); + const isVertical = + positioning.direction & (Direction.south | Direction.north); + + if (image !== null && positioning.isResizing) { + // Corner cursor + if (isHorizontal && isVertical) { + let diff = Math.floor(positioning.startX - event.clientX); + diff = positioning.direction & Direction.east ? -diff : diff; + + const width = clamp( + positioning.startWidth + diff, + minWidth, + maxWidthContainer, + ); + + const height = width / positioning.ratio; + image.style.width = `${width}px`; + image.style.height = `${height}px`; + positioning.currentHeight = height; + positioning.currentWidth = width; + } else if (isVertical) { + let diff = Math.floor(positioning.startY - event.clientY); + diff = positioning.direction & Direction.south ? -diff : diff; + + const height = clamp( + positioning.startHeight + diff, + minHeight, + maxHeightContainer, + ); + + image.style.height = `${height}px`; + positioning.currentHeight = height; + } else { + let diff = Math.floor(positioning.startX - event.clientX); + diff = positioning.direction & Direction.east ? -diff : diff; + + const width = clamp( + positioning.startWidth + diff, + minWidth, + maxWidthContainer, + ); + + image.style.width = `${width}px`; + positioning.currentWidth = width; + } + } + }; + const handlePointerUp = () => { + const image = imageRef.current; + const positioning = positioningRef.current; + const controlWrapper = controlWrapperRef.current; + if (image !== null && controlWrapper !== null && positioning.isResizing) { + const width = positioning.currentWidth; + const height = positioning.currentHeight; + positioning.startWidth = 0; + positioning.startHeight = 0; + positioning.ratio = 0; + positioning.startX = 0; + positioning.startY = 0; + positioning.currentWidth = 0; + positioning.currentHeight = 0; + positioning.isResizing = false; + + controlWrapper.classList.remove('image-control-wrapper--resizing'); + + setEndCursor(); + onResizeEnd(width, height); + + document.removeEventListener('pointermove', handlePointerMove); + document.removeEventListener('pointerup', handlePointerUp); + } + }; + return ( +
+ {!showCaption && captionsEnabled && ( + + )} +
{ + handlePointerDown(event, Direction.north); + }} + /> +
{ + handlePointerDown(event, Direction.north | Direction.east); + }} + /> +
{ + handlePointerDown(event, Direction.east); + }} + /> +
{ + handlePointerDown(event, Direction.south | Direction.east); + }} + /> +
{ + handlePointerDown(event, Direction.south); + }} + /> +
{ + handlePointerDown(event, Direction.south | Direction.west); + }} + /> +
{ + handlePointerDown(event, Direction.west); + }} + /> +
{ + handlePointerDown(event, Direction.north | Direction.west); + }} + /> +
+ ); +} diff --git a/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.jsx b/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.jsx new file mode 100644 index 0000000..7c9e12e --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.jsx @@ -0,0 +1,11 @@ + +import './index.less'; + +import * as React from 'react'; + +export default function Placeholder({ + children, + className, + }) { + return
{children}
; +} diff --git a/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.less b/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.less new file mode 100644 index 0000000..38dbd3c --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/Input/Placeholder/index.less @@ -0,0 +1,19 @@ +.Placeholder__root { + font-size: 15px; + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 8px; + left: 28px; + right: 28px; + user-select: none; + white-space: nowrap; + display: inline-block; + pointer-events: none; +} +@media (max-width: 1025px) { + .Placeholder__root { + left: 8px; + } +} diff --git a/src/pages/Note/Hlexical/plugins/KeywordsPlugin/index.jsx b/src/pages/Note/Hlexical/plugins/KeywordsPlugin/index.jsx new file mode 100644 index 0000000..d941c04 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/KeywordsPlugin/index.jsx @@ -0,0 +1,45 @@ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {useLexicalTextEntity} from '@lexical/react/useLexicalTextEntity'; +import {useCallback, useEffect} from 'react'; + +import {$createKeywordNode, KeywordNode} from '../../nodes/KeywordNode'; + +const KEYWORDS_REGEX = + /(^|$|[^A-Za-zªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԧԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠࢢ-ࢬऄ-हऽॐक़-ॡॱ-ॷॹ-ॿঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚗꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞓꞠ-Ɦꟸ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꪀ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ])(congrats|congratulations|gratuluju|gratuluji|gratulujeme|blahopřeju|blahopřeji|blahopřejeme|Til lykke|Tillykke|Glückwunsch|Gratuliere|felicitaciones|enhorabuena|paljon onnea|onnittelut|Félicitations|gratula|gratulálok|gratulálunk|congratulazioni|complimenti|おめでとう|おめでとうございます|축하해|축하해요|gratulerer|Gefeliciteerd|gratulacje|Parabéns|parabéns|felicitações|felicitări|мои поздравления|поздравляем|поздравляю|gratulujem|blahoželám|ยินดีด้วย|ขอแสดงความยินดี|tebrikler|tebrik ederim|恭喜|祝贺你|恭喜你|恭喜|恭喜|baie geluk|veels geluk|অভিনন্দন|Čestitam|Čestitke|Čestitamo|Συγχαρητήρια|Μπράβο|અભિનંદન|badhai|बधाई|अभिनंदन|Честитам|Свака част|hongera|வாழ்த்துகள்|வாழ்த்துக்கள்|అభినందనలు|അഭിനന്ദനങ്ങൾ|Chúc mừng|מזל טוב|mazel tov|mazal tov)(^|$|[^A-Za-zªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԧԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠࢢ-ࢬऄ-हऽॐक़-ॡॱ-ॷॹ-ॿঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚗꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞓꞠ-Ɦꟸ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꪀ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ])/i; + +export default function KeywordsPlugin(){ + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!editor.hasNodes([KeywordNode])) { + throw new Error('KeywordsPlugin: KeywordNode not registered on editor'); + } + }, [editor]); + + const createKeywordNode = useCallback((textNode) => { + return $createKeywordNode(textNode.getTextContent()); + }, []); + + const getKeywordMatch = useCallback((text) => { + const matchArr = KEYWORDS_REGEX.exec(text); + + if (matchArr === null) { + return null; + } + + const hashtagLength = matchArr[2].length; + const startOffset = matchArr.index + matchArr[1].length; + const endOffset = startOffset + hashtagLength; + return { + end: endOffset, + start: startOffset, + }; + }, []); + + useLexicalTextEntity( + getKeywordMatch, + KeywordNode, + createKeywordNode, + ); + return null; +} diff --git a/src/pages/Note/Hlexical/plugins/LinkPlugin/index.jsx b/src/pages/Note/Hlexical/plugins/LinkPlugin/index.jsx new file mode 100644 index 0000000..e2d1023 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/LinkPlugin/index.jsx @@ -0,0 +1,8 @@ +import {LinkPlugin as LexicalLinkPlugin} from '@lexical/react/LexicalLinkPlugin'; +import * as React from 'react'; + +import {validateUrl} from '../../utils/url'; + +export default function LinkPlugin() { + return ; +} diff --git a/src/pages/Note/Hlexical/plugins/MentionsPlugin/index.jsx b/src/pages/Note/Hlexical/plugins/MentionsPlugin/index.jsx new file mode 100644 index 0000000..759b494 --- /dev/null +++ b/src/pages/Note/Hlexical/plugins/MentionsPlugin/index.jsx @@ -0,0 +1,682 @@ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import { + LexicalTypeaheadMenuPlugin, + MenuOption, + MenuTextMatch, + useBasicTypeaheadTriggerMatch, +} from '@lexical/react/LexicalTypeaheadMenuPlugin'; +import {TextNode} from 'lexical'; +import {useCallback, useEffect, useMemo, useState} from 'react'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import {$createMentionNode} from '../../nodes/MentionNode'; + +const PUNCTUATION = + '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'; +const NAME = '\\b[A-Z][^\\s' + PUNCTUATION + ']'; + +const DocumentMentionsRegex = { + NAME, + PUNCTUATION, +}; + +const PUNC = DocumentMentionsRegex.PUNCTUATION; + +const TRIGGERS = ['@'].join(''); + +// Chars we expect to see in a mention (non-space, non-punctuation). +const VALID_CHARS = '[^' + TRIGGERS + PUNC + '\\s]'; + +// Non-standard series of chars. Each series must be preceded and followed by +// a valid char. +const VALID_JOINS = + '(?:' + + '\\.[ |$]|' + // E.g. "r. " in "Mr. Smith" + ' |' + // E.g. " " in "Josh Duck" + '[' + + PUNC + + ']|' + // E.g. "-' in "Salier-Hellendag" + ')'; + +const LENGTH_LIMIT = 75; + +const AtSignMentionsRegex = new RegExp( + '(^|\\s|\\()(' + + '[' + + TRIGGERS + + ']' + + '((?:' + + VALID_CHARS + + VALID_JOINS + + '){0,' + + LENGTH_LIMIT + + '})' + + ')$', +); + +// 50 is the longest alias length limit. +const ALIAS_LENGTH_LIMIT = 50; + +// Regex used to match alias. +const AtSignMentionsRegexAliasRegex = new RegExp( + '(^|\\s|\\()(' + + '[' + + TRIGGERS + + ']' + + '((?:' + + VALID_CHARS + + '){0,' + + ALIAS_LENGTH_LIMIT + + '})' + + ')$', +); + +// At most, 5 suggestions are shown in the popup. +const SUGGESTION_LIST_LENGTH_LIMIT = 5; + +const mentionsCache = new Map(); + +const dummyMentionsData = [ + 'Aayla Secura', + 'Adi Gallia', + 'Admiral Dodd Rancit', + 'Admiral Firmus Piett', + 'Admiral Gial Ackbar', + 'Admiral Ozzel', + 'Admiral Raddus', + 'Admiral Terrinald Screed', + 'Admiral Trench', + 'Admiral U.O. Statura', + 'Agen Kolar', + 'Agent Kallus', + 'Aiolin and Morit Astarte', + 'Aks Moe', + 'Almec', + 'Alton Kastle', + 'Amee', + 'AP-5', + 'Armitage Hux', + 'Artoo', + 'Arvel Crynyd', + 'Asajj Ventress', + 'Aurra Sing', + 'AZI-3', + 'Bala-Tik', + 'Barada', + 'Bargwill Tomder', + 'Baron Papanoida', + 'Barriss Offee', + 'Baze Malbus', + 'Bazine Netal', + 'BB-8', + 'BB-9E', + 'Ben Quadinaros', + 'Berch Teller', + 'Beru Lars', + 'Bib Fortuna', + 'Biggs Darklighter', + 'Black Krrsantan', + 'Bo-Katan Kryze', + 'Boba Fett', + 'Bobbajo', + 'Bodhi Rook', + 'Borvo the Hutt', + 'Boss Nass', + 'Bossk', + 'Breha Antilles-Organa', + 'Bren Derlin', + 'Brendol Hux', + 'BT-1', + 'C-3PO', + 'C1-10P', + 'Cad Bane', + 'Caluan Ematt', + 'Captain Gregor', + 'Captain Phasma', + 'Captain Quarsh Panaka', + 'Captain Rex', + 'Carlist Rieekan', + 'Casca Panzoro', + 'Cassian Andor', + 'Cassio Tagge', + 'Cham Syndulla', + 'Che Amanwe Papanoida', + 'Chewbacca', + 'Chi Eekway Papanoida', + 'Chief Chirpa', + 'Chirrut Îmwe', + 'Ciena Ree', + 'Cin Drallig', + 'Clegg Holdfast', + 'Cliegg Lars', + 'Coleman Kcaj', + 'Coleman Trebor', + 'Colonel Kaplan', + 'Commander Bly', + 'Commander Cody (CC-2224)', + 'Commander Fil (CC-3714)', + 'Commander Fox', + 'Commander Gree', + 'Commander Jet', + 'Commander Wolffe', + 'Conan Antonio Motti', + 'Conder Kyl', + 'Constable Zuvio', + 'Cordé', + 'Cpatain Typho', + 'Crix Madine', + 'Cut Lawquane', + 'Dak Ralter', + 'Dapp', + 'Darth Bane', + 'Darth Maul', + 'Darth Tyranus', + 'Daultay Dofine', + 'Del Meeko', + 'Delian Mors', + 'Dengar', + 'Depa Billaba', + 'Derek Klivian', + 'Dexter Jettster', + 'Dineé Ellberger', + 'DJ', + 'Doctor Aphra', + 'Doctor Evazan', + 'Dogma', + 'Dormé', + 'Dr. Cylo', + 'Droidbait', + 'Droopy McCool', + 'Dryden Vos', + 'Dud Bolt', + 'Ebe E. Endocott', + 'Echuu Shen-Jon', + 'Eeth Koth', + 'Eighth Brother', + 'Eirtaé', + 'Eli Vanto', + 'Ellé', + 'Ello Asty', + 'Embo', + 'Eneb Ray', + 'Enfys Nest', + 'EV-9D9', + 'Evaan Verlaine', + 'Even Piell', + 'Ezra Bridger', + 'Faro Argyus', + 'Feral', + 'Fifth Brother', + 'Finis Valorum', + 'Finn', + 'Fives', + 'FN-1824', + 'FN-2003', + 'Fodesinbeed Annodue', + 'Fulcrum', + 'FX-7', + 'GA-97', + 'Galen Erso', + 'Gallius Rax', + 'Garazeb "Zeb" Orrelios', + 'Gardulla the Hutt', + 'Garrick Versio', + 'Garven Dreis', + 'Gavyn Sykes', + 'Gideon Hask', + 'Gizor Dellso', + 'Gonk droid', + 'Grand Inquisitor', + 'Greeata Jendowanian', + 'Greedo', + 'Greer Sonnel', + 'Grievous', + 'Grummgar', + 'Gungi', + 'Hammerhead', + 'Han Solo', + 'Harter Kalonia', + 'Has Obbit', + 'Hera Syndulla', + 'Hevy', + 'Hondo Ohnaka', + 'Huyang', + 'Iden Versio', + 'IG-88', + 'Ima-Gun Di', + 'Inquisitors', + 'Inspector Thanoth', + 'Jabba', + 'Jacen Syndulla', + 'Jan Dodonna', + 'Jango Fett', + 'Janus Greejatus', + 'Jar Jar Binks', + 'Jas Emari', + 'Jaxxon', + 'Jek Tono Porkins', + 'Jeremoch Colton', + 'Jira', + 'Jobal Naberrie', + 'Jocasta Nu', + 'Joclad Danva', + 'Joh Yowza', + 'Jom Barell', + 'Joph Seastriker', + 'Jova Tarkin', + 'Jubnuk', + 'Jyn Erso', + 'K-2SO', + 'Kanan Jarrus', + 'Karbin', + 'Karina the Great', + 'Kes Dameron', + 'Ketsu Onyo', + 'Ki-Adi-Mundi', + 'King Katuunko', + 'Kit Fisto', + 'Kitster Banai', + 'Klaatu', + 'Klik-Klak', + 'Korr Sella', + 'Kylo Ren', + 'L3-37', + 'Lama Su', + 'Lando Calrissian', + 'Lanever Villecham', + 'Leia Organa', + 'Letta Turmond', + 'Lieutenant Kaydel Ko Connix', + 'Lieutenant Thire', + 'Lobot', + 'Logray', + 'Lok Durd', + 'Longo Two-Guns', + 'Lor San Tekka', + 'Lorth Needa', + 'Lott Dod', + 'Luke Skywalker', + 'Lumat', + 'Luminara Unduli', + 'Lux Bonteri', + 'Lyn Me', + 'Lyra Erso', + 'Mace Windu', + 'Malakili', + 'Mama the Hutt', + 'Mars Guo', + 'Mas Amedda', + 'Mawhonic', + 'Max Rebo', + 'Maximilian Veers', + 'Maz Kanata', + 'ME-8D9', + 'Meena Tills', + 'Mercurial Swift', + 'Mina Bonteri', + 'Miraj Scintel', + 'Mister Bones', + 'Mod Terrik', + 'Moden Canady', + 'Mon Mothma', + 'Moradmin Bast', + 'Moralo Eval', + 'Morley', + 'Mother Talzin', + 'Nahdar Vebb', + 'Nahdonnis Praji', + 'Nien Nunb', + 'Niima the Hutt', + 'Nines', + 'Norra Wexley', + 'Nute Gunray', + 'Nuvo Vindi', + 'Obi-Wan Kenobi', + 'Odd Ball', + 'Ody Mandrell', + 'Omi', + 'Onaconda Farr', + 'Oola', + 'OOM-9', + 'Oppo Rancisis', + 'Orn Free Taa', + 'Oro Dassyne', + 'Orrimarko', + 'Osi Sobeck', + 'Owen Lars', + 'Pablo-Jill', + 'Padmé Amidala', + 'Pagetti Rook', + 'Paige Tico', + 'Paploo', + 'Petty Officer Thanisson', + 'Pharl McQuarrie', + 'Plo Koon', + 'Po Nudo', + 'Poe Dameron', + 'Poggle the Lesser', + 'Pong Krell', + 'Pooja Naberrie', + 'PZ-4CO', + 'Quarrie', + 'Quay Tolsite', + 'Queen Apailana', + 'Queen Jamillia', + 'Queen Neeyutnee', + 'Qui-Gon Jinn', + 'Quiggold', + 'Quinlan Vos', + 'R2-D2', + 'R2-KT', + 'R3-S6', + 'R4-P17', + 'R5-D4', + 'RA-7', + 'Rabé', + 'Rako Hardeen', + 'Ransolm Casterfo', + 'Rappertunie', + 'Ratts Tyerell', + 'Raymus Antilles', + 'Ree-Yees', + 'Reeve Panzoro', + 'Rey', + 'Ric Olié', + 'Riff Tamson', + 'Riley', + 'Rinnriyin Di', + 'Rio Durant', + 'Rogue Squadron', + 'Romba', + 'Roos Tarpals', + 'Rose Tico', + 'Rotta the Hutt', + 'Rukh', + 'Rune Haako', + 'Rush Clovis', + 'Ruwee Naberrie', + 'Ryoo Naberrie', + 'Sabé', + 'Sabine Wren', + 'Saché', + 'Saelt-Marae', + 'Saesee Tiin', + 'Salacious B. Crumb', + 'San Hill', + 'Sana Starros', + 'Sarco Plank', + 'Sarkli', + 'Satine Kryze', + 'Savage Opress', + 'Sebulba', + 'Senator Organa', + 'Sergeant Kreel', + 'Seventh Sister', + 'Shaak Ti', + 'Shara Bey', + 'Shmi Skywalker', + 'Shu Mai', + 'Sidon Ithano', + 'Sifo-Dyas', + 'Sim Aloo', + 'Siniir Rath Velus', + 'Sio Bibble', + 'Sixth Brother', + 'Slowen Lo', + 'Sly Moore', + 'Snaggletooth', + 'Snap Wexley', + 'Snoke', + 'Sola Naberrie', + 'Sora Bulq', + 'Strono Tuggs', + 'Sy Snootles', + 'Tallissan Lintra', + 'Tarfful', + 'Tasu Leech', + 'Taun We', + 'TC-14', + 'Tee Watt Kaa', + 'Teebo', + 'Teedo', + 'Teemto Pagalies', + 'Temiri Blagg', + 'Tessek', + 'Tey How', + 'Thane Kyrell', + 'The Bendu', + 'The Smuggler', + 'Thrawn', + 'Tiaan Jerjerrod', + 'Tion Medon', + 'Tobias Beckett', + 'Tulon Voidgazer', + 'Tup', + 'U9-C4', + 'Unkar Plutt', + 'Val Beckett', + 'Vanden Willard', + 'Vice Admiral Amilyn Holdo', + 'Vober Dand', + 'WAC-47', + 'Wag Too', + 'Wald', + 'Walrus Man', + 'Warok', + 'Wat Tambor', + 'Watto', + 'Wedge Antilles', + 'Wes Janson', + 'Wicket W. Warrick', + 'Wilhuff Tarkin', + 'Wollivan', + 'Wuher', + 'Wullf Yularen', + 'Xamuel Lennox', + 'Yaddle', + 'Yarael Poof', + 'Yoda', + 'Zam Wesell', + 'Zev Senesca', + 'Ziro the Hutt', + 'Zuckuss', +]; + +const dummyLookupService = { + search(string, callback) { + setTimeout(() => { + const results = dummyMentionsData.filter((mention) => + mention.toLowerCase().includes(string.toLowerCase()), + ); + callback(results); + }, 500); + }, +}; + +function useMentionLookupService(mentionString) { + const [results, setResults] = useState([]); + + useEffect(() => { + const cachedResults = mentionsCache.get(mentionString); + + if (mentionString == null) { + setResults([]); + return; + } + + if (cachedResults === null) { + return; + } else if (cachedResults !== undefined) { + setResults(cachedResults); + return; + } + + mentionsCache.set(mentionString, null); + dummyLookupService.search(mentionString, (newResults) => { + mentionsCache.set(mentionString, newResults); + setResults(newResults); + }); + }, [mentionString]); + + return results; +} + +function checkForAtSignMentions( + text, + minMatchLength, +){ + let match = AtSignMentionsRegex.exec(text); + + if (match === null) { + match = AtSignMentionsRegexAliasRegex.exec(text); + } + if (match !== null) { + // The strategy ignores leading whitespace but we need to know it's + // length to add it to the leadOffset + const maybeLeadingWhitespace = match[1]; + + const matchingString = match[3]; + if (matchingString.length >= minMatchLength) { + return { + leadOffset: match.index + maybeLeadingWhitespace.length, + matchingString, + replaceableString: match[2], + }; + } + } + return null; +} + +function getPossibleQueryMatch(text) { + return checkForAtSignMentions(text, 1); +} + +class MentionTypeaheadOption extends MenuOption { + name; + picture; + + constructor(name, picture) { + super(name); + this.name = name; + this.picture = picture; + } +} + +function MentionsTypeaheadMenuItem({ + index, + isSelected, + onClick, + onMouseEnter, + option, +}) { + let className = 'item'; + if (isSelected) { + className += ' selected'; + } + return ( +
  • + {option.picture} + {option.name} +
  • + ); +} + +// export default function NewMentionsPlugin() { +// const [editor] = useLexicalComposerContext(); +// +// const [queryString, setQueryString] = useState(null); +// +// const results = useMentionLookupService(queryString); +// +// const checkForSlashTriggerMatch = useBasicTypeaheadTriggerMatch('/', { +// minLength: 0, +// }); +// +// const options = useMemo( +// () => +// results +// .map( +// (result) => +// new MentionTypeaheadOption(result, ), +// ) +// .slice(0, SUGGESTION_LIST_LENGTH_LIMIT), +// [results], +// ); +// +// const onSelectOption = useCallback( +// ( +// selectedOption, +// nodeToReplace, +// closeMenu, +// ) => { +// editor.update(() => { +// const mentionNode = $createMentionNode(selectedOption.name); +// if (nodeToReplace) { +// nodeToReplace.replace(mentionNode); +// } +// mentionNode.select(); +// closeMenu(); +// }); +// }, +// [editor], +// ); +// +// const checkForMentionMatch = useCallback( +// (text) => { +// const slashMatch = checkForSlashTriggerMatch(text, editor); +// if (slashMatch !== null) { +// return null; +// } +// return getPossibleQueryMatch(text); +// }, +// [checkForSlashTriggerMatch, editor], +// ); +// +// return ( +// +// onQueryChange={setQueryString} +// onSelectOption={onSelectOption} +// triggerFn={checkForMentionMatch} +// options={options} +// menuRenderFn={( +// anchorElementRef, +// {selectedIndex, selectOptionAndCleanUp, setHighlightedIndex}, +// ) => +// anchorElementRef.current && results.length +// ? ReactDOM.createPortal( +//
    +//
      +// {options.map((option, i) => ( +// { +// setHighlightedIndex(i); +// selectOptionAndCleanUp(option); +// }} +// onMouseEnter={() => { +// setHighlightedIndex(i); +// }} +// key={option.key} +// option={option} +// /> +// ))} +//
    +//
    , +// anchorElementRef.current, +// ) +// : null +// } +// /> +// ); +// } diff --git a/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js b/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js index cd317cb..06209df 100644 --- a/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js +++ b/src/pages/Note/Hlexical/plugins/SaveFilePlugin.js @@ -13,7 +13,7 @@ const {ipcRenderer} = window.require('electron') const SaveFilePlugin=(props)=> { let activeKey = useSelector(state => state.tableBarItem.activeKey); const dispatch = useDispatch(); - const [messageApi,contextHolder] = message.useMessage(); + // const [messageApi,contextHolder] = message.useMessage(); const [editor] = useLexicalComposerContext(); const [editorState,setEditorState]=useState(); @@ -86,7 +86,8 @@ const SaveFilePlugin=(props)=> { if (save) { overWriteFile(filePath, resultSave) console.log("保存成功"+ filePath) - messageApi.open({type:"success",content:"保存成功:" + filePath,duration:1}) + // messageApi.open({type:"success",content:"保存成功:" + filePath,duration:1}) + message.success("保存成功:" + filePath) } }).catch(error => console.error(error) @@ -106,10 +107,10 @@ const SaveFilePlugin=(props)=> { }; },[editor,onChange] ) - return ( - <> - {contextHolder} - - ) + // return ( + //
    + // {contextHolder} + //
    + // ) } export default SaveFilePlugin diff --git a/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js b/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js index deecc04..9404a98 100644 --- a/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js +++ b/src/pages/Note/Hlexical/plugins/ToolbarPlugin.js @@ -728,7 +728,7 @@ export default function ToolbarPlugin() { {/**/} { - showModal('Insert Image', (onClose) => ( + showModal('插入图片', (onClose) => ( + ); +} diff --git a/src/pages/Note/Hlexical/utils/url/index.jsx b/src/pages/Note/Hlexical/utils/url/index.jsx new file mode 100644 index 0000000..cfbb5b5 --- /dev/null +++ b/src/pages/Note/Hlexical/utils/url/index.jsx @@ -0,0 +1,27 @@ +const SUPPORTED_URL_PROTOCOLS = new Set([ + 'http:', + 'https:', + 'mailto:', + 'sms:', + 'tel:', +]); + +export function sanitizeUrl(url) { + try { + const parsedUrl = new URL(url); + // eslint-disable-next-line no-script-url + if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) { + return 'about:blank'; + } + } catch { + return url; + } + return url; +} + +const urlRegExp = new RegExp( + /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/, +); +export function validateUrl(url) { + return url === 'https://' || urlRegExp.test(url); +}