diff --git a/libs/components/affine-board/src/Board.tsx b/libs/components/affine-board/src/Board.tsx index db969d29da..497720d2bd 100644 --- a/libs/components/affine-board/src/Board.tsx +++ b/libs/components/affine-board/src/Board.tsx @@ -5,7 +5,10 @@ import { getSession } from '@toeverything/components/board-sessions'; import { deepCopy, TldrawApp } from '@toeverything/components/board-state'; import { tools } from '@toeverything/components/board-tools'; import { TDShapeType } from '@toeverything/components/board-types'; -import { RecastBlockProvider } from '@toeverything/components/editor-core'; +import { + getClipDataOfBlocksById, + RecastBlockProvider, +} from '@toeverything/components/editor-core'; import { services } from '@toeverything/datasource/db-service'; import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo'; import { useEffect, useState } from 'react'; @@ -16,7 +19,11 @@ interface AffineBoardProps { rootBlockId: string; } -const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => { +const AffineBoard = ({ + workspace, + rootBlockId, + editor, +}: AffineBoardProps & { editor: BlockEditor }) => { const [app, set_app] = useState(); const [document] = useState(() => { return { @@ -61,8 +68,20 @@ const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => { onMount(app) { set_app(app); }, + + async onCopy(e, groupIds) { + const clip = await getClipDataOfBlocksById( + editor, + groupIds + ); + + e.clipboardData?.setData( + clip.getMimeType(), + clip.getData() + ); + }, async onChangePage(app, shapes, bindings, assets) { - await Promise.all( + Promise.all( Object.entries(shapes).map(async ([id, shape]) => { if (shape === undefined) { return services.api.editorBlock.delete({ @@ -167,7 +186,11 @@ export const AffineBoardWitchContext = ({ }, [editor, rootBlockId]); return page ? ( - + ) : null; }; diff --git a/libs/components/board-shapes/src/editor-util/EditorUtil.tsx b/libs/components/board-shapes/src/editor-util/EditorUtil.tsx index 6e6f123a72..805ada0623 100644 --- a/libs/components/board-shapes/src/editor-util/EditorUtil.tsx +++ b/libs/components/board-shapes/src/editor-util/EditorUtil.tsx @@ -8,9 +8,10 @@ import { TDShapeType, TransformInfo, } from '@toeverything/components/board-types'; +import type { BlockEditor } from '@toeverything/components/editor-core'; import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; -import type { SyntheticEvent } from 'react'; +import type { MouseEvent, SyntheticEvent } from 'react'; import { memo, useCallback, useEffect, useRef } from 'react'; import { defaultTextStyle, @@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil { Component = TDShapeUtil.Component( ({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => { const containerRef = useRef(); + const editorRef = useRef(); const { workspace, rootBlockId, @@ -135,6 +137,27 @@ export class EditorUtil extends TDShapeUtil { } }, [app, state, shape.id, editingText, editingId]); + useEffect(() => { + (async () => { + if (isEditing) { + const lastBlock = + await editorRef.current.getLastBlock(); + editorRef.current.selectionManager.activeNodeByNodeId( + lastBlock.id + ); + } + })(); + }, [isEditing]); + + const onMouseDown = useCallback( + (e: MouseEvent) => { + if (e.detail === 2) { + app.setEditingText(shape.id); + } + }, + [app, shape.id] + ); + return ( { onPointerDown={stopPropagation} onMouseEnter={activateIfEditing} onDragEnter={activateIfEditing} + onMouseDown={onMouseDown} > {editingText ? null : } diff --git a/libs/components/board-state/src/tldraw-app.ts b/libs/components/board-state/src/tldraw-app.ts index 79d7215e99..49c352c2e9 100644 --- a/libs/components/board-state/src/tldraw-app.ts +++ b/libs/components/board-state/src/tldraw-app.ts @@ -171,6 +171,10 @@ interface TDCallbacks { * (optional) A callback to run when the user exports their page or selection. */ onExport?: (app: TldrawApp, info: TDExport) => Promise; + /** + * (optional) A callback to run when the shape is copied. + */ + onCopy?: (e: ClipboardEvent, ids: string[]) => void; } export interface TldrawAppCtorProps { @@ -1898,12 +1902,14 @@ export class TldrawApp extends StateManager { /** * Copy one or more shapes to the clipboard. * @param ids The ids of the shapes to copy. + * @param pageId + * @param e */ - copy = ( + copy = async ( ids = this.selectedIds, pageId = this.currentPageId, e?: ClipboardEvent - ): this => { + ) => { e?.preventDefault(); this.clipboard = this.get_clipboard(ids, pageId); @@ -1919,17 +1925,24 @@ export class TldrawApp extends StateManager { if (e) { e.clipboardData?.setData('text/html', tldrawString); + await this.callbacks.onCopy?.(e, this.selectedIds); } - if (navigator.clipboard && window.ClipboardItem) { - navigator.clipboard.write([ - new ClipboardItem({ - 'text/html': new Blob([tldrawString], { - type: 'text/html', - }), - }), - ]); - } + /** + * Reasons for not using Clipboard API for now: + * 1. The `clipboardData.setData` method temporarily satisfies the need for replication functionality + * 2. Clipboard API requires the user to agree to access(https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + * + * **/ + // if (navigator.clipboard && window.ClipboardItem) { + // navigator.clipboard.write([ + // new ClipboardItem({ + // 'text/html': new Blob([tldrawString], { + // type: 'text/html', + // }), + // }), + // ]); + // } this.pasteInfo.offset = [0, 0]; this.pasteInfo.center = [0, 0]; @@ -3841,7 +3854,7 @@ export class TldrawApp extends StateManager { private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => { if (typeof svgStr === 'string') { - let viewBox = new DOMParser().parseFromString(svgStr, 'text/xml'); + const viewBox = new DOMParser().parseFromString(svgStr, 'text/xml'); return viewBox.children[0].getAttribute('viewBox'); } @@ -4125,7 +4138,7 @@ export class TldrawApp extends StateManager { }; onPointerDown: TLPointerEventHandler = (info, e) => { - if (e.buttons === 4) { + if (e.button === 1) { this.patchState({ settings: { forcePanning: true, @@ -4142,6 +4155,13 @@ export class TldrawApp extends StateManager { }; onPointerUp: TLPointerEventHandler = (info, e) => { + if (e.button === 1) { + this.patchState({ + settings: { + forcePanning: false, + }, + }); + } this.isPointing = false; this.updateInputs(info, e); this.currentTool.onPointerUp?.(info, e); diff --git a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx index 86881478ff..86b43908d1 100644 --- a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx +++ b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx @@ -1,12 +1,12 @@ -import { useState } from 'react'; -import { CreateView } from '@toeverything/framework/virgo'; import { BlockPendantProvider, useOnSelect, } from '@toeverything/components/editor-core'; -import { Upload } from '../../components/upload/upload'; +import { CreateView } from '@toeverything/framework/virgo'; +import { useState } from 'react'; import { SourceView } from '../../components/source-view'; import { LinkContainer } from '../../components/style-container'; +import { Upload } from '../../components/upload/upload'; const MESSAGES = { ADD_EMBED_LINK: 'Add embed link', @@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => { {embedLinkUrl ? ( *': { + visibility: 'hidden', + position: 'absolute', + right: '24px', + top: '16px', + }, + '&:hover > *': { + visibility: 'visible', + }, +}); + export const CardItem = ({ id, block, @@ -64,24 +91,43 @@ export const CardItem = ({ }) => { const { addSubItem } = useKanban(); const { openSubPage } = useRefPage(); + const [editable, setEditable] = useState(false); const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false); + const { editor } = useEditor(); + const onAddItem = async () => { + setEditable(true); await addSubItem(block); }; const onClickCard = async () => { - showKanbanRefPageFlag && openSubPage(id); + openSubPage(id); + }; + + const onClickPen = (e: MouseEvent) => { + e.stopPropagation(); + setEditable(true); + editor.selectionManager.activeNodeByNodeId(block.id); }; return ( - - - - - - - Add a sub-block - - + setEditable(false)}> + + + + + {showKanbanRefPageFlag && !editable && ( + + + + + + )} + + + Add a sub-block + + + ); }; diff --git a/libs/components/editor-core/src/ref-page/ModalPage.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx similarity index 80% rename from libs/components/editor-core/src/ref-page/ModalPage.tsx rename to libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx index 52e1d6554f..3d223d121d 100644 --- a/libs/components/editor-core/src/ref-page/ModalPage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx @@ -1,7 +1,7 @@ +import { useEditor } from '@toeverything/components/editor-core'; import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui'; import { createContext, ReactNode, useContext, useState } from 'react'; import { createPortal } from 'react-dom'; -import { RenderBlock } from '../render-block'; const Dialog = styled('div')({ flex: 1, @@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { onClick={closeSubPage} > { + onClick={(e: { stopPropagation: () => void }) => { e.stopPropagation(); }} > @@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { }; const ModalPage = ({ blockId }: { blockId: string | null }) => { + const { editor, editorElement } = useEditor(); + + const AffineEditor = editorElement as any; + return ( - {blockId && } + {blockId && ( + + )} ); }; diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx index 27d5543177..73424708a7 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx @@ -4,30 +4,33 @@ import { SceneKanbanContext } from './context'; import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper'; import type { ComponentType } from 'react'; import type { CreateView } from '@toeverything/framework/virgo'; +import { RefPageProvider } from './RefPage'; export const SceneKanban: ComponentType = withKanban( ({ editor, block }) => { const { kanban } = useKanban(); return ( - - ( - - )} - /> - + + + ( + + )} + /> + + ); } ); diff --git a/libs/components/editor-blocks/src/components/source-view/BlockView.tsx b/libs/components/editor-blocks/src/components/source-view/BlockView.tsx index 33c0f5d66c..0b2c953312 100644 --- a/libs/components/editor-blocks/src/components/source-view/BlockView.tsx +++ b/libs/components/editor-blocks/src/components/source-view/BlockView.tsx @@ -1,9 +1,9 @@ -import { memo, useEffect, useRef, useState } from 'react'; import { nanoid } from 'nanoid'; +import { memo, useEffect, useRef, useState } from 'react'; import { StyledBlockPreview } from '@toeverything/components/common'; +import { AsyncBlock, useEditor } from '@toeverything/components/editor-core'; import { services } from '@toeverything/datasource/db-service'; -import { AsyncBlock } from '@toeverything/framework/virgo'; import { debounce, sleep } from '@toeverything/utils'; const updateTitle = async ( @@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => { type BlockPreviewProps = { block: AsyncBlock; blockId: string; - editorElement?: () => JSX.Element; }; const InternalBlockPreview = (props: BlockPreviewProps) => { const container = useRef(); const [preview, setPreview] = useState(true); const title = useBlockTitle(props.block, props.blockId); + const { editorElement } = useEditor(); - const AffineEditor = props.editorElement as any; + const AffineEditor = editorElement as any; useEffect(() => { if (container?.current) { diff --git a/libs/components/editor-blocks/src/components/source-view/SourceView.tsx b/libs/components/editor-blocks/src/components/source-view/SourceView.tsx index b03189d7d4..27d19433bf 100644 --- a/libs/components/editor-blocks/src/components/source-view/SourceView.tsx +++ b/libs/components/editor-blocks/src/components/source-view/SourceView.tsx @@ -1,17 +1,15 @@ import { AsyncBlock, useCurrentView, - useLazyIframe, } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; -import { ReactElement, ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; import { SCENE_CONFIG } from '../../blocks/group/config'; import { BlockPreview } from './BlockView'; import { formatUrl } from './format-url'; export interface Props { block: AsyncBlock; - editorElement?: () => JSX.Element; viewType?: string; link: string; // onResizeEnd: (data: any) => void; @@ -150,7 +148,7 @@ const LoadingContiner = () => { }; export const SourceView = (props: Props) => { - const { link, isSelected, block, editorElement } = props; + const { link, isSelected, block } = props; const src = formatUrl(link); // let iframeShow = useLazyIframe(src, 3000, iframeContainer); const [currentView] = useCurrentView(); @@ -161,10 +159,7 @@ export const SourceView = (props: Props) => { - + ); @@ -175,11 +170,7 @@ export const SourceView = (props: Props) => { style={{ padding: '0' }} scene={type} > - + ); } diff --git a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts index 64eecbc5ca..687c5280ee 100644 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts +++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts @@ -1,18 +1,6 @@ import { HooksRunner } from '../types'; -import { - OFFICE_CLIPBOARD_MIMETYPE, - InnerClipInfo, - ClipBlockInfo, -} from './types'; import { Editor } from '../editor'; -import { AsyncBlock } from '../block'; import ClipboardParse from './clipboard-parse'; -import { SelectInfo } from '../selection'; -import { - Protocol, - BlockFlavorKeys, - services, -} from '@toeverything/datasource/db-service'; import { MarkdownParser } from './markdown-parse'; import { shouldHandlerContinue } from './utils'; import { Paste } from './paste'; diff --git a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts b/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts index 2f05866ab9..d7a8172566 100644 --- a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts +++ b/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts @@ -1,23 +1,16 @@ import { Editor } from '../editor'; -import { SelectionManager, SelectInfo, SelectBlock } from '../selection'; +import { SelectionManager } from '../selection'; import { HookType, PluginHooks } from '../types'; -import { - ClipBlockInfo, - OFFICE_CLIPBOARD_MIMETYPE, - InnerClipInfo, -} from './types'; -import { Clip } from './clip'; -import assert from 'assert'; import ClipboardParse from './clipboard-parse'; import { Subscription } from 'rxjs'; - +import { Copy } from './copy'; class ClipboardPopulator { private _editor: Editor; private _hooks: PluginHooks; private _selectionManager: SelectionManager; private _clipboardParse: ClipboardParse; private _sub = new Subscription(); - + private _copy: Copy; constructor( editor: Editor, hooks: PluginHooks, @@ -27,128 +20,15 @@ class ClipboardPopulator { this._hooks = hooks; this._selectionManager = selectionManager; this._clipboardParse = new ClipboardParse(editor); + this._copy = new Copy(editor); this._sub.add( - hooks - .get(HookType.BEFORE_COPY) - .subscribe(this._populateAppClipboard) + hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy) ); this._sub.add( - hooks.get(HookType.BEFORE_CUT).subscribe(this._populateAppClipboard) + hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy) ); } - private _populateAppClipboard = async (e: ClipboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - const clips = await this.getClips(); - if (!clips.length) { - return; - } - - // TODO: is not compatible with safari - const success = this._copyToClipboardFromPc(clips); - if (!success) { - // This way, not compatible with firefox - const clipboardData = e.clipboardData; - if (clipboardData) { - try { - clips.forEach(clip => { - clipboardData.setData( - clip.getMimeType(), - clip.getData() - ); - }); - } catch (e) { - // TODO handle exception - } - } - } - }; - - private _copyToClipboardFromPc(clips: any[]) { - let success = false; - const tempElem = document.createElement('textarea'); - tempElem.value = 'temp'; - document.body.appendChild(tempElem); - tempElem.select(); - tempElem.setSelectionRange(0, tempElem.value.length); - - const listener = function (e: any) { - const clipboardData = e.clipboardData; - if (clipboardData) { - clips.forEach(clip => { - clipboardData.setData(clip.getMimeType(), clip.getData()); - }); - } - - e.preventDefault(); - e.stopPropagation(); - tempElem.removeEventListener('copy', listener); - } as any; - - tempElem.addEventListener('copy', listener); - try { - success = document.execCommand('copy'); - } finally { - tempElem.removeEventListener('copy', listener); - document.body.removeChild(tempElem); - } - return success; - } - - private async _getClipBlockInfo(selBlock: SelectBlock) { - const block = await this._editor.getBlockById(selBlock.blockId); - const blockView = this._editor.getView(block.type); - assert(blockView); - const blockInfo: ClipBlockInfo = { - type: block.type, - properties: blockView.getSelProperties(block, selBlock), - children: [] as any[], - }; - - for (let i = 0; i < selBlock.children.length; i++) { - const childInfo = await this._getClipBlockInfo( - selBlock.children[i] - ); - blockInfo.children.push(childInfo); - } - - return blockInfo; - } - - private async _getInnerClip(): Promise { - const clips: ClipBlockInfo[] = []; - const selectInfo: SelectInfo = - await this._selectionManager.getSelectInfo(); - for (let i = 0; i < selectInfo.blocks.length; i++) { - const selBlock = selectInfo.blocks[i]; - const clipBlockInfo = await this._getClipBlockInfo(selBlock); - clips.push(clipBlockInfo); - } - return { - select: selectInfo, - data: clips, - }; - } - - async getClips() { - const clips: any[] = []; - - const innerClip = await this._getInnerClip(); - clips.push( - new Clip( - OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, - JSON.stringify(innerClip) - ) - ); - - const htmlClip = await this._clipboardParse.generateHtml(); - htmlClip && - clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip)); - - return clips; - } - disposeInternal() { this._sub.unsubscribe(); this._hooks = null; diff --git a/libs/components/editor-core/src/editor/clipboard/copy.ts b/libs/components/editor-core/src/editor/clipboard/copy.ts new file mode 100644 index 0000000000..94b3117264 --- /dev/null +++ b/libs/components/editor-core/src/editor/clipboard/copy.ts @@ -0,0 +1,102 @@ +import { Editor } from '../editor'; +import { SelectInfo } from '../selection'; +import { OFFICE_CLIPBOARD_MIMETYPE } from './types'; +import { Clip } from './clip'; +import ClipboardParse from './clipboard-parse'; +import { getClipDataOfBlocksById } from './utils'; +import { copyToClipboard } from '@toeverything/utils'; +class Copy { + private _editor: Editor; + private _clipboardParse: ClipboardParse; + + constructor(editor: Editor) { + this._editor = editor; + this._clipboardParse = new ClipboardParse(editor); + + this.handleCopy = this.handleCopy.bind(this); + } + public async handleCopy(e: ClipboardEvent) { + e.preventDefault(); + e.stopPropagation(); + const clips = await this.getClips(); + if (!clips.length) { + return; + } + // TODO: is not compatible with safari + const success = this._copyToClipboardFromPc(clips); + if (!success) { + // This way, not compatible with firefox + const clipboardData = e.clipboardData; + if (clipboardData) { + try { + clips.forEach(clip => { + clipboardData.setData( + clip.getMimeType(), + clip.getData() + ); + }); + } catch (e) { + // TODO handle exception + } + } + } + } + + async getClips() { + const clips: Clip[] = []; + + // get custom clip + const affineClip = await this._getAffineClip(); + clips.push(affineClip); + + // get common html clip + const htmlClip = await this._clipboardParse.generateHtml(); + htmlClip && + clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip)); + + return clips; + } + + private async _getAffineClip(): Promise { + const selectInfo: SelectInfo = + await this._editor.selectionManager.getSelectInfo(); + + return getClipDataOfBlocksById( + this._editor, + selectInfo.blocks.map(block => block.blockId) + ); + } + + private _copyToClipboardFromPc(clips: any[]) { + let success = false; + const tempElem = document.createElement('textarea'); + tempElem.value = 'temp'; + document.body.appendChild(tempElem); + tempElem.select(); + tempElem.setSelectionRange(0, tempElem.value.length); + + const listener = function (e: any) { + const clipboardData = e.clipboardData; + if (clipboardData) { + clips.forEach(clip => { + clipboardData.setData(clip.getMimeType(), clip.getData()); + }); + } + + e.preventDefault(); + e.stopPropagation(); + tempElem.removeEventListener('copy', listener); + } as any; + + tempElem.addEventListener('copy', listener); + try { + success = document.execCommand('copy'); + } finally { + tempElem.removeEventListener('copy', listener); + document.body.removeChild(tempElem); + } + return success; + } +} + +export { Copy }; diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts index b24d06b23a..37168c2614 100644 --- a/libs/components/editor-core/src/editor/clipboard/paste.ts +++ b/libs/components/editor-core/src/editor/clipboard/paste.ts @@ -241,15 +241,12 @@ export class Paste { [] ); - selectedBlock.setProperties({ + await selectedBlock.setProperties({ text: { value: newTextValue, }, }); - const pasteBlocks = await this._createBlocks(blocks); - await Promise.all( - pasteBlocks.map(block => selectedBlock.after(block)) - ); + const pastedBlocks = await this._createBlocks(blocks); const nextBlock = await this._editor.createBlock( selectedBlock?.type @@ -259,11 +256,12 @@ export class Paste { value: nextTextValue, }, }); - pasteBlocks[pasteBlocks.length - 1].after(nextBlock); - this._setEndSelectToBlock( - pasteBlocks[pasteBlocks.length - 1].id - ); + await this._insertBlocksAfterBlock(selectedBlock, [ + ...pastedBlocks, + nextBlock, + ]); + await this._setEndSelectToBlock(nextBlock.id); } else { this._editor.blockHelper.insertNodes( selectedBlock.id, @@ -327,10 +325,7 @@ export class Paste { value: newTextValue, }, }); - const pasteBlocks = await this._createBlocks(blocks); - pasteBlocks.forEach((block: AsyncBlock) => { - selectedBlock.after(block); - }); + const pastedBlocks = await this._createBlocks(blocks); const nextBlock = await this._editor.createBlock( selectedBlock?.type ); @@ -339,11 +334,12 @@ export class Paste { value: nextTextValue, }, }); - pasteBlocks[pasteBlocks.length - 1].after(nextBlock); + await this._insertBlocksAfterBlock(selectedBlock, [ + ...pastedBlocks, + nextBlock, + ]); - this._setEndSelectToBlock( - pasteBlocks[pasteBlocks.length - 1].id - ); + await this._setEndSelectToBlock(nextBlock.id); } else { this._editor.blockHelper.insertNodes( selectedBlock.id, @@ -353,10 +349,10 @@ export class Paste { } } } else { - const pasteBlocks = await this._createBlocks(blocks); + const pastedBlocks = await this._createBlocks(blocks); await Promise.all( - pasteBlocks.map(block => selectedBlock.after(block)) + pastedBlocks.map(block => selectedBlock.after(block)) ); if (isSelectedBlockEmpty) { @@ -364,7 +360,7 @@ export class Paste { } this._setEndSelectToBlock( - pasteBlocks[pasteBlocks.length - 1].id + pastedBlocks[pastedBlocks.length - 1].id ); } } @@ -374,27 +370,39 @@ export class Paste { currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1] .blockId ); - const pasteBlocks = await this._createBlocks(blocks); + const pastedBlocks = await this._createBlocks(blocks); let groupBlock: AsyncBlock; - if ( - selectedBlock?.type === 'group' || - selectedBlock?.type === 'page' - ) { + if (selectedBlock?.type === 'page') { groupBlock = await this._editor.createBlock('group'); await Promise.all( - pasteBlocks.map(block => groupBlock.append(block)) + pastedBlocks.map(block => groupBlock.append(block)) ); await selectedBlock.after(groupBlock); + } else if (selectedBlock?.type === 'group') { + await Promise.all( + pastedBlocks.map(block => selectedBlock.append(block)) + ); } else { await Promise.all( - pasteBlocks.map(block => selectedBlock.after(block)) + pastedBlocks.map(block => selectedBlock.after(block)) ); } - this._setEndSelectToBlock(pasteBlocks[pasteBlocks.length - 1].id); + this._setEndSelectToBlock(pastedBlocks[pastedBlocks.length - 1].id); } } + private async _insertBlocksAfterBlock( + targetBlock: AsyncBlock, + blocks: AsyncBlock[] + ) { + if (blocks.length === 0) { + return; + } + const [firstBlock, ...otherBlock] = blocks; + await targetBlock.after(firstBlock); + await this._insertBlocksAfterBlock(blocks[0], otherBlock); + } private async _setEndSelectToBlock(blockId: string) { const block = await this._editor.getBlockById(blockId); const isBlockCanEdit = Paste._isTextEditBlock(block.type); @@ -406,14 +414,37 @@ export class Paste { }, 100); } - private async _createBlocks(blocks: ClipBlockInfo[], parentId?: string) { + private _flatGroupBlocks(blocks: ClipBlockInfo[]) { + return blocks.reduce( + (blockList: ClipBlockInfo[], block: ClipBlockInfo) => { + if (block.type === 'group') { + block?.children?.forEach(childBlock => { + childBlock.children = this._flatGroupBlocks( + childBlock.children + ); + }); + block?.children?.length && + blockList.push(...block.children); + } else { + blockList.push(block); + block.children = this._flatGroupBlocks(block.children); + } + return blockList; + }, + [] + ); + } + private async _createBlocks(blocks: ClipBlockInfo[]) { return Promise.all( - blocks.map(async clipBlockInfo => { + this._flatGroupBlocks(blocks).map(async clipBlockInfo => { const block = await this._editor.createBlock( clipBlockInfo.type ); block?.setProperties(clipBlockInfo.properties); - await this._createBlocks(clipBlockInfo.children, block?.id); + const children = await this._createBlocks( + clipBlockInfo.children + ); + await Promise.all(children.map(child => block?.append(child))); return block; }) ); diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts index cb5d10241e..c807a869a0 100644 --- a/libs/components/editor-core/src/editor/clipboard/utils.ts +++ b/libs/components/editor-core/src/editor/clipboard/utils.ts @@ -1,4 +1,6 @@ import { Editor } from '../editor'; +import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types'; +import { Clip } from './clip'; export const shouldHandlerContinue = (event: Event, editor: Editor) => { const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; @@ -12,3 +14,39 @@ export const shouldHandlerContinue = (event: Event, editor: Editor) => { return editor.selectionManager.currentSelectInfo.type !== 'None'; }; + +export const getClipInfoOfBlockById = async ( + editor: Editor, + blockId: string +) => { + const block = await editor.getBlockById(blockId); + const blockView = editor.getView(block.type); + const blockInfo: ClipBlockInfo = { + type: block.type, + properties: blockView.getSelProperties(block, {}), + children: [] as ClipBlockInfo[], + }; + const children = (await block?.children()) ?? []; + + for (let i = 0; i < children.length; i++) { + const childInfo = await getClipInfoOfBlockById(editor, children[i].id); + blockInfo.children.push(childInfo); + } + return blockInfo; +}; + +export const getClipDataOfBlocksById = async ( + editor: Editor, + blockIds: string[] +) => { + const clipInfos = await Promise.all( + blockIds.map(blockId => getClipInfoOfBlockById(editor, blockId)) + ); + + return new Clip( + OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, + JSON.stringify({ + data: clipInfos, + }) + ); +}; diff --git a/libs/components/editor-core/src/editor/editor.ts b/libs/components/editor-core/src/editor/editor.ts index 1d1cc95684..e85db2ba65 100644 --- a/libs/components/editor-core/src/editor/editor.ts +++ b/libs/components/editor-core/src/editor/editor.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import HotKeys from 'hotkeys-js'; -import LRUCache from 'lru-cache'; import type { PatchNode } from '@toeverything/components/ui'; import type { @@ -46,10 +45,7 @@ export interface EditorCtorProps { } export class Editor implements Virgo { - private _cacheManager = new LRUCache>({ - max: 8192, - ttl: 1000 * 60 * 30, - }); + private _cacheManager = new Map>(); public mouseManager = new MouseManager(this); public selectionManager = new SelectionManager(this); public keyboardManager = new KeyboardManager(this); @@ -234,7 +230,12 @@ export class Editor implements Virgo { return await services.api.editorBlock.update(patches); }, remove: async ({ workspace, id }: WorkspaceAndBlockId) => { - return await services.api.editorBlock.delete({ workspace, id }); + const ret = await services.api.editorBlock.delete({ + workspace, + id, + }); + this._cacheManager.delete(id); + return ret; }, observe: async ( { workspace, id }: WorkspaceAndBlockId, diff --git a/libs/components/editor-core/src/editor/index.ts b/libs/components/editor-core/src/editor/index.ts index 03903d31bf..3131e6f0a1 100644 --- a/libs/components/editor-core/src/editor/index.ts +++ b/libs/components/editor-core/src/editor/index.ts @@ -10,3 +10,4 @@ export { BlockDropPlacement, HookType, GroupDirection } from './types'; export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types'; export { BaseView, getTextHtml, getTextProperties } from './views/base-view'; export type { ChildrenView, CreateView } from './views/base-view'; +export { getClipDataOfBlocksById } from './clipboard/utils'; diff --git a/libs/components/editor-core/src/editor/views/base-view.ts b/libs/components/editor-core/src/editor/views/base-view.ts index b69cdd848a..eb5c293243 100644 --- a/libs/components/editor-core/src/editor/views/base-view.ts +++ b/libs/components/editor-core/src/editor/views/base-view.ts @@ -18,7 +18,6 @@ import { SelectBlock } from '../selection'; export interface CreateView { block: AsyncBlock; editor: Editor; - editorElement: () => JSX.Element; /** * @deprecated Use recast table instead */ diff --git a/libs/components/editor-core/src/index.ts b/libs/components/editor-core/src/index.ts index bea2fed3a0..d28d8e96b3 100644 --- a/libs/components/editor-core/src/index.ts +++ b/libs/components/editor-core/src/index.ts @@ -16,4 +16,4 @@ export * from './utils'; export * from './editor'; -export { RefPageProvider, useRefPage } from './ref-page'; +export { useEditor } from './Contexts'; diff --git a/libs/components/editor-core/src/recast-block/Context.tsx b/libs/components/editor-core/src/recast-block/Context.tsx index 47ec6cfcdb..4236ede55a 100644 --- a/libs/components/editor-core/src/recast-block/Context.tsx +++ b/libs/components/editor-core/src/recast-block/Context.tsx @@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service'; import { AsyncBlock } from '../editor'; import { ComponentType, createContext, ReactNode, useContext } from 'react'; import { RecastBlock } from './types'; -import { RefPageProvider } from '../ref-page'; /** * Determine whether the block supports RecastBlock @@ -48,7 +47,7 @@ export const RecastBlockProvider = ({ return ( - {children} + {children} ); }; diff --git a/libs/components/editor-core/src/ref-page/index.ts b/libs/components/editor-core/src/ref-page/index.ts deleted file mode 100644 index 4b06310a5c..0000000000 --- a/libs/components/editor-core/src/ref-page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useRefPage, RefPageProvider } from './ModalPage'; diff --git a/libs/components/editor-core/src/render-block/RenderBlock.tsx b/libs/components/editor-core/src/render-block/RenderBlock.tsx index 0627aba95b..0a1531a709 100644 --- a/libs/components/editor-core/src/render-block/RenderBlock.tsx +++ b/libs/components/editor-core/src/render-block/RenderBlock.tsx @@ -13,7 +13,7 @@ export function RenderBlock({ blockId, hasContainer = true, }: RenderBlockProps) { - const { editor, editorElement } = useEditor(); + const { editor } = useEditor(); const { block } = useBlock(blockId); const setRef = useCallback( @@ -50,7 +50,6 @@ export function RenderBlock({ block={block} columns={columns.columns} columnsFromId={columns.fromId} - editorElement={editorElement} /> ); diff --git a/libs/components/editor-plugins/src/index.ts b/libs/components/editor-plugins/src/index.ts index be7bb9bd97..ab657a9904 100644 --- a/libs/components/editor-plugins/src/index.ts +++ b/libs/components/editor-plugins/src/index.ts @@ -21,7 +21,7 @@ export const plugins: PluginCreator[] = [ CommandMenuPlugin, ReferenceMenuPlugin, TemplatePlugin, - SelectionGroupPlugin, + // SelectionGroupPlugin, AddCommentPlugin, GroupMenuPlugin, ]; diff --git a/tools/executors/svgOptimize/executor.json b/tools/executors/svgOptimize/executor.json deleted file mode 100644 index 1c19a2f9c5..0000000000 --- a/tools/executors/svgOptimize/executor.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "executors": { - "svgOptimize": { - "implementation": "./svgo.js", - "schema": "./schema.json", - "description": "Run `svgo` (to optimize svg)." - } - } -} diff --git a/tools/executors/svgOptimize/package.json b/tools/executors/svgOptimize/package.json deleted file mode 100644 index 721ff1df5d..0000000000 --- a/tools/executors/svgOptimize/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "executors": "./executor.json" -} diff --git a/tools/executors/svgOptimize/schema.json b/tools/executors/svgOptimize/schema.json deleted file mode 100644 index dc2fdc029b..0000000000 --- a/tools/executors/svgOptimize/schema.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "type": "object", - "cli": "nx", - "properties": { - "svgOptimize": { - "type": "string", - "description": "optimize svg" - } - } -} diff --git a/tools/executors/svgOptimize/svgo.js b/tools/executors/svgOptimize/svgo.js deleted file mode 100644 index a0f6c21539..0000000000 --- a/tools/executors/svgOptimize/svgo.js +++ /dev/null @@ -1,133 +0,0 @@ -const path = require('path'); -const svgo = require('svgo'); -const { readdir, readFile, writeFile, exists } = require('fs/promises'); -const { pascalCase, paramCase } = require('change-case'); -const svgr = require('@svgr/core'); - -async function optimizeSvg(folder) { - try { - const icons = await readdir(folder); - const generateIcons = icons - .filter(n => n.endsWith('.svg')) - .map(async icon => { - let originSvg; - try { - originSvg = await readFile(path.resolve(folder, icon)); - } catch (err) { - console.error(err); - } - let optimizedSvg; - try { - const data = optimize(originSvg); - optimizedSvg = data.data; - } catch (err) { - console.error(err); - } - - const JSXContent = await getJSXContent( - pascalCase(icon), - optimizedSvg - ); - - const iconName = path.basename(icon, '.svg'); - await writeFile( - path.resolve(folder, `${iconName}.tsx`), - JSXContent, - { encoding: 'utf8', flag: '' } - ); - - console.log('Generated:', iconName); - }); - - await Promise.allSettled([ - ...generateIcons, - generateImportEntry(icons, folder), - ]); - } catch (err) { - console.error(err); - } -} - -function optimize(input) { - return svgo.optimize(input, { - plugins: [ - 'preset-default', - 'prefixIds', - { - name: 'sortAttrs', - params: { - xmlnsOrder: 'alphabetical', - }, - }, - ], - }); -} - -/** - * get icon component template - * - * @param {string} name - */ -async function getJSXContent(name, svgCode) { - let svgrContent = ''; - try { - svgrContent = await svgr.transform( - svgCode, - { - icon: true, - typescript: true, - }, - { componentName: `${name}Icon1` } - ); - } catch (err) { - console.error(err); - } - let matcher = svgrContent.match(/]+)>([\s\S]*?)<\/svg>/); - return ` -import { SvgIcon, SvgIconProps } from '@mui/material'; -export const ${name}Icon = (props: SvgIconProps) => ( - - ${matcher[2]} - -); -`; -} - -async function generateImportEntry(iconNodes, folder) { - const fileWithImportsPath = path.resolve(folder, 'index.ts'); - - const importsContent = iconNodes - .map(iconNode => { - const iconName = paramCase(iconNode.name); - if (!iconName) { - return `// Error: ${iconNode.name}`; - } - - return `export * from './${iconName}/${iconName}';`; - }) - .join('\n'); - - await fs.writeFile( - fileWithImportsPath, - `export const timestamp = ${Date.now()};\n${importsContent}`, - { encoding: 'utf8' } - ); -} - -/** - * @param {*} options - * @param {array} options.assets - * @param {string} options.assets.folder - * @param {*} context - * @returns - */ -exports['default'] = async function svgo(options, context) { - const libRoot = context.workspace.projects[context.projectName].root; - await Promise.allSettled( - (options.assets || []).map(async (asset, index) => { - await optimizeSvg(path.resolve(libRoot, asset.folder)); - }) - ); - - return { success: true }; -};