feat:添加图片

This commit is contained in:
shixiaohua 2024-02-27 16:54:04 +08:00
parent 36f2cb43e0
commit 26951e8477
23 changed files with 1813 additions and 53 deletions

View File

@ -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);
};

View File

@ -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,
// },
// );
// }

View File

@ -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);
}

View File

@ -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>);
}

View File

@ -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({

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>
)}

View File

@ -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'} />;
}

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>;
}

View File

@ -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

View File

@ -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} />;
}

View File

@ -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
// }
// />
// );
// }

View File

@ -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

View File

@ -728,7 +728,7 @@ export default function ToolbarPlugin() {
{/*</DropDownItem>*/}
<DropDownItem
onClick={() => {
showModal('Insert Image', (onClose) => (
showModal('插入图片', (onClose) => (
<InsertImageDialog
activeEditor={activeEditor}
onClose={onClose}

View File

@ -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}
/>
);
}

View File

@ -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);
}