feat:添加图片
This commit is contained in:
parent
36f2cb43e0
commit
26951e8477
|
@ -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 <Context.Provider value={historyContext}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
export const useSharedHistoryContext = () => {
|
||||
return useContext(Context);
|
||||
};
|
|
@ -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,
|
||||
// },
|
||||
// );
|
||||
// }
|
|
@ -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);
|
||||
}
|
|
@ -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 (<img
|
||||
className={className || undefined}
|
||||
src={src}
|
||||
alt={altText}
|
||||
ref={imageRef}
|
||||
style={{
|
||||
height, maxWidth, width,
|
||||
}}
|
||||
draggable="false"
|
||||
/>);
|
||||
}
|
||||
|
||||
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 (<Suspense fallback={null}>
|
||||
<>
|
||||
<div draggable={draggable}>
|
||||
<LazyImage
|
||||
className={isFocused ? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}` : null}
|
||||
src={src}
|
||||
altText={altText}
|
||||
imageRef={imageRef}
|
||||
width={width}
|
||||
height={height}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
</div>
|
||||
{showCaption && (<div className="image-caption-container">
|
||||
<LexicalNestedComposer initialEditor={caption}>
|
||||
<AutoFocusPlugin/>
|
||||
{/*<MentionsPlugin/>*/}
|
||||
<LinkPlugin/>
|
||||
<EmojisPlugin/>
|
||||
<HashtagPlugin/>
|
||||
<KeywordsPlugin/>
|
||||
{isCollabActive ? (<CollaborationPlugin
|
||||
id={caption.getKey()}
|
||||
// providerFactory={createWebsocketProvider}
|
||||
shouldBootstrap={true}
|
||||
/>) : (<HistoryPlugin externalHistoryState={historyState}/>)}
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className="ImageNode__contentEditable"/>}
|
||||
placeholder={<Placeholder className="ImageNode__placeholder">
|
||||
Enter a caption...
|
||||
</Placeholder>}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{showNestedEditorTreeView === true ? <TreeViewPlugin/> : null}
|
||||
</LexicalNestedComposer>
|
||||
</div>)}
|
||||
{resizable && $isNodeSelection(selection) && isFocused && (<ImageResizer
|
||||
showCaption={showCaption}
|
||||
setShowCaption={setShowCaption}
|
||||
editor={editor}
|
||||
buttonRef={buttonRef}
|
||||
imageRef={imageRef}
|
||||
maxWidth={maxWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResizeEnd={onResizeEnd}
|
||||
captionsEnabled={captionsEnabled}
|
||||
/>)}
|
||||
</>
|
||||
</Suspense>);
|
||||
}
|
|
@ -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 (
|
||||
// <Suspense fallback={null}>
|
||||
// <ImageComponent
|
||||
// src={this.__src}
|
||||
// altText={this.__altText}
|
||||
// width={this.__width}
|
||||
// height={this.__height}
|
||||
// maxWidth={this.__maxWidth}
|
||||
// nodeKey={this.getKey()}
|
||||
// showCaption={this.__showCaption}
|
||||
// caption={this.__caption}
|
||||
// captionsEnabled={this.__captionsEnabled}
|
||||
// resizable={true}
|
||||
// />
|
||||
// </Suspense>
|
||||
// );
|
||||
// }
|
||||
decorate() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ImageComponent
|
||||
src={this.__src}
|
||||
altText={this.__altText}
|
||||
width={this.__width}
|
||||
height={this.__height}
|
||||
maxWidth={this.__maxWidth}
|
||||
nodeKey={this.getKey()}
|
||||
showCaption={this.__showCaption}
|
||||
caption={this.__caption}
|
||||
captionsEnabled={this.__captionsEnabled}
|
||||
resizable={true}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createImageNode({
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -97,14 +97,14 @@ export function InsertImageUploadedDialogBody({
|
|||
return (
|
||||
<>
|
||||
<FileInput
|
||||
label="Image Upload"
|
||||
label="上传文件"
|
||||
onChange={loadImage}
|
||||
accept="image/*"
|
||||
data-test-id="image-modal-file-upload"
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt Text"
|
||||
placeholder="Descriptive alternative text"
|
||||
label="文件描述"
|
||||
placeholder="文件描述"
|
||||
onChange={setAltText}
|
||||
value={altText}
|
||||
data-test-id="image-modal-alt-text-input"
|
||||
|
@ -148,33 +148,15 @@ export function InsertImageDialog({
|
|||
<>
|
||||
{!mode && (
|
||||
<DialogButtonsList>
|
||||
<Button
|
||||
data-test-id="image-modal-option-sample"
|
||||
onClick={() =>
|
||||
onClick(
|
||||
hasModifier.current
|
||||
? {
|
||||
altText:
|
||||
'Daylight fir trees forest glacier green high ice landscape',
|
||||
src: landscapeImage,
|
||||
}
|
||||
: {
|
||||
altText: 'Yellow flower in tilt shift lens',
|
||||
src: yellowFlowerImage,
|
||||
},
|
||||
)
|
||||
}>
|
||||
Sample
|
||||
</Button>
|
||||
<Button
|
||||
data-test-id="image-modal-option-url"
|
||||
onClick={() => setMode('url')}>
|
||||
URL
|
||||
添加路径
|
||||
</Button>
|
||||
<Button
|
||||
data-test-id="image-modal-option-file"
|
||||
onClick={() => setMode('file')}>
|
||||
File
|
||||
添加文件
|
||||
</Button>
|
||||
</DialogButtonsList>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import './index.less';
|
||||
|
||||
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
||||
import * as React from 'react';
|
||||
|
||||
export default function LexicalContentEditable({
|
||||
className,
|
||||
}) {
|
||||
return <ContentEditable className={className || 'ContentEditable__root'} />;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="Input__wrapper">
|
||||
<label className="Input__label">{label}</label>
|
||||
<input
|
||||
type="file"
|
||||
placeholder="选择文件"
|
||||
accept={accept}
|
||||
className="Input__input"
|
||||
onChange={(e) => onChange(e.target.files)}
|
||||
data-test-id={dataTestId}
|
||||
/>
|
||||
{/*<Upload {...props}>*/}
|
||||
{/* <Button icon={<UploadOutlined />}>选择文件</Button>*/}
|
||||
{/*</Upload>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div ref={controlWrapperRef}>
|
||||
{!showCaption && captionsEnabled && (
|
||||
<button
|
||||
className="image-caption-button"
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
setShowCaption(!showCaption);
|
||||
}}>
|
||||
Add Caption
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className="image-resizer image-resizer-n"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.north);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-ne"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.north | Direction.east);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-e"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.east);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-se"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.south | Direction.east);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-s"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.south);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-sw"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.south | Direction.west);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-w"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.west);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="image-resizer image-resizer-nw"
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, Direction.north | Direction.west);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
import './index.less';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export default function Placeholder({
|
||||
children,
|
||||
className,
|
||||
}) {
|
||||
return <div className={className || 'Placeholder__root'}>{children}</div>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -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 <LexicalLinkPlugin validateUrl={validateUrl} />;
|
||||
}
|
|
@ -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 (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={className}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
{option.picture}
|
||||
<span className="text">{option.name}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// 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, <i className="icon user" />),
|
||||
// )
|
||||
// .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 (
|
||||
// <LexicalTypeaheadMenuPlugin<MentionTypeaheadOption>
|
||||
// onQueryChange={setQueryString}
|
||||
// onSelectOption={onSelectOption}
|
||||
// triggerFn={checkForMentionMatch}
|
||||
// options={options}
|
||||
// menuRenderFn={(
|
||||
// anchorElementRef,
|
||||
// {selectedIndex, selectOptionAndCleanUp, setHighlightedIndex},
|
||||
// ) =>
|
||||
// anchorElementRef.current && results.length
|
||||
// ? ReactDOM.createPortal(
|
||||
// <div className="typeahead-popover mentions-menu">
|
||||
// <ul>
|
||||
// {options.map((option, i) => (
|
||||
// <MentionsTypeaheadMenuItem
|
||||
// index={i}
|
||||
// isSelected={selectedIndex === i}
|
||||
// onClick={() => {
|
||||
// setHighlightedIndex(i);
|
||||
// selectOptionAndCleanUp(option);
|
||||
// }}
|
||||
// onMouseEnter={() => {
|
||||
// setHighlightedIndex(i);
|
||||
// }}
|
||||
// key={option.key}
|
||||
// option={option}
|
||||
// />
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>,
|
||||
// anchorElementRef.current,
|
||||
// )
|
||||
// : null
|
||||
// }
|
||||
// />
|
||||
// );
|
||||
// }
|
|
@ -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 (
|
||||
// <div>
|
||||
// {contextHolder}
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
export default SaveFilePlugin
|
||||
|
|
|
@ -728,7 +728,7 @@ export default function ToolbarPlugin() {
|
|||
{/*</DropDownItem>*/}
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
showModal('Insert Image', (onClose) => (
|
||||
showModal('插入图片', (onClose) => (
|
||||
<InsertImageDialog
|
||||
activeEditor={activeEditor}
|
||||
onClose={onClose}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {TreeView} from '@lexical/react/LexicalTreeView';
|
||||
import * as React from 'react';
|
||||
|
||||
export default function TreeViewPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return (
|
||||
<TreeView
|
||||
viewClassName="tree-view-output"
|
||||
treeTypeButtonClassName="debug-treetype-button"
|
||||
timeTravelPanelClassName="debug-timetravel-panel"
|
||||
timeTravelButtonClassName="debug-timetravel-button"
|
||||
timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
|
||||
timeTravelPanelButtonClassName="debug-timetravel-panel-button"
|
||||
editor={editor}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue