fix: lint

This commit is contained in:
DiamondThree
2022-08-19 16:46:04 +08:00
26 changed files with 412 additions and 412 deletions

View File

@@ -5,7 +5,10 @@ import { getSession } from '@toeverything/components/board-sessions';
import { deepCopy, TldrawApp } from '@toeverything/components/board-state'; import { deepCopy, TldrawApp } from '@toeverything/components/board-state';
import { tools } from '@toeverything/components/board-tools'; import { tools } from '@toeverything/components/board-tools';
import { TDShapeType } from '@toeverything/components/board-types'; 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 { services } from '@toeverything/datasource/db-service';
import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo'; import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -16,7 +19,11 @@ interface AffineBoardProps {
rootBlockId: string; rootBlockId: string;
} }
const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => { const AffineBoard = ({
workspace,
rootBlockId,
editor,
}: AffineBoardProps & { editor: BlockEditor }) => {
const [app, set_app] = useState<TldrawApp>(); const [app, set_app] = useState<TldrawApp>();
const [document] = useState(() => { const [document] = useState(() => {
return { return {
@@ -61,8 +68,20 @@ const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => {
onMount(app) { onMount(app) {
set_app(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) { async onChangePage(app, shapes, bindings, assets) {
await Promise.all( Promise.all(
Object.entries(shapes).map(async ([id, shape]) => { Object.entries(shapes).map(async ([id, shape]) => {
if (shape === undefined) { if (shape === undefined) {
return services.api.editorBlock.delete({ return services.api.editorBlock.delete({
@@ -167,7 +186,11 @@ export const AffineBoardWitchContext = ({
}, [editor, rootBlockId]); }, [editor, rootBlockId]);
return page ? ( return page ? (
<RecastBlockProvider block={page}> <RecastBlockProvider block={page}>
<AffineBoard workspace={workspace} rootBlockId={rootBlockId} /> <AffineBoard
workspace={workspace}
rootBlockId={rootBlockId}
editor={editor}
/>
</RecastBlockProvider> </RecastBlockProvider>
) : null; ) : null;
}; };

View File

@@ -8,9 +8,10 @@ import {
TDShapeType, TDShapeType,
TransformInfo, TransformInfo,
} from '@toeverything/components/board-types'; } from '@toeverything/components/board-types';
import type { BlockEditor } from '@toeverything/components/editor-core';
import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core'; import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; 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 { memo, useCallback, useEffect, useRef } from 'react';
import { import {
defaultTextStyle, defaultTextStyle,
@@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
Component = TDShapeUtil.Component<T, E, TDMeta>( Component = TDShapeUtil.Component<T, E, TDMeta>(
({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => { ({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const editorRef = useRef<BlockEditor>();
const { const {
workspace, workspace,
rootBlockId, rootBlockId,
@@ -135,6 +137,27 @@ export class EditorUtil extends TDShapeUtil<T, E> {
} }
}, [app, state, shape.id, editingText, editingId]); }, [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 ( return (
<HTMLContainer ref={ref} {...events}> <HTMLContainer ref={ref} {...events}>
<Container <Container
@@ -143,12 +166,14 @@ export class EditorUtil extends TDShapeUtil<T, E> {
onPointerDown={stopPropagation} onPointerDown={stopPropagation}
onMouseEnter={activateIfEditing} onMouseEnter={activateIfEditing}
onDragEnter={activateIfEditing} onDragEnter={activateIfEditing}
onMouseDown={onMouseDown}
> >
<MemoAffineEditor <MemoAffineEditor
workspace={workspace} workspace={workspace}
rootBlockId={rootBlockId} rootBlockId={rootBlockId}
scrollBlank={false} scrollBlank={false}
isEdgeless isEdgeless
ref={editorRef}
/> />
{editingText ? null : <Mask />} {editingText ? null : <Mask />}
</Container> </Container>

View File

@@ -171,6 +171,10 @@ interface TDCallbacks {
* (optional) A callback to run when the user exports their page or selection. * (optional) A callback to run when the user exports their page or selection.
*/ */
onExport?: (app: TldrawApp, info: TDExport) => Promise<void>; onExport?: (app: TldrawApp, info: TDExport) => Promise<void>;
/**
* (optional) A callback to run when the shape is copied.
*/
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
} }
export interface TldrawAppCtorProps { export interface TldrawAppCtorProps {
@@ -1898,12 +1902,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
/** /**
* Copy one or more shapes to the clipboard. * Copy one or more shapes to the clipboard.
* @param ids The ids of the shapes to copy. * @param ids The ids of the shapes to copy.
* @param pageId
* @param e
*/ */
copy = ( copy = async (
ids = this.selectedIds, ids = this.selectedIds,
pageId = this.currentPageId, pageId = this.currentPageId,
e?: ClipboardEvent e?: ClipboardEvent
): this => { ) => {
e?.preventDefault(); e?.preventDefault();
this.clipboard = this.get_clipboard(ids, pageId); this.clipboard = this.get_clipboard(ids, pageId);
@@ -1919,17 +1925,24 @@ export class TldrawApp extends StateManager<TDSnapshot> {
if (e) { if (e) {
e.clipboardData?.setData('text/html', tldrawString); e.clipboardData?.setData('text/html', tldrawString);
await this.callbacks.onCopy?.(e, this.selectedIds);
} }
if (navigator.clipboard && window.ClipboardItem) { /**
navigator.clipboard.write([ * Reasons for not using Clipboard API for now:
new ClipboardItem({ * 1. The `clipboardData.setData` method temporarily satisfies the need for replication functionality
'text/html': new Blob([tldrawString], { * 2. Clipboard API requires the user to agree to access(https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)
type: 'text/html', *
}), * **/
}), // 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.offset = [0, 0];
this.pasteInfo.center = [0, 0]; this.pasteInfo.center = [0, 0];
@@ -3841,7 +3854,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => { private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => {
if (typeof svgStr === 'string') { 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'); return viewBox.children[0].getAttribute('viewBox');
} }
@@ -4125,7 +4138,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}; };
onPointerDown: TLPointerEventHandler = (info, e) => { onPointerDown: TLPointerEventHandler = (info, e) => {
if (e.buttons === 4) { if (e.button === 1) {
this.patchState({ this.patchState({
settings: { settings: {
forcePanning: true, forcePanning: true,
@@ -4142,6 +4155,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}; };
onPointerUp: TLPointerEventHandler = (info, e) => { onPointerUp: TLPointerEventHandler = (info, e) => {
if (e.button === 1) {
this.patchState({
settings: {
forcePanning: false,
},
});
}
this.isPointing = false; this.isPointing = false;
this.updateInputs(info, e); this.updateInputs(info, e);
this.currentTool.onPointerUp?.(info, e); this.currentTool.onPointerUp?.(info, e);

View File

@@ -1,12 +1,12 @@
import { useState } from 'react';
import { CreateView } from '@toeverything/framework/virgo';
import { import {
BlockPendantProvider, BlockPendantProvider,
useOnSelect, useOnSelect,
} from '@toeverything/components/editor-core'; } 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 { SourceView } from '../../components/source-view';
import { LinkContainer } from '../../components/style-container'; import { LinkContainer } from '../../components/style-container';
import { Upload } from '../../components/upload/upload';
const MESSAGES = { const MESSAGES = {
ADD_EMBED_LINK: 'Add embed link', ADD_EMBED_LINK: 'Add embed link',
@@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
{embedLinkUrl ? ( {embedLinkUrl ? (
<SourceView <SourceView
block={block} block={block}
editorElement={props.editorElement}
isSelected={isSelect} isSelected={isSelect}
viewType="embedLink" viewType="embedLink"
link={embedLinkUrl} link={embedLinkUrl}

View File

@@ -1,11 +1,18 @@
import type { KanbanCard } from '@toeverything/components/editor-core';
import { import {
KanbanCard,
RenderBlock, RenderBlock,
useEditor,
useKanban, useKanban,
useRefPage,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { PenIcon } from '@toeverything/components/icons';
import {
IconButton,
MuiClickAwayListener,
styled,
} from '@toeverything/components/ui';
import { useFlag } from '@toeverything/datasource/feature-flags'; import { useFlag } from '@toeverything/datasource/feature-flags';
import { useState, type MouseEvent } from 'react';
import { useRefPage } from './RefPage';
const CardContent = styled('div')({ const CardContent = styled('div')({
margin: '20px', margin: '20px',
@@ -23,6 +30,7 @@ const CardActions = styled('div')({
fontWeight: '300', fontWeight: '300',
color: '#98ACBD', color: '#98ACBD',
transition: 'all ease-in 0.2s', transition: 'all ease-in 0.2s',
zIndex: 1,
':hover': { ':hover': {
background: '#F5F7F8', background: '#F5F7F8',
@@ -39,11 +47,13 @@ const PlusIcon = styled('div')({
}); });
const CardContainer = styled('div')({ const CardContainer = styled('div')({
position: 'relative',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: '#fff', backgroundColor: '#fff',
border: '1px solid #E2E7ED', border: '1px solid #E2E7ED',
borderRadius: '5px', borderRadius: '5px',
overflow: 'hidden',
[CardActions.toString()]: { [CardActions.toString()]: {
opacity: '0', opacity: '0',
@@ -55,6 +65,23 @@ const CardContainer = styled('div')({
}, },
}); });
const Overlay = styled('div')({
position: 'absolute',
width: '100%',
height: '100%',
background: 'transparent',
'& > *': {
visibility: 'hidden',
position: 'absolute',
right: '24px',
top: '16px',
},
'&:hover > *': {
visibility: 'visible',
},
});
export const CardItem = ({ export const CardItem = ({
id, id,
block, block,
@@ -64,24 +91,43 @@ export const CardItem = ({
}) => { }) => {
const { addSubItem } = useKanban(); const { addSubItem } = useKanban();
const { openSubPage } = useRefPage(); const { openSubPage } = useRefPage();
const [editable, setEditable] = useState(false);
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false); const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
const { editor } = useEditor();
const onAddItem = async () => { const onAddItem = async () => {
setEditable(true);
await addSubItem(block); await addSubItem(block);
}; };
const onClickCard = async () => { const onClickCard = async () => {
showKanbanRefPageFlag && openSubPage(id); openSubPage(id);
};
const onClickPen = (e: MouseEvent<Element>) => {
e.stopPropagation();
setEditable(true);
editor.selectionManager.activeNodeByNodeId(block.id);
}; };
return ( return (
<CardContainer onClick={onClickCard}> <MuiClickAwayListener onClickAway={() => setEditable(false)}>
<CardContainer>
<CardContent> <CardContent>
<RenderBlock blockId={id} /> <RenderBlock blockId={id} />
</CardContent> </CardContent>
{showKanbanRefPageFlag && !editable && (
<Overlay onClick={onClickCard}>
<IconButton backgroundColor="#fff" onClick={onClickPen}>
<PenIcon />
</IconButton>
</Overlay>
)}
<CardActions onClick={onAddItem}> <CardActions onClick={onAddItem}>
<PlusIcon /> <PlusIcon />
<span>Add a sub-block</span> <span>Add a sub-block</span>
</CardActions> </CardActions>
</CardContainer> </CardContainer>
</MuiClickAwayListener>
); );
}; };

View File

@@ -1,7 +1,7 @@
import { useEditor } from '@toeverything/components/editor-core';
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui'; import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
import { createContext, ReactNode, useContext, useState } from 'react'; import { createContext, ReactNode, useContext, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { RenderBlock } from '../render-block';
const Dialog = styled('div')({ const Dialog = styled('div')({
flex: 1, flex: 1,
@@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
onClick={closeSubPage} onClick={closeSubPage}
> >
<Dialog <Dialog
onClick={e => { onClick={(e: { stopPropagation: () => void }) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
@@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
}; };
const ModalPage = ({ blockId }: { blockId: string | null }) => { const ModalPage = ({ blockId }: { blockId: string | null }) => {
const { editor, editorElement } = useEditor();
const AffineEditor = editorElement as any;
return ( return (
<Modal open={!!blockId}> <Modal open={!!blockId}>
{blockId && <RenderBlock blockId={blockId} />} {blockId && (
<AffineEditor
workspace={editor.workspace}
rootBlockId={blockId}
scrollBlank={false}
// use edgeless mode prevent padding and blank bottom
isEdgeless
/>
)}
</Modal> </Modal>
); );
}; };

View File

@@ -4,12 +4,14 @@ import { SceneKanbanContext } from './context';
import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper'; import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import type { CreateView } from '@toeverything/framework/virgo'; import type { CreateView } from '@toeverything/framework/virgo';
import { RefPageProvider } from './RefPage';
export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>( export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
({ editor, block }) => { ({ editor, block }) => {
const { kanban } = useKanban(); const { kanban } = useKanban();
return ( return (
<RefPageProvider>
<SceneKanbanContext.Provider value={{ editor, block }}> <SceneKanbanContext.Provider value={{ editor, block }}>
<CardContainerWrapper <CardContainerWrapper
dataSource={kanban} dataSource={kanban}
@@ -28,6 +30,7 @@ export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
)} )}
/> />
</SceneKanbanContext.Provider> </SceneKanbanContext.Provider>
</RefPageProvider>
); );
} }
); );

View File

@@ -1,9 +1,9 @@
import { memo, useEffect, useRef, useState } from 'react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { memo, useEffect, useRef, useState } from 'react';
import { StyledBlockPreview } from '@toeverything/components/common'; import { StyledBlockPreview } from '@toeverything/components/common';
import { AsyncBlock, useEditor } from '@toeverything/components/editor-core';
import { services } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service';
import { AsyncBlock } from '@toeverything/framework/virgo';
import { debounce, sleep } from '@toeverything/utils'; import { debounce, sleep } from '@toeverything/utils';
const updateTitle = async ( const updateTitle = async (
@@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => {
type BlockPreviewProps = { type BlockPreviewProps = {
block: AsyncBlock; block: AsyncBlock;
blockId: string; blockId: string;
editorElement?: () => JSX.Element;
}; };
const InternalBlockPreview = (props: BlockPreviewProps) => { const InternalBlockPreview = (props: BlockPreviewProps) => {
const container = useRef<HTMLDivElement>(); const container = useRef<HTMLDivElement>();
const [preview, setPreview] = useState(true); const [preview, setPreview] = useState(true);
const title = useBlockTitle(props.block, props.blockId); const title = useBlockTitle(props.block, props.blockId);
const { editorElement } = useEditor();
const AffineEditor = props.editorElement as any; const AffineEditor = editorElement as any;
useEffect(() => { useEffect(() => {
if (container?.current) { if (container?.current) {

View File

@@ -1,17 +1,15 @@
import { import {
AsyncBlock, AsyncBlock,
useCurrentView, useCurrentView,
useLazyIframe,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; 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 { SCENE_CONFIG } from '../../blocks/group/config';
import { BlockPreview } from './BlockView'; import { BlockPreview } from './BlockView';
import { formatUrl } from './format-url'; import { formatUrl } from './format-url';
export interface Props { export interface Props {
block: AsyncBlock; block: AsyncBlock;
editorElement?: () => JSX.Element;
viewType?: string; viewType?: string;
link: string; link: string;
// onResizeEnd: (data: any) => void; // onResizeEnd: (data: any) => void;
@@ -150,7 +148,7 @@ const LoadingContiner = () => {
}; };
export const SourceView = (props: Props) => { export const SourceView = (props: Props) => {
const { link, isSelected, block, editorElement } = props; const { link, isSelected, block } = props;
const src = formatUrl(link); const src = formatUrl(link);
// let iframeShow = useLazyIframe(src, 3000, iframeContainer); // let iframeShow = useLazyIframe(src, 3000, iframeContainer);
const [currentView] = useCurrentView(); const [currentView] = useCurrentView();
@@ -161,10 +159,7 @@ export const SourceView = (props: Props) => {
<SourceViewContainer isSelected={isSelected} scene={type}> <SourceViewContainer isSelected={isSelected} scene={type}>
<MouseMaskContainer /> <MouseMaskContainer />
<LazyIframe <LazyIframe src={src} fallback={LoadingContiner()} />
src={src}
fallback={LoadingContiner()}
></LazyIframe>
</SourceViewContainer> </SourceViewContainer>
</div> </div>
); );
@@ -175,11 +170,7 @@ export const SourceView = (props: Props) => {
style={{ padding: '0' }} style={{ padding: '0' }}
scene={type} scene={type}
> >
<BlockPreview <BlockPreview block={block} blockId={src} />
block={block}
editorElement={editorElement}
blockId={src}
/>
</SourceViewContainer> </SourceViewContainer>
); );
} }

View File

@@ -1,18 +1,6 @@
import { HooksRunner } from '../types'; import { HooksRunner } from '../types';
import {
OFFICE_CLIPBOARD_MIMETYPE,
InnerClipInfo,
ClipBlockInfo,
} from './types';
import { Editor } from '../editor'; import { Editor } from '../editor';
import { AsyncBlock } from '../block';
import ClipboardParse from './clipboard-parse'; import ClipboardParse from './clipboard-parse';
import { SelectInfo } from '../selection';
import {
Protocol,
BlockFlavorKeys,
services,
} from '@toeverything/datasource/db-service';
import { MarkdownParser } from './markdown-parse'; import { MarkdownParser } from './markdown-parse';
import { shouldHandlerContinue } from './utils'; import { shouldHandlerContinue } from './utils';
import { Paste } from './paste'; import { Paste } from './paste';

View File

@@ -1,23 +1,16 @@
import { Editor } from '../editor'; import { Editor } from '../editor';
import { SelectionManager, SelectInfo, SelectBlock } from '../selection'; import { SelectionManager } from '../selection';
import { HookType, PluginHooks } from '../types'; 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 ClipboardParse from './clipboard-parse';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Copy } from './copy';
class ClipboardPopulator { class ClipboardPopulator {
private _editor: Editor; private _editor: Editor;
private _hooks: PluginHooks; private _hooks: PluginHooks;
private _selectionManager: SelectionManager; private _selectionManager: SelectionManager;
private _clipboardParse: ClipboardParse; private _clipboardParse: ClipboardParse;
private _sub = new Subscription(); private _sub = new Subscription();
private _copy: Copy;
constructor( constructor(
editor: Editor, editor: Editor,
hooks: PluginHooks, hooks: PluginHooks,
@@ -27,128 +20,15 @@ class ClipboardPopulator {
this._hooks = hooks; this._hooks = hooks;
this._selectionManager = selectionManager; this._selectionManager = selectionManager;
this._clipboardParse = new ClipboardParse(editor); this._clipboardParse = new ClipboardParse(editor);
this._copy = new Copy(editor);
this._sub.add( this._sub.add(
hooks hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy)
.get(HookType.BEFORE_COPY)
.subscribe(this._populateAppClipboard)
); );
this._sub.add( 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<InnerClipInfo> {
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() { disposeInternal() {
this._sub.unsubscribe(); this._sub.unsubscribe();
this._hooks = null; this._hooks = null;

View File

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

View File

@@ -241,15 +241,12 @@ export class Paste {
[] []
); );
selectedBlock.setProperties({ await selectedBlock.setProperties({
text: { text: {
value: newTextValue, value: newTextValue,
}, },
}); });
const pasteBlocks = await this._createBlocks(blocks); const pastedBlocks = await this._createBlocks(blocks);
await Promise.all(
pasteBlocks.map(block => selectedBlock.after(block))
);
const nextBlock = await this._editor.createBlock( const nextBlock = await this._editor.createBlock(
selectedBlock?.type selectedBlock?.type
@@ -259,11 +256,12 @@ export class Paste {
value: nextTextValue, value: nextTextValue,
}, },
}); });
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
this._setEndSelectToBlock( await this._insertBlocksAfterBlock(selectedBlock, [
pasteBlocks[pasteBlocks.length - 1].id ...pastedBlocks,
); nextBlock,
]);
await this._setEndSelectToBlock(nextBlock.id);
} else { } else {
this._editor.blockHelper.insertNodes( this._editor.blockHelper.insertNodes(
selectedBlock.id, selectedBlock.id,
@@ -327,10 +325,7 @@ export class Paste {
value: newTextValue, value: newTextValue,
}, },
}); });
const pasteBlocks = await this._createBlocks(blocks); const pastedBlocks = await this._createBlocks(blocks);
pasteBlocks.forEach((block: AsyncBlock) => {
selectedBlock.after(block);
});
const nextBlock = await this._editor.createBlock( const nextBlock = await this._editor.createBlock(
selectedBlock?.type selectedBlock?.type
); );
@@ -339,11 +334,12 @@ export class Paste {
value: nextTextValue, value: nextTextValue,
}, },
}); });
pasteBlocks[pasteBlocks.length - 1].after(nextBlock); await this._insertBlocksAfterBlock(selectedBlock, [
...pastedBlocks,
nextBlock,
]);
this._setEndSelectToBlock( await this._setEndSelectToBlock(nextBlock.id);
pasteBlocks[pasteBlocks.length - 1].id
);
} else { } else {
this._editor.blockHelper.insertNodes( this._editor.blockHelper.insertNodes(
selectedBlock.id, selectedBlock.id,
@@ -353,10 +349,10 @@ export class Paste {
} }
} }
} else { } else {
const pasteBlocks = await this._createBlocks(blocks); const pastedBlocks = await this._createBlocks(blocks);
await Promise.all( await Promise.all(
pasteBlocks.map(block => selectedBlock.after(block)) pastedBlocks.map(block => selectedBlock.after(block))
); );
if (isSelectedBlockEmpty) { if (isSelectedBlockEmpty) {
@@ -364,7 +360,7 @@ export class Paste {
} }
this._setEndSelectToBlock( 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] currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1]
.blockId .blockId
); );
const pasteBlocks = await this._createBlocks(blocks); const pastedBlocks = await this._createBlocks(blocks);
let groupBlock: AsyncBlock; let groupBlock: AsyncBlock;
if ( if (selectedBlock?.type === 'page') {
selectedBlock?.type === 'group' ||
selectedBlock?.type === 'page'
) {
groupBlock = await this._editor.createBlock('group'); groupBlock = await this._editor.createBlock('group');
await Promise.all( await Promise.all(
pasteBlocks.map(block => groupBlock.append(block)) pastedBlocks.map(block => groupBlock.append(block))
); );
await selectedBlock.after(groupBlock); await selectedBlock.after(groupBlock);
} else if (selectedBlock?.type === 'group') {
await Promise.all(
pastedBlocks.map(block => selectedBlock.append(block))
);
} else { } else {
await Promise.all( 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) { private async _setEndSelectToBlock(blockId: string) {
const block = await this._editor.getBlockById(blockId); const block = await this._editor.getBlockById(blockId);
const isBlockCanEdit = Paste._isTextEditBlock(block.type); const isBlockCanEdit = Paste._isTextEditBlock(block.type);
@@ -406,14 +414,37 @@ export class Paste {
}, 100); }, 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( return Promise.all(
blocks.map(async clipBlockInfo => { this._flatGroupBlocks(blocks).map(async clipBlockInfo => {
const block = await this._editor.createBlock( const block = await this._editor.createBlock(
clipBlockInfo.type clipBlockInfo.type
); );
block?.setProperties(clipBlockInfo.properties); 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; return block;
}) })
); );

View File

@@ -1,4 +1,6 @@
import { Editor } from '../editor'; import { Editor } from '../editor';
import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types';
import { Clip } from './clip';
export const shouldHandlerContinue = (event: Event, editor: Editor) => { export const shouldHandlerContinue = (event: Event, editor: Editor) => {
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
@@ -12,3 +14,39 @@ export const shouldHandlerContinue = (event: Event, editor: Editor) => {
return editor.selectionManager.currentSelectInfo.type !== 'None'; 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,
})
);
};

View File

@@ -1,6 +1,5 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import HotKeys from 'hotkeys-js'; import HotKeys from 'hotkeys-js';
import LRUCache from 'lru-cache';
import type { PatchNode } from '@toeverything/components/ui'; import type { PatchNode } from '@toeverything/components/ui';
import type { import type {
@@ -46,10 +45,7 @@ export interface EditorCtorProps {
} }
export class Editor implements Virgo { export class Editor implements Virgo {
private _cacheManager = new LRUCache<string, Promise<AsyncBlock | null>>({ private _cacheManager = new Map<string, Promise<AsyncBlock | null>>();
max: 8192,
ttl: 1000 * 60 * 30,
});
public mouseManager = new MouseManager(this); public mouseManager = new MouseManager(this);
public selectionManager = new SelectionManager(this); public selectionManager = new SelectionManager(this);
public keyboardManager = new KeyboardManager(this); public keyboardManager = new KeyboardManager(this);
@@ -234,7 +230,12 @@ export class Editor implements Virgo {
return await services.api.editorBlock.update(patches); return await services.api.editorBlock.update(patches);
}, },
remove: async ({ workspace, id }: WorkspaceAndBlockId) => { 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 ( observe: async (
{ workspace, id }: WorkspaceAndBlockId, { workspace, id }: WorkspaceAndBlockId,

View File

@@ -10,3 +10,4 @@ export { BlockDropPlacement, HookType, GroupDirection } from './types';
export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types'; export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
export { BaseView, getTextHtml, getTextProperties } from './views/base-view'; export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
export type { ChildrenView, CreateView } from './views/base-view'; export type { ChildrenView, CreateView } from './views/base-view';
export { getClipDataOfBlocksById } from './clipboard/utils';

View File

@@ -18,7 +18,6 @@ import { SelectBlock } from '../selection';
export interface CreateView { export interface CreateView {
block: AsyncBlock; block: AsyncBlock;
editor: Editor; editor: Editor;
editorElement: () => JSX.Element;
/** /**
* @deprecated Use recast table instead * @deprecated Use recast table instead
*/ */

View File

@@ -16,4 +16,4 @@ export * from './utils';
export * from './editor'; export * from './editor';
export { RefPageProvider, useRefPage } from './ref-page'; export { useEditor } from './Contexts';

View File

@@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock } from '../editor'; import { AsyncBlock } from '../editor';
import { ComponentType, createContext, ReactNode, useContext } from 'react'; import { ComponentType, createContext, ReactNode, useContext } from 'react';
import { RecastBlock } from './types'; import { RecastBlock } from './types';
import { RefPageProvider } from '../ref-page';
/** /**
* Determine whether the block supports RecastBlock * Determine whether the block supports RecastBlock
@@ -48,7 +47,7 @@ export const RecastBlockProvider = ({
return ( return (
<RecastBlockContext.Provider value={block}> <RecastBlockContext.Provider value={block}>
<RefPageProvider>{children}</RefPageProvider> {children}
</RecastBlockContext.Provider> </RecastBlockContext.Provider>
); );
}; };

View File

@@ -1 +0,0 @@
export { useRefPage, RefPageProvider } from './ModalPage';

View File

@@ -13,7 +13,7 @@ export function RenderBlock({
blockId, blockId,
hasContainer = true, hasContainer = true,
}: RenderBlockProps) { }: RenderBlockProps) {
const { editor, editorElement } = useEditor(); const { editor } = useEditor();
const { block } = useBlock(blockId); const { block } = useBlock(blockId);
const setRef = useCallback( const setRef = useCallback(
@@ -50,7 +50,6 @@ export function RenderBlock({
block={block} block={block}
columns={columns.columns} columns={columns.columns}
columnsFromId={columns.fromId} columnsFromId={columns.fromId}
editorElement={editorElement}
/> />
); );

View File

@@ -21,7 +21,7 @@ export const plugins: PluginCreator[] = [
CommandMenuPlugin, CommandMenuPlugin,
ReferenceMenuPlugin, ReferenceMenuPlugin,
TemplatePlugin, TemplatePlugin,
SelectionGroupPlugin, // SelectionGroupPlugin,
AddCommentPlugin, AddCommentPlugin,
GroupMenuPlugin, GroupMenuPlugin,
]; ];

View File

@@ -1,9 +0,0 @@
{
"executors": {
"svgOptimize": {
"implementation": "./svgo.js",
"schema": "./schema.json",
"description": "Run `svgo` (to optimize svg)."
}
}
}

View File

@@ -1,3 +0,0 @@
{
"executors": "./executor.json"
}

View File

@@ -1,11 +0,0 @@
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"cli": "nx",
"properties": {
"svgOptimize": {
"type": "string",
"description": "optimize svg"
}
}
}

View File

@@ -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(/<svg ([^\>]+)>([\s\S]*?)<\/svg>/);
return `
import { SvgIcon, SvgIconProps } from '@mui/material';
export const ${name}Icon = (props: SvgIconProps) => (
<SvgIcon ${matcher[1]}>
${matcher[2]}
</SvgIcon>
);
`;
}
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 };
};