mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +08:00
merge branch develop into branch feat/doublelink220820
This commit is contained in:
@@ -5,10 +5,7 @@ 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 {
|
||||
getClipDataOfBlocksById,
|
||||
RecastBlockProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { 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';
|
||||
@@ -51,10 +48,10 @@ const AffineBoard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const { shapes, bindings } = useShapes(workspace, rootBlockId);
|
||||
const { shapes } = useShapes(workspace, rootBlockId);
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
app.replacePageContent(shapes || {}, bindings, {});
|
||||
app.replacePageContent(shapes || {}, {}, {});
|
||||
}
|
||||
}, [app, shapes]);
|
||||
|
||||
@@ -68,12 +65,14 @@ const AffineBoard = ({
|
||||
onMount(app) {
|
||||
set_app(app);
|
||||
},
|
||||
|
||||
async onPaste(e, data) {
|
||||
console.log('e,data: ', e, data);
|
||||
},
|
||||
async onCopy(e, groupIds) {
|
||||
const clip = await getClipDataOfBlocksById(
|
||||
editor,
|
||||
groupIds
|
||||
);
|
||||
const clip =
|
||||
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
|
||||
groupIds
|
||||
);
|
||||
|
||||
e.clipboardData?.setData(
|
||||
clip.getMimeType(),
|
||||
@@ -109,19 +108,6 @@ const AffineBoard = ({
|
||||
});
|
||||
}
|
||||
shape.affineId = block.id;
|
||||
|
||||
Object.keys(bindings).forEach(bilingKey => {
|
||||
if (
|
||||
bindings[bilingKey]?.fromId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].fromId = block.id;
|
||||
}
|
||||
if (
|
||||
bindings[bilingKey]?.toId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].toId = block.id;
|
||||
}
|
||||
});
|
||||
return await services.api.editorBlock.update({
|
||||
workspace: shape.workspace,
|
||||
id: block.id,
|
||||
@@ -134,32 +120,6 @@ const AffineBoard = ({
|
||||
}
|
||||
})
|
||||
);
|
||||
let pageBindingsString = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
)?.[0].properties.bindings?.value;
|
||||
console.log(123123123);
|
||||
let pageBindings = JSON.parse(pageBindingsString ?? '{}');
|
||||
console.log(pageBindings, 3333, bindings);
|
||||
Object.keys(bindings).forEach(bindingsKey => {
|
||||
console.log(345345345345345);
|
||||
if (!bindings[bindingsKey]) {
|
||||
delete pageBindings[bindingsKey];
|
||||
} else {
|
||||
Object.assign(pageBindings, bindings);
|
||||
}
|
||||
});
|
||||
services.api.editorBlock.update({
|
||||
workspace: workspace,
|
||||
id: rootBlockId,
|
||||
properties: {
|
||||
bindings: {
|
||||
value: JSON.stringify(pageBindings),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,24 +5,12 @@ import { services } from '@toeverything/datasource/db-service';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const getBindings = (workspace: string, rootBlockId: string) => {
|
||||
return services.api.editorBlock
|
||||
.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
.then(blcoks => {
|
||||
return blcoks[0].properties.bindings?.value;
|
||||
});
|
||||
};
|
||||
|
||||
export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
const [blocks, setBlocks] = useState<{
|
||||
shapes: [ReturnEditorBlock[]];
|
||||
bindings: string;
|
||||
}>();
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -43,11 +31,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return shapes;
|
||||
}),
|
||||
]).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes,
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: shapes,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,11 +50,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return childBlock;
|
||||
})
|
||||
).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -104,8 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, TDShape>);
|
||||
|
||||
return {
|
||||
shapes: blocksShapes,
|
||||
bindings: JSON.parse(blocks?.bindings ?? '{}'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import {
|
||||
RenderBlock,
|
||||
RenderRoot,
|
||||
type BlockEditor,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
editor={editor}
|
||||
editorElement={AffineEditor as any}
|
||||
scrollBlank={scrollBlank}
|
||||
>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</RenderRoot>
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -362,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name === 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId;
|
||||
|
||||
@@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px',
|
||||
padding: '3px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
'&:hover': {
|
||||
|
||||
@@ -21,6 +21,9 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
let activeShape = shapes[0];
|
||||
let toNextShapBindings: ArrowBinding[] = [];
|
||||
let bindingId = '';
|
||||
if (!activeShape) {
|
||||
return;
|
||||
}
|
||||
Object.keys(bindings).forEach(key => {
|
||||
if (bindings[key].toId === activeShape.id) {
|
||||
bindingId = bindings[key].fromId;
|
||||
@@ -35,7 +38,6 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
toNextShapBindings.forEach(binding => {
|
||||
if (binding.toId !== activeShape.id) {
|
||||
allShape.forEach(item => {
|
||||
console.log(item);
|
||||
if (item.id === binding.toId) {
|
||||
ArrowToArr.push(item);
|
||||
}
|
||||
@@ -44,7 +46,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
});
|
||||
setarrowToArr(ArrowToArr);
|
||||
return () => {};
|
||||
}, [app.page.bindings, app.shapes]);
|
||||
}, [app.page.bindings]);
|
||||
const jumpToNextShap = (shape: TDShape) => {
|
||||
app.zoomToShapes([shape]);
|
||||
};
|
||||
@@ -68,7 +70,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip content="Font Size" placement="top-start">
|
||||
<Tooltip content="ArrowToEditor" placement="top-start">
|
||||
<IconButton>
|
||||
<ConnectorIcon></ConnectorIcon>
|
||||
</IconButton>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TLDR, TldrawApp } from '@toeverything/components/board-state';
|
||||
import { Divider, Popover, styled } from '@toeverything/components/ui';
|
||||
import { Fragment } from 'react';
|
||||
import { AlignOperation } from './AlignOperation';
|
||||
import { ArrowTo } from './ArrowTo';
|
||||
import { BorderColorConfig } from './BorderColorConfig';
|
||||
import { DeleteShapes } from './DeleteOperation';
|
||||
import { FillColorConfig } from './FillColorConfig';
|
||||
@@ -107,12 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></AlignOperation>
|
||||
) : null,
|
||||
toNextShap: (
|
||||
<ArrowTo
|
||||
app={app}
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></ArrowTo>
|
||||
),
|
||||
// toNextShap: (
|
||||
// <ArrowTo
|
||||
// app={app}
|
||||
// shapes={config.deleteShapes.selectedShapes}
|
||||
// ></ArrowTo>
|
||||
// ),
|
||||
};
|
||||
|
||||
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);
|
||||
|
||||
@@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Line
|
||||
@@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Rectangle
|
||||
@@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
import {
|
||||
IconButton,
|
||||
PopoverContainer,
|
||||
styled,
|
||||
// MuiIconButton as IconButton,
|
||||
// MuiTooltip as Tooltip,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { TldrawApp } from '@toeverything/components/board-state';
|
||||
import {
|
||||
@@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
}}
|
||||
direction="none"
|
||||
>
|
||||
<div className={styles('container')}>
|
||||
<div className={styles('toolBar')}>
|
||||
<Container>
|
||||
<ToolBar>
|
||||
{tools.map(
|
||||
({
|
||||
type,
|
||||
@@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolBar>
|
||||
</Container>
|
||||
</PopoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
toolBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
},
|
||||
const Container = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
const ToolBar = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
styled,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { ReactElement, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties, type ReactElement } from 'react';
|
||||
import { Palette } from '../../palette';
|
||||
import { Pen } from './Pen';
|
||||
|
||||
@@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
|
||||
|
||||
export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
const appCurrentTool = app.useStore(state => state.appState.activeTool);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const chosenPen =
|
||||
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
|
||||
PENCIL_CONFIGS[0];
|
||||
@@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<Container>
|
||||
<PensContainer>
|
||||
@@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
name={title}
|
||||
primaryColor={color_vars['--color-0']}
|
||||
secondaryColor={color_vars['--color-1']}
|
||||
onClick={() => setPen(key)}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
setPen(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
<Palette
|
||||
selected={chosenColor}
|
||||
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
|
||||
onSelect={color => setPenColor(color)}
|
||||
onSelect={color => {
|
||||
setVisible(false);
|
||||
setPenColor(color);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
</Container>
|
||||
|
||||
@@ -42,7 +42,7 @@ type E = HTMLDivElement;
|
||||
export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Arrow as const;
|
||||
|
||||
override hideBounds = true;
|
||||
override hideBounds = false;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import {
|
||||
BINDING_DISTANCE,
|
||||
ShapeStyles,
|
||||
} from '@toeverything/components/board-types';
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
@@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
className={'tl-fill-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={1}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
width={w - sw / 2}
|
||||
height={h - sw / 2}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
@@ -175,6 +175,10 @@ interface TDCallbacks {
|
||||
* (optional) A callback to run when the shape is copied.
|
||||
*/
|
||||
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
|
||||
/**
|
||||
* (optional) A callback to run when the shape is paste.
|
||||
*/
|
||||
onPaste?: (e: ClipboardEvent, data: any) => void;
|
||||
}
|
||||
|
||||
export interface TldrawAppCtorProps {
|
||||
@@ -626,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
private prev_bindings = this.page.bindings;
|
||||
private prev_assets = this.document.assets;
|
||||
|
||||
private _broadcastPageChanges = () => {
|
||||
private _broadcastPageChanges = async () => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {};
|
||||
@@ -683,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.just_sent = true;
|
||||
this.callbacks.onChangePage?.(
|
||||
await this.callbacks.onChangePage?.(
|
||||
this,
|
||||
changedShapes,
|
||||
changedBindings,
|
||||
@@ -1956,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return;
|
||||
|
||||
if (e) {
|
||||
const data = e.clipboardData?.getData('text/html');
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
return json;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
this.callbacks.onPaste?.(e, paste_as_html(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const pasteInCurrentPage = (
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[],
|
||||
@@ -2107,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
// this.select(shapeId);
|
||||
};
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (e !== undefined) {
|
||||
const items: DataTransferItemList =
|
||||
e.clipboardData?.items ?? ({} as DataTransferItemList);
|
||||
|
||||
@@ -819,7 +819,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
|
||||
backgroundColor: '#F2F5F9',
|
||||
borderRadius: '5px',
|
||||
color: '#3A4C5C',
|
||||
padding: '3px 8px',
|
||||
padding: '1px 8px',
|
||||
margin: '0 2px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -978,7 +978,41 @@ class SlateUtils {
|
||||
}
|
||||
|
||||
public getNodeByPath(path: Path) {
|
||||
Editor.node(this.editor, path);
|
||||
return Editor.node(this.editor, path);
|
||||
}
|
||||
|
||||
public getNodeByRange(range: Range) {
|
||||
return Editor.node(this.editor, range);
|
||||
}
|
||||
|
||||
// This may should write with logic of render slate
|
||||
public convertLeaf2Html(textValue: any) {
|
||||
const { text, fontColor, fontBgColor } = textValue;
|
||||
|
||||
const style = `style="${fontColor ? `color: ${fontColor};` : ''}${
|
||||
fontBgColor ? `backgroundColor: ${fontBgColor};` : ''
|
||||
}"`;
|
||||
if (textValue.bold) {
|
||||
return `<strong ${style}>${text}</strong>`;
|
||||
}
|
||||
if (textValue.italic) {
|
||||
return `<em ${style}>${text}</em>`;
|
||||
}
|
||||
if (textValue.underline) {
|
||||
return `<u ${style}>${text}</u>`;
|
||||
}
|
||||
if (textValue.inlinecode) {
|
||||
return `<code ${style}>${text}</code>`;
|
||||
}
|
||||
if (textValue.strikethrough) {
|
||||
return `<s ${style}>${text}</s>`;
|
||||
}
|
||||
if (textValue.type === 'link') {
|
||||
return `<a href='${textValue.url}' ${style}>${this.convertLeaf2Html(
|
||||
textValue.children
|
||||
)}</a>`;
|
||||
}
|
||||
return `<span ${style}>${text}</span>`;
|
||||
}
|
||||
|
||||
public getStartSelection() {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"react-window": "^1.8.7",
|
||||
"slate": "^0.81.1",
|
||||
"slate-react": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codemirror": "^5.60.5",
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { BulletIcon, getChildrenType, NumberType } from './data';
|
||||
import { BulletBlock, BulletProperties } from './types';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
export const defaultBulletProps: BulletProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -189,7 +188,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<BulletLeft>
|
||||
<BulletIcon numberType={properties.numberType} />
|
||||
@@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
</div>
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
CreateView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { defaultBulletProps, BulletView } from './BulletView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { BulletView, defaultBulletProps } from './BulletView';
|
||||
|
||||
export class BulletBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.bullet;
|
||||
@@ -27,66 +26,44 @@ export class BulletBlock extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
if (!firstList || firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && !el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,54 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { cpp } from '@codemirror/lang-cpp';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { lezer } from '@codemirror/lang-lezer';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { julia } from '@codemirror/legacy-modes/mode/julia';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { DuplicateIcon } from '@toeverything/components/icons';
|
||||
import { Option, Select, styled } from '@toeverything/components/ui';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
|
||||
interface CreateCodeView extends CreateView {
|
||||
style9?: StyleWithAtRules;
|
||||
@@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
'&:hover': {
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
opacity: 0,
|
||||
transition: 'opacity 1.5s',
|
||||
},
|
||||
'.copy-block': {
|
||||
padding: '0px 10px',
|
||||
@@ -172,7 +170,7 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
editor.selectionManager.activePreviousNode(block.id, 'start');
|
||||
};
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<CodeBlock
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
@@ -200,7 +198,8 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
</div>
|
||||
<div>
|
||||
<div className="copy-block" onClick={copyCode}>
|
||||
<DuplicateIcon></DuplicateIcon>Copy
|
||||
<DuplicateIcon />
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import {
|
||||
BaseView,
|
||||
AsyncBlock,
|
||||
getTextProperties,
|
||||
CreateView,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CodeView } from './CodeView';
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class CodeBlock extends BaseView {
|
||||
type = Protocol.Block.Type.code;
|
||||
@@ -28,56 +27,22 @@ export class CodeBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'CODE',
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: internal format not implemented yet
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'CODE') {
|
||||
const childNodes = el.childNodes;
|
||||
let text_value = '';
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
if (block_texts.length > 0) {
|
||||
text_value += block_texts[0].text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: [{ text: text_value }] },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<code>${content}</code>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<code>${await commonBlock2HtmlContent(props)}<code/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { DividerView } from './divider-view';
|
||||
import { Block2HtmlProps, commonHTML2block } from '../../utils/commonBlockClip';
|
||||
|
||||
export class DividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.divider;
|
||||
View = DividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'HR',
|
||||
ignoreEmptyElement: false,
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { EmbedLinkView } from './EmbedLinkView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class EmbedLinkBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
@@ -12,36 +13,8 @@ export class EmbedLinkBlock extends BaseView {
|
||||
type = Protocol.Block.Type.embedLink;
|
||||
View = EmbedLinkView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: Not sure what value to fill for name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const figma_url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${figma_url}>${figma_url}</p>`;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a href="${url}">${url}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
useOnSelect,
|
||||
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 { styled } from '@toeverything/components/ui';
|
||||
import { LinkContainer } from '../../components/style-container';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
|
||||
const MESSAGES = {
|
||||
ADD_FIGMA_LINK: 'Add figma link',
|
||||
@@ -30,7 +29,7 @@ export const FigmaView = ({ block, editor }: FigmaView) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{figmaUrl ? (
|
||||
<SourceView
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { FigmaView } from './FigmaView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FigmaBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
@@ -12,42 +13,8 @@ export class FigmaBlock extends BaseView {
|
||||
type = Protocol.Block.Type.figma;
|
||||
View = FigmaView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
const href = el.getAttribute('href');
|
||||
const allowedHosts = ['www.figma.com'];
|
||||
const host = new URL(href).host;
|
||||
|
||||
if (allowedHosts.includes(host)) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: Not sure what value to fill for name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const figma_url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${figma_url}>${figma_url}</p>`;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const figmaUrl = block.getProperty('embedLink')?.value;
|
||||
return `<p><a href="${figmaUrl}">${figmaUrl}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,22 @@ import {
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { FileView } from './FileView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FileBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.file;
|
||||
View = FileView;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const fileProperty = block.getProperty('file');
|
||||
const fileId = fileProperty?.value;
|
||||
const fileInfo = fileId
|
||||
? await services.api.file.get(fileId, block.workspace)
|
||||
: null;
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const file_property =
|
||||
block.getProperty('file') || ({} as FileColumnValue);
|
||||
const file_id = file_property.value;
|
||||
let file_info = null;
|
||||
if (file_id) {
|
||||
file_info = await services.api.file.get(file_id, block.workspace);
|
||||
}
|
||||
return `<p><a src=${file_info?.url}>${file_property?.name}</p>`;
|
||||
return fileInfo
|
||||
? `<p><a href=${fileInfo?.url}>${fileProperty?.name}</a></p>`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import { ChildrenView, CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const GridItemRender = function (
|
||||
@@ -6,13 +6,7 @@ export const GridItemRender = function (
|
||||
) {
|
||||
const GridItem = function (props: CreateView) {
|
||||
const { block } = props;
|
||||
const children = (
|
||||
<>
|
||||
{block.childrenIds.map(id => {
|
||||
return <RenderBlock key={id} blockId={id} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
const children = <RenderBlockChildren block={block} indent={false} />;
|
||||
return <>{creator({ ...props, children })}</>;
|
||||
};
|
||||
return GridItem;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
import { BlockRender } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
GRID_ITEM_CLASS_NAME,
|
||||
GRID_ITEM_CONTENT_CLASS_NAME,
|
||||
} from '../grid-item/GridItem';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
|
||||
const DB_UPDATE_DELAY = 50;
|
||||
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
|
||||
@@ -226,7 +226,7 @@ export const Grid = function (props: CreateView) {
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<BlockRender hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { GroupView } from './GroupView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class Group extends BaseView {
|
||||
public override selectable = true;
|
||||
@@ -25,13 +29,12 @@ export class Group extends BaseView {
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
return block;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const content = await generateHtml(children);
|
||||
return `<div>${content}<div>`;
|
||||
override async block2html({ editor, selectInfo, block }: Block2HtmlProps) {
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `<div>${childrenHtml}<code/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,6 @@ const SceneMap: Record<RecastScene, ComponentType<CreateView>> = {
|
||||
kanban: SceneKanban,
|
||||
} as const;
|
||||
|
||||
const GroupBox = styled('div')(({ theme }) => {
|
||||
return {
|
||||
'&:hover': {
|
||||
// Workaround referring to other components
|
||||
// See https://emotion.sh/docs/styled#targeting-another-emotion-component
|
||||
// [GroupActionWrapper.toString()]: {},
|
||||
'& > *': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
@@ -59,6 +46,14 @@ const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const GroupBox = styled('div')({
|
||||
'&:hover': {
|
||||
[GroupActionWrapper.toString()]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
({ isSelect, theme }) => ({
|
||||
background: theme.affine.palette.white,
|
||||
|
||||
@@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const ScenePage = ({ block }: CreateView) => {
|
||||
return <RenderBlockChildren block={block} />;
|
||||
return <RenderBlockChildren block={block} indent={false} />;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const AddViewMenu = () => {
|
||||
onClick={() => setActivePanel(!activePanel)}
|
||||
>
|
||||
<AddViewIcon fontSize="small" />
|
||||
<span>Add View</span>
|
||||
<span style={{ userSelect: 'none' }}>Add View</span>
|
||||
{activePanel && (
|
||||
<Panel>
|
||||
<PanelItem>
|
||||
@@ -66,10 +66,10 @@ export const AddViewMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ViewsMenu = () => {
|
||||
useRecastView();
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setViewName(e.target.value.trim());
|
||||
setViewName(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -36,7 +36,7 @@ export const ViewsMenu = () => {
|
||||
}
|
||||
await updateView({
|
||||
...activeView,
|
||||
name: viewName,
|
||||
name: viewName.trim(),
|
||||
type: viewType,
|
||||
});
|
||||
setActiveView(null);
|
||||
@@ -99,10 +99,10 @@ export const ViewsMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { RecastScene } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
KanBanIcon,
|
||||
TableIcon,
|
||||
TodoListIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { KanBanIcon, TodoListIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const VIEW_LIST = [
|
||||
{
|
||||
@@ -16,9 +12,9 @@ export const VIEW_LIST = [
|
||||
scene: RecastScene.Kanban,
|
||||
icon: <KanBanIcon fontSize="small" />,
|
||||
},
|
||||
{
|
||||
name: 'Table',
|
||||
scene: RecastScene.Table,
|
||||
icon: <TableIcon fontSize="small" />,
|
||||
},
|
||||
// {
|
||||
// name: 'Table',
|
||||
// scene: RecastScene.Table,
|
||||
// icon: <TableIcon fontSize="small" />,
|
||||
// },
|
||||
] as const;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
import type {
|
||||
KanbanCard,
|
||||
KanbanGroup,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
|
||||
const AddCardWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
@@ -48,7 +48,7 @@ export const CardContext = (props: Props) => {
|
||||
item={item}
|
||||
active={activeId === id}
|
||||
>
|
||||
<CardItem id={id} block={block} />
|
||||
<CardItem block={block} />
|
||||
</CardItemPanelWrapper>
|
||||
</StyledCardContainer>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
KanbanBlockRender,
|
||||
KanbanCard,
|
||||
RenderBlock,
|
||||
useEditor,
|
||||
useKanban,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
MuiClickAwayListener,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { useRefPage } from './RefPage';
|
||||
|
||||
@@ -82,41 +81,37 @@ const Overlay = styled('div')({
|
||||
},
|
||||
});
|
||||
|
||||
export const CardItem = ({
|
||||
id,
|
||||
block,
|
||||
}: {
|
||||
id: KanbanCard['id'];
|
||||
block: KanbanCard['block'];
|
||||
}) => {
|
||||
export const CardItem = ({ block }: { block: KanbanCard['block'] }) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const [editable, setEditable] = useState(false);
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const [editableBlock, setEditableBlock] = useState<string | null>(null);
|
||||
const { editor } = useEditor();
|
||||
|
||||
const onAddItem = async () => {
|
||||
setEditable(true);
|
||||
await addSubItem(block);
|
||||
const newItem = await addSubItem(block);
|
||||
setEditableBlock(newItem.id);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
openSubPage(id);
|
||||
openSubPage(block.id);
|
||||
};
|
||||
|
||||
const onClickPen = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
setEditable(true);
|
||||
setEditableBlock(block.id);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
|
||||
<MuiClickAwayListener onClickAway={() => setEditableBlock(null)}>
|
||||
<CardContainer>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
<KanbanBlockRender
|
||||
blockId={block.id}
|
||||
activeBlock={editableBlock}
|
||||
/>
|
||||
</CardContent>
|
||||
{showKanbanRefPageFlag && !editable && (
|
||||
{!editableBlock && (
|
||||
<Overlay onClick={onClickCard}>
|
||||
<IconButton backgroundColor="#fff" onClick={onClickPen}>
|
||||
<PenIcon />
|
||||
|
||||
@@ -21,13 +21,17 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
onMouseDown={(e: { stopPropagation: () => void }) => {
|
||||
// Prevent trigger the bottom editor's selection
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={(e: { stopPropagation: () => void }) => {
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
import { CardItem } from '../../CardItem';
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import type { DndableItems } from '../type';
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
|
||||
export function renderContainerDragOverlay({
|
||||
containerId,
|
||||
@@ -18,7 +17,7 @@ export function renderContainerDragOverlay({
|
||||
return (
|
||||
<CardItemWrapper
|
||||
key={id}
|
||||
card={<CardItem key={id} id={id} block={block} />}
|
||||
card={<CardItem key={id} block={block} />}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,35 +5,13 @@ import {
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GroupDividerView } from './groupDividerView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class GroupDividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.groupDivider;
|
||||
View = GroupDividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +165,10 @@ export const ImageView = ({ block, editor }: ImageView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<ImageBlock>
|
||||
<div style={{ position: 'relative' }} ref={resizeBox}>
|
||||
{!isSelect ? (
|
||||
<ImageShade onClick={handleClick}></ImageShade>
|
||||
) : null}
|
||||
{!isSelect ? <ImageShade onClick={handleClick} /> : null}
|
||||
{imgUrl ? (
|
||||
<div
|
||||
onMouseDown={e => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
SelectBlock,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { ImageView } from './ImageView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
|
||||
export class ImageBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
@@ -13,42 +15,40 @@ export class ImageBlock extends BaseView {
|
||||
View = ImageView;
|
||||
|
||||
// TODO: needs to download the image and then upload it to get a new link and then assign it
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'IMG') {
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'IMG') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
value: '',
|
||||
url: el.getAttribute('src'),
|
||||
name: el.getAttribute('src'),
|
||||
size: 0,
|
||||
type: 'link',
|
||||
image: {
|
||||
value: getRandomString('image'),
|
||||
url: element.getAttribute('src'),
|
||||
name: element.getAttribute('src'),
|
||||
size: 0,
|
||||
type: 'link',
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const text = block.getProperty('text');
|
||||
override async block2html({ block, editor }: Block2HtmlProps) {
|
||||
const textValue = block.getProperty('text');
|
||||
const content = '';
|
||||
if (text) {
|
||||
text.value.map(text => `<span>${text}</span>`).join('');
|
||||
}
|
||||
// TODO: child
|
||||
return `<p><img src=${content}></p>`;
|
||||
// TODO: text.value should export with style??
|
||||
const figcaption = (textValue?.value ?? [])
|
||||
.map(({ text }) => `<span>${text}</span>`)
|
||||
.join('');
|
||||
return `<figure><img src="${content}" alt="${figcaption}"/><figcaption>${figcaption}<figcaption/></figure>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -11,18 +11,17 @@ import {
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import type { Numbered, NumberedAsyncBlock } from './types';
|
||||
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
|
||||
export const defaultTodoProps: Numbered = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -184,7 +183,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<div className={'checkBoxContainer'}>
|
||||
{getNumber(properties.numberType, number)}.
|
||||
@@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
|
||||
export class NumberedBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.numbered;
|
||||
@@ -29,71 +28,39 @@ export class NumberedBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'OL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'OL') {
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ol><li>${content}</li></ol>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ol><li>${await commonBlock2HtmlContent(props)}</li></ol>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useRef, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { BackLink, TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
RenderBlockChildren,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { ContentColumnValue } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { Theme, styled } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
TextManage,
|
||||
@@ -81,7 +81,7 @@ export const PageView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<PageTitleBlock>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextManage
|
||||
alwaysShowPlaceholder
|
||||
ref={textRef}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { withRecastBlock } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
ChildrenView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
|
||||
|
||||
import { PageView } from './PageView';
|
||||
|
||||
export const PageChildrenView: (prop: ChildrenView) => JSX.Element = props =>
|
||||
props.children;
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class PageBlock extends BaseView {
|
||||
type = Protocol.Block.Type.page;
|
||||
@@ -35,21 +23,17 @@ export class PageBlock extends BaseView {
|
||||
return this.get_decoration<any>(content, 'text')?.value?.[0].text;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const content = getTextHtml(block);
|
||||
const childrenContent = await generateHtml(children);
|
||||
return `<h1>${content}</h1> ${childrenContent}`;
|
||||
override async block2html({ block, editor, selectInfo }: Block2HtmlProps) {
|
||||
const header =
|
||||
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
|
||||
block,
|
||||
selectInfo
|
||||
);
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `<h1>${header}</h1>${childrenHtml}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
CreateView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
import { TextView } from './TextView';
|
||||
|
||||
@@ -29,54 +30,25 @@ export class QuoteBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'BLOCKQUOTE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'BLOCKQUOTE') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<blockquote>${content}</blockquote>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<blockquote>${await commonBlock2HtmlContent(
|
||||
props
|
||||
)}</blockquote>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,69 +68,22 @@ export class CalloutBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'ASIDE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (
|
||||
tag_name === 'ASIDE' ||
|
||||
el.firstChild?.nodeValue?.startsWith('<aside>')
|
||||
) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
texts.length > 0 &&
|
||||
(texts[0].text || '').startsWith('<aside>')
|
||||
) {
|
||||
texts[0].text = texts[0].text.substring('<aside>'.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (el.firstChild?.nodeValue?.startsWith('</aside>')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<aside>${content}</aside>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<aside>${await commonBlock2HtmlContent(props)}</aside>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ import {
|
||||
BaseView,
|
||||
CreateView,
|
||||
AsyncBlock,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
HTML2BlockResult,
|
||||
BlockEditor,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { TextView } from './TextView';
|
||||
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class TextBlock extends BaseView {
|
||||
type = Protocol.Block.Type.text;
|
||||
@@ -28,106 +27,30 @@ export class TextBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
if (el instanceof Text) {
|
||||
// TODO: parsing style
|
||||
return el.textContent.split('\n').map(text => {
|
||||
return {
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: [{ text: text }] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const tag_name = el.tagName;
|
||||
const block_style: any = {};
|
||||
switch (tag_name) {
|
||||
case 'STRONG':
|
||||
case 'B':
|
||||
block_style.bold = true;
|
||||
break;
|
||||
case 'A':
|
||||
block_style.type = 'link';
|
||||
block_style.url = el.getAttribute('href');
|
||||
block_style.id = getRandomString('link');
|
||||
block_style.children = [];
|
||||
break;
|
||||
case 'EM':
|
||||
block_style.italic = true;
|
||||
break;
|
||||
case 'U':
|
||||
block_style.underline = true;
|
||||
break;
|
||||
case 'CODE':
|
||||
block_style.inlinecode = true;
|
||||
break;
|
||||
case 'S':
|
||||
case 'DEL':
|
||||
block_style.strikethrough = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const child_nodes = el.childNodes;
|
||||
let texts = [];
|
||||
if (Object.keys(block_style).length > 0) {
|
||||
for (let i = 0; i < child_nodes.length; i++) {
|
||||
const blocks_info: any[] = parseEl(child_nodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === this.type) {
|
||||
const block_texts = block.properties.text.value.map(
|
||||
(text_value: any) => {
|
||||
return tag_name === 'A'
|
||||
? { ...text_value }
|
||||
: { ...block_style, ...text_value };
|
||||
}
|
||||
);
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tag_name === 'A') {
|
||||
block_style.children.push(...texts);
|
||||
texts = [block_style];
|
||||
}
|
||||
return texts.length > 0
|
||||
? [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<p>${content}</p>`;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: [
|
||||
'DIV',
|
||||
'P',
|
||||
'PRE',
|
||||
'B',
|
||||
'A',
|
||||
'EM',
|
||||
'U',
|
||||
'CODE',
|
||||
'S',
|
||||
'DEL',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,55 +67,23 @@ export class Heading1Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H1',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H1') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h1>${content}</h1>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h1>${await commonBlock2HtmlContent(props)}</h1>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,55 +100,23 @@ export class Heading2Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H2',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H2') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h2>${content}</h2>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h2>${await commonBlock2HtmlContent(props)}</h2>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,53 +134,22 @@ export class Heading3Block extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H3',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H3') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h3>${content}</h3>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h3>${await commonBlock2HtmlContent(props)}</h3>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { TextManage } from '../../components/text-manage';
|
||||
import { dedentBlock, tabBlock } from '../../utils/indent';
|
||||
interface CreateTextView extends CreateView {
|
||||
@@ -243,7 +242,7 @@ export const TextView = ({
|
||||
selected={isSelect}
|
||||
className={containerClassName}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextBlock
|
||||
block={block}
|
||||
type={block.type}
|
||||
@@ -255,9 +254,7 @@ export const TextView = ({
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
@@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
export const TodoView = ({ block, editor }: CreateView) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
@@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={
|
||||
properties.checked?.value ? 'checked' : ''
|
||||
}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
</BlockPendantProvider>
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
AsyncBlock,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
withTreeViewChildren,
|
||||
} from '@toeverything/framework/virgo';
|
||||
// import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import { defaultTodoProps, TodoView } from './TodoView';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
@@ -29,71 +28,44 @@ export class TodoBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: TodoAsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
if (!firstList || !firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>[ ] ${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>[ ] ${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { YoutubeView } from './YoutubeView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class YoutubeBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
@@ -12,42 +16,11 @@ export class YoutubeBlock extends BaseView {
|
||||
type = Protocol.Block.Type.youtube;
|
||||
View = YoutubeView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
const href = el.getAttribute('href');
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(href).host;
|
||||
|
||||
if (allowedHosts.includes(host)) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: is not sure what value to fill in name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
override async block2Text(block: AsyncBlock, selectInfo: SelectBlock) {
|
||||
return block.getProperty('embedLink')?.value ?? '';
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${url}>${url}</p>`;
|
||||
return `<p><a href="${url}">${url}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const BlockContainer = function ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Container = styled('div')<{ selected: boolean }>(
|
||||
export const Container = styled('div')<{ selected?: boolean }>(
|
||||
({ selected, theme }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ChildrenView } from '@toeverything/framework/virgo';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
export const IndentWrapper = (props: PropsWithChildren) => {
|
||||
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
|
||||
};
|
||||
|
||||
const StyledIdentWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
marginLeft: '30px',
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './IndentWrapper';
|
||||
@@ -1,4 +1,5 @@
|
||||
export const isYoutubeUrl = (url?: string): boolean => {
|
||||
if (!url) return false;
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(url).host;
|
||||
return allowedHosts.includes(host);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.v-basic-table-body {
|
||||
overflow: hidden !important;
|
||||
&:hover {
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import {
|
||||
useMemo,
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { VariableSizeGrid, areEqual } from 'react-window';
|
||||
import type {
|
||||
GridChildComponentProps,
|
||||
GridItemKeySelector,
|
||||
} from 'react-window';
|
||||
import { VariableSizeGrid } from 'react-window';
|
||||
import style9 from 'style9';
|
||||
import './basic-table.scss';
|
||||
|
||||
export interface TableColumn {
|
||||
dataKey: string;
|
||||
label: string;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
SelectBlock,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
|
||||
export type Block2HtmlProps = {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
// The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range
|
||||
selectInfo?: SelectBlock;
|
||||
};
|
||||
|
||||
export const commonBlock2HtmlContent = async ({
|
||||
editor,
|
||||
block,
|
||||
selectInfo,
|
||||
}: Block2HtmlProps) => {
|
||||
const html =
|
||||
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
|
||||
block,
|
||||
selectInfo
|
||||
);
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `${html}${childrenHtml}`;
|
||||
};
|
||||
|
||||
export const commonHTML2block = ({
|
||||
element,
|
||||
editor,
|
||||
tagName,
|
||||
type,
|
||||
ignoreEmptyElement = true,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
tagName: string | string[];
|
||||
type: BlockFlavorKeys;
|
||||
ignoreEmptyElement?: boolean;
|
||||
}): HTML2BlockResult => {
|
||||
const tagNames = typeof tagName === 'string' ? [tagName] : tagName;
|
||||
if (tagNames.includes(element.tagName)) {
|
||||
const res = editor.clipboard.clipboardUtils.commonHTML2Block(
|
||||
element,
|
||||
type,
|
||||
ignoreEmptyElement
|
||||
);
|
||||
return res ? [res] : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -4,12 +4,14 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns": "^2.29.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"hotkeys-js": "^3.9.4",
|
||||
"html-escaper": "^3.0.3",
|
||||
"lru-cache": "^7.10.1",
|
||||
"marked": "^4.0.19",
|
||||
"nanoid": "^4.0.0",
|
||||
"slate": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from './editor';
|
||||
|
||||
const RootContext = createContext<{
|
||||
type EditorProps = {
|
||||
editor: BlockEditor;
|
||||
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
|
||||
editorElement: () => JSX.Element;
|
||||
}>(
|
||||
};
|
||||
|
||||
const EditorContext = createContext<EditorProps>(
|
||||
genErrorObj(
|
||||
'Failed to get context! The context only can use under the "render-root"'
|
||||
'Failed to get EditorContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
return useContext(EditorContext);
|
||||
};
|
||||
|
||||
export const EditorProvider = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<EditorProps>) => {
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, editorElement }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import type { BlockEditor } from './editor';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { BlockRenderProvider, RenderBlock } from './render-block';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -24,11 +24,7 @@ interface RenderRootProps {
|
||||
const MAX_PAGE_WIDTH = 5000;
|
||||
export const MIN_PAGE_WIDTH = 1480;
|
||||
|
||||
export const RenderRoot = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<RenderRootProps>) => {
|
||||
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
|
||||
const selectionRef = useRef<SelectionRef>(null);
|
||||
const triggeredBySelect = useRef(false);
|
||||
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
|
||||
@@ -158,39 +154,43 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
{children}
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
<EditorProvider editor={editor} editorElement={editorElement}>
|
||||
<BlockRenderProvider blockRender={RenderBlock}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && (
|
||||
<SelectionRect ref={selectionRef} editor={editor} />
|
||||
)}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</BlockRenderProvider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
@@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBlankContainter
|
||||
<ScrollBlankContainer
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onClick}
|
||||
@@ -283,7 +283,7 @@ const Content = styled('div')({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
});
|
||||
|
||||
const ScrollBlankContainter = styled('div')({
|
||||
const ScrollBlankContainer = styled('div')({
|
||||
paddingBottom: '30vh',
|
||||
margin: `0 -${PADDING_X}px`,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { containerFlavor } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
type MouseEvent,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from '../editor';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
import { PendantPopover } from './pendant-popover';
|
||||
import { PendantRender } from './pendant-render';
|
||||
import { useRef } from 'react';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
interface BlockTagProps {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
}
|
||||
|
||||
export const BlockPendantProvider = ({
|
||||
editor,
|
||||
block,
|
||||
children,
|
||||
}: PropsWithChildren<BlockTagProps>) => {
|
||||
@@ -23,8 +30,23 @@ export const BlockPendantProvider = ({
|
||||
const showTriggerLine =
|
||||
properties.filter(property => getValue(property.id)).length === 0;
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (containerFlavor.includes(block.type)) {
|
||||
return;
|
||||
}
|
||||
if (e.target === e.currentTarget) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const middle = (rect.left + rect.right) / 2;
|
||||
const position = e.clientX < middle ? 'start' : 'end';
|
||||
editor.selectionManager.activeNodeByNodeId(block.id, position);
|
||||
}
|
||||
},
|
||||
[editor, block]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container onClick={onClick}>
|
||||
{children}
|
||||
|
||||
{showTriggerLine ? (
|
||||
|
||||
@@ -180,9 +180,49 @@ export class AsyncBlock {
|
||||
return this.event_emitter.emit(eventName, eventData);
|
||||
}
|
||||
|
||||
onUpdate(callback: (event: EventData) => void) {
|
||||
this.on('update', callback);
|
||||
/**
|
||||
* @param deep observe deep
|
||||
*
|
||||
* NOTICE: the observe of children is async,
|
||||
* so there maybe have some delay before observe done.
|
||||
*/
|
||||
onUpdate(callback: (event: EventData) => void, deep = 0) {
|
||||
let expired = false;
|
||||
const unobserveMap: Record<string, () => void> = {};
|
||||
const observeChildren = () => {
|
||||
if (deep <= 0) {
|
||||
return;
|
||||
}
|
||||
this.children().then(children => {
|
||||
// Check current event listeners is not expired
|
||||
if (expired) {
|
||||
return;
|
||||
}
|
||||
children.forEach(child => {
|
||||
if (unobserveMap[child.id]) return;
|
||||
const unobserve = child.onUpdate(callback, deep - 1);
|
||||
unobserveMap[child.id] = unobserve;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const unobserveChildren = () => {
|
||||
Object.values(unobserveMap).forEach(unobserve => {
|
||||
unobserve();
|
||||
});
|
||||
};
|
||||
|
||||
this.on('update', e => {
|
||||
callback(e);
|
||||
// Update children observe
|
||||
observeChildren();
|
||||
});
|
||||
|
||||
observeChildren();
|
||||
|
||||
return () => {
|
||||
expired = true;
|
||||
unobserveChildren();
|
||||
this.off('update', callback);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
Point,
|
||||
Selection as SlateSelection,
|
||||
} from 'slate';
|
||||
import { AsyncBlock } from '../block';
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock } from '../selection';
|
||||
|
||||
type TextUtilsFunctions =
|
||||
| 'getString'
|
||||
@@ -43,7 +45,9 @@ type TextUtilsFunctions =
|
||||
| 'setSelection'
|
||||
| 'insertNodes'
|
||||
| 'getNodeByPath'
|
||||
| 'wrapLink';
|
||||
| 'wrapLink'
|
||||
| 'getNodeByRange'
|
||||
| 'convertLeaf2Html';
|
||||
|
||||
type ExtendedTextUtils = SlateUtils & {
|
||||
setLinkModalVisible: (visible: boolean) => void;
|
||||
@@ -104,15 +108,116 @@ export class BlockHelper {
|
||||
return '';
|
||||
}
|
||||
|
||||
public getBlockTextBetweenSelection(blockId: string) {
|
||||
public async isBlockEditable(blockOrBlockId: AsyncBlock | string) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const blockView = this._editor.getView(block.type);
|
||||
|
||||
return blockView.editable;
|
||||
}
|
||||
|
||||
public async getFlatBlocksUnderParent(
|
||||
parentBlockId: string,
|
||||
includeParent = false
|
||||
): Promise<AsyncBlock[]> {
|
||||
const blocks = [];
|
||||
const block = await this._editor.getBlockById(parentBlockId);
|
||||
if (includeParent) {
|
||||
blocks.push(block);
|
||||
}
|
||||
const children = await block.children();
|
||||
(
|
||||
await Promise.all(
|
||||
children.map(child => {
|
||||
return this.getFlatBlocksUnderParent(child.id, true);
|
||||
})
|
||||
)
|
||||
).forEach(editableChildren => {
|
||||
blocks.push(...editableChildren);
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public getBlockTextBetweenSelection(
|
||||
blockId: string,
|
||||
shouldUsePreviousSelection = true
|
||||
) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
return text_utils.getStringBetweenSelection(true);
|
||||
return text_utils.getStringBetweenSelection(
|
||||
shouldUsePreviousSelection
|
||||
);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
return '';
|
||||
}
|
||||
|
||||
public async getEditableBlockPropertiesBySelectInfo(
|
||||
block: AsyncBlock,
|
||||
selectInfo?: SelectBlock
|
||||
) {
|
||||
const properties = block.getProperties();
|
||||
if (properties.text.value.length === 0) {
|
||||
return properties;
|
||||
}
|
||||
const text_value = properties.text.value;
|
||||
|
||||
const {
|
||||
text: { value: originTextValue, ...otherTextProperties },
|
||||
...otherProperties
|
||||
} = properties;
|
||||
|
||||
// Use deepClone method will throw incomprehensible error
|
||||
let textValue = JSON.parse(JSON.stringify(originTextValue));
|
||||
|
||||
if (selectInfo?.endInfo) {
|
||||
textValue = textValue.slice(0, selectInfo.endInfo.arrayIndex + 1);
|
||||
textValue[textValue.length - 1].text = text_value[
|
||||
textValue.length - 1
|
||||
].text.substring(0, selectInfo.endInfo.offset);
|
||||
}
|
||||
if (selectInfo?.startInfo) {
|
||||
textValue = textValue.slice(selectInfo.startInfo.arrayIndex);
|
||||
textValue[0].text = textValue[0].text.substring(
|
||||
selectInfo.startInfo.offset
|
||||
);
|
||||
}
|
||||
return {
|
||||
...otherProperties,
|
||||
text: {
|
||||
...otherTextProperties,
|
||||
value: textValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For editable blocks, the properties containing the selected text will be returned with the selection information
|
||||
public async getBlockPropertiesBySelectInfo(selectBlockInfo: SelectBlock) {
|
||||
const block = await this._editor.getBlockById(selectBlockInfo.blockId);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
if (blockView.editable) {
|
||||
return this.getEditableBlockPropertiesBySelectInfo(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
} else {
|
||||
return block?.getProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public convertTextValue2Html(blockId: string, textValue: any) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (!text_utils) {
|
||||
return '';
|
||||
}
|
||||
return textValue.reduce((html: string, textValueItem: any) => {
|
||||
const fragment = text_utils.convertLeaf2Html(textValueItem);
|
||||
return `${html}${fragment}`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
public setBlockBlur(blockId: string) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { HooksRunner } from '../types';
|
||||
import { Editor } from '../editor';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
import { Paste } from './paste';
|
||||
// todo needs to be a switch
|
||||
|
||||
enum ClipboardAction {
|
||||
COPY = 'copy',
|
||||
CUT = 'cut',
|
||||
PASTE = 'paste',
|
||||
}
|
||||
|
||||
//TODO: need to consider the cursor position after inserting the children
|
||||
class BrowserClipboard {
|
||||
private _eventTarget: Element;
|
||||
private _hooks: HooksRunner;
|
||||
private _editor: Editor;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
private _markdownParse: MarkdownParser;
|
||||
private _paste: Paste;
|
||||
|
||||
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||
this._eventTarget = eventTarget;
|
||||
this._hooks = hooks;
|
||||
this._editor = editor;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
this._markdownParse = new MarkdownParser();
|
||||
this._paste = new Paste(
|
||||
editor,
|
||||
this._clipboardParse,
|
||||
this._markdownParse
|
||||
);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
public getClipboardParse() {
|
||||
return this._clipboardParse;
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._handleCopy = this._handleCopy.bind(this);
|
||||
this._handleCut = this._handleCut.bind(this);
|
||||
|
||||
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCopy(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _handleCut(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||
switch (action) {
|
||||
case ClipboardAction.COPY:
|
||||
this._hooks.beforeCopy(e);
|
||||
break;
|
||||
|
||||
case ClipboardAction.CUT:
|
||||
this._hooks.beforeCut(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _dispatchClipboardEvent(
|
||||
action: ClipboardAction,
|
||||
e: ClipboardEvent
|
||||
) {
|
||||
this._preCopyCut(action, e);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._clipboardParse.dispose();
|
||||
this._clipboardParse = null;
|
||||
this._eventTarget = null;
|
||||
this._hooks = null;
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { BrowserClipboard };
|
||||
@@ -1,207 +0,0 @@
|
||||
import { Protocol, BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
import { escape } from '@toeverything/utils';
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock } from '../selection';
|
||||
import { ClipBlockInfo } from './types';
|
||||
|
||||
class DefaultBlockParse {
|
||||
public static html2block(el: Element): ClipBlockInfo[] | undefined | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'DIV' || el instanceof Text) {
|
||||
return el.textContent?.split('\n').map(str => {
|
||||
const data = {
|
||||
text: escape(str),
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [data] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ClipboardParse {
|
||||
private editor: Editor;
|
||||
private static block_types: BlockFlavorKeys[] = [
|
||||
Protocol.Block.Type.page,
|
||||
Protocol.Block.Type.reference,
|
||||
Protocol.Block.Type.heading1,
|
||||
Protocol.Block.Type.heading2,
|
||||
Protocol.Block.Type.heading3,
|
||||
Protocol.Block.Type.quote,
|
||||
Protocol.Block.Type.todo,
|
||||
Protocol.Block.Type.code,
|
||||
Protocol.Block.Type.text,
|
||||
Protocol.Block.Type.toc,
|
||||
Protocol.Block.Type.file,
|
||||
Protocol.Block.Type.image,
|
||||
Protocol.Block.Type.divider,
|
||||
Protocol.Block.Type.callout,
|
||||
Protocol.Block.Type.youtube,
|
||||
Protocol.Block.Type.figma,
|
||||
Protocol.Block.Type.group,
|
||||
Protocol.Block.Type.embedLink,
|
||||
Protocol.Block.Type.numbered,
|
||||
Protocol.Block.Type.bullet,
|
||||
];
|
||||
private static break_flags: Set<string> = new Set([
|
||||
'BLOCKQUOTE',
|
||||
'BODY',
|
||||
'CENTER',
|
||||
'DD',
|
||||
'DIR',
|
||||
'DIV',
|
||||
'DL',
|
||||
'DT',
|
||||
'FORM',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'HEAD',
|
||||
'HTML',
|
||||
'ISINDEX',
|
||||
'MENU',
|
||||
'NOFRAMES',
|
||||
'P',
|
||||
'PRE',
|
||||
'TABLE',
|
||||
'TD',
|
||||
'TH',
|
||||
'TITLE',
|
||||
'TR',
|
||||
]);
|
||||
|
||||
constructor(editor: Editor) {
|
||||
this.editor = editor;
|
||||
this.generate_html = this.generate_html.bind(this);
|
||||
this.parse_dom = this.parse_dom.bind(this);
|
||||
}
|
||||
// TODO: escape
|
||||
public text2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return (clipData || '').split('\n').map((str: string) => {
|
||||
const block_info: ClipBlockInfo = {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [{ text: str }] },
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
return block_info;
|
||||
});
|
||||
}
|
||||
|
||||
public html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return this.common_html2blocks(clipData);
|
||||
}
|
||||
|
||||
private common_html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
const html_el = document.createElement('html');
|
||||
html_el.innerHTML = clipData;
|
||||
return this.parse_dom(html_el);
|
||||
}
|
||||
|
||||
// tTODO:odo escape
|
||||
private parse_dom(el: Element): ClipBlockInfo[] {
|
||||
for (let i = 0; i < ClipboardParse.block_types.length; i++) {
|
||||
const block_utils = this.editor.getView(
|
||||
ClipboardParse.block_types[i]
|
||||
);
|
||||
const blocks = block_utils?.html2block?.(el, this.parse_dom);
|
||||
|
||||
if (blocks) {
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
const blocks: ClipBlockInfo[] = [];
|
||||
// blocks = DefaultBlockParse.html2block(el);
|
||||
for (let i = 0; i < el.childNodes.length; i++) {
|
||||
const child = el.childNodes[i];
|
||||
const last_block_info =
|
||||
blocks.length === 0 ? null : blocks[blocks.length - 1];
|
||||
let blocks_info = this.parse_dom(child as Element);
|
||||
if (
|
||||
last_block_info &&
|
||||
last_block_info.type === 'text' &&
|
||||
!ClipboardParse.break_flags.has((child as Element).tagName)
|
||||
) {
|
||||
const texts = last_block_info.properties?.text?.value || [];
|
||||
let j = 0;
|
||||
for (; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === 'text') {
|
||||
const block_texts = block.properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
last_block_info.properties = {
|
||||
text: { value: texts },
|
||||
};
|
||||
blocks_info = blocks_info.slice(j);
|
||||
}
|
||||
blocks.push(...blocks_info);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public async generateHtml(): Promise<string> {
|
||||
const select_info = await this.editor.selectionManager.getSelectInfo();
|
||||
return await this.generate_html(select_info.blocks);
|
||||
}
|
||||
|
||||
public async page2html(): Promise<string> {
|
||||
const root_block_id = this.editor.getRootBlockId();
|
||||
if (!root_block_id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const block_info = await this.get_select_info(root_block_id);
|
||||
return await this.generate_html([block_info]);
|
||||
}
|
||||
|
||||
private async get_select_info(blockId: string) {
|
||||
const block = await this.editor.getBlockById(blockId);
|
||||
if (!block) return null;
|
||||
const block_info: SelectBlock = {
|
||||
blockId: block.id,
|
||||
children: [],
|
||||
};
|
||||
const children_ids = block.childrenIds;
|
||||
for (let i = 0; i < children_ids.length; i++) {
|
||||
block_info.children.push(
|
||||
await this.get_select_info(children_ids[i])
|
||||
);
|
||||
}
|
||||
return block_info;
|
||||
}
|
||||
|
||||
private async generate_html(selectBlocks: SelectBlock[]): Promise<string> {
|
||||
let result = '';
|
||||
for (let i = 0; i < selectBlocks.length; i++) {
|
||||
const sel_block = selectBlocks[i];
|
||||
if (!sel_block || !sel_block.blockId) continue;
|
||||
const block = await this.editor.getBlockById(sel_block.blockId);
|
||||
if (!block) continue;
|
||||
const block_utils = this.editor.getView(block.type);
|
||||
const html = await block_utils.block2html(
|
||||
block,
|
||||
sel_block.children,
|
||||
this.generate_html
|
||||
);
|
||||
result += html;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Editor } from '../editor';
|
||||
import { SelectionManager } from '../selection';
|
||||
import { HookType, PluginHooks } from '../types';
|
||||
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,
|
||||
selectionManager: SelectionManager
|
||||
) {
|
||||
this._editor = editor;
|
||||
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._copy.handleCopy)
|
||||
);
|
||||
this._sub.add(
|
||||
hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy)
|
||||
);
|
||||
}
|
||||
|
||||
disposeInternal() {
|
||||
this._sub.unsubscribe();
|
||||
this._hooks = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { ClipboardPopulator };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ClipboardEventDispatcher } from './clipboardEventDispatcher';
|
||||
import { HookType } from '../types';
|
||||
import { Editor } from '../editor';
|
||||
import { Copy } from './copy';
|
||||
import { Paste } from './paste';
|
||||
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
|
||||
export class Clipboard {
|
||||
private _clipboardEventDispatcher: ClipboardEventDispatcher;
|
||||
private _copy: Copy;
|
||||
private _paste: Paste;
|
||||
public clipboardUtils: ClipboardUtils;
|
||||
private _clipboardTarget: HTMLElement;
|
||||
|
||||
constructor(editor: Editor, clipboardTarget: HTMLElement) {
|
||||
this.clipboardUtils = new ClipboardUtils(editor);
|
||||
this._clipboardTarget = clipboardTarget;
|
||||
this._copy = new Copy(editor);
|
||||
|
||||
this._paste = new Paste(editor);
|
||||
|
||||
this._clipboardEventDispatcher = new ClipboardEventDispatcher(
|
||||
editor,
|
||||
this._clipboardTarget
|
||||
);
|
||||
|
||||
editor
|
||||
.getHooks()
|
||||
.get(HookType.ON_COPY)
|
||||
.subscribe(this._copy.handleCopy);
|
||||
|
||||
editor.getHooks().get(HookType.ON_CUT).subscribe(this._copy.handleCopy);
|
||||
|
||||
editor
|
||||
.getHooks()
|
||||
.get(HookType.ON_PASTE)
|
||||
.subscribe(this._paste.handlePaste);
|
||||
}
|
||||
|
||||
set clipboardTarget(clipboardTarget: HTMLElement) {
|
||||
this._clipboardTarget = clipboardTarget;
|
||||
this._clipboardEventDispatcher.initialClipboardTargetEvent(
|
||||
this._clipboardTarget
|
||||
);
|
||||
}
|
||||
get clipboardTarget() {
|
||||
return this._clipboardTarget;
|
||||
}
|
||||
|
||||
public clipboardEvent2Blocks(e: ClipboardEvent) {
|
||||
return this._paste.clipboardEvent2Blocks(e);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this._clipboardEventDispatcher.dispose(this.clipboardTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Editor } from '../editor';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
|
||||
enum ClipboardAction {
|
||||
copy = 'copy',
|
||||
cut = 'cut',
|
||||
paste = 'paste',
|
||||
}
|
||||
|
||||
export class ClipboardEventDispatcher {
|
||||
private _editor: Editor;
|
||||
private _utils: ClipboardUtils;
|
||||
|
||||
constructor(editor: Editor, clipboardTarget: HTMLElement) {
|
||||
this._editor = editor;
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
|
||||
this._copyHandler = this._copyHandler.bind(this);
|
||||
this._cutHandler = this._cutHandler.bind(this);
|
||||
this._pasteHandler = this._pasteHandler.bind(this);
|
||||
|
||||
this.initialDocumentEvent();
|
||||
if (clipboardTarget) {
|
||||
this.initialClipboardTargetEvent(clipboardTarget);
|
||||
}
|
||||
}
|
||||
initialDocumentEvent() {
|
||||
this.disposeDocumentEvent();
|
||||
|
||||
document.addEventListener(ClipboardAction.copy, this._copyHandler);
|
||||
document.addEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
document.addEventListener(ClipboardAction.paste, this._pasteHandler);
|
||||
}
|
||||
initialClipboardTargetEvent(clipboardTarget: HTMLElement) {
|
||||
if (!clipboardTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeClipboardTargetEvent(clipboardTarget);
|
||||
|
||||
clipboardTarget.addEventListener(
|
||||
ClipboardAction.copy,
|
||||
this._copyHandler
|
||||
);
|
||||
clipboardTarget.addEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
clipboardTarget.addEventListener(
|
||||
ClipboardAction.paste,
|
||||
this._pasteHandler
|
||||
);
|
||||
}
|
||||
disposeDocumentEvent() {
|
||||
document.removeEventListener(ClipboardAction.copy, this._copyHandler);
|
||||
document.removeEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
document.removeEventListener(ClipboardAction.paste, this._pasteHandler);
|
||||
}
|
||||
disposeClipboardTargetEvent(clipboardTarget: HTMLElement) {
|
||||
if (!clipboardTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.copy,
|
||||
this._copyHandler
|
||||
);
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.cut,
|
||||
this._cutHandler
|
||||
);
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.paste,
|
||||
this._pasteHandler
|
||||
);
|
||||
}
|
||||
|
||||
private async _copyHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
this._editor.getHooks().onCopy(e);
|
||||
}
|
||||
|
||||
private async _cutHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
this._editor.getHooks().onCut(e);
|
||||
}
|
||||
private async _pasteHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editor.getHooks().onPaste(e);
|
||||
}
|
||||
|
||||
dispose(clipboardTarget: HTMLElement) {
|
||||
this.disposeDocumentEvent();
|
||||
this.disposeClipboardTargetEvent(clipboardTarget);
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock, SelectInfo } from '../selection';
|
||||
import { AsyncBlock } from '../block';
|
||||
import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types';
|
||||
import { Clip } from './clip';
|
||||
import { commonHTML2Block, commonHTML2Text } from './utils';
|
||||
|
||||
export class ClipboardUtils {
|
||||
private _editor: Editor;
|
||||
constructor(editor: Editor) {
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
async shouldHandlerContinue(event: ClipboardEvent) {
|
||||
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||
return false;
|
||||
}
|
||||
const selectInfo = await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
return (
|
||||
selectInfo.blocks.length &&
|
||||
this._editor.selectionManager.currentSelectInfo.type !== 'None'
|
||||
);
|
||||
}
|
||||
|
||||
async getClipInfoOfBlockById(blockId: string) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block.type,
|
||||
properties: block?.getProperties(),
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
const children = (await block?.children()) ?? [];
|
||||
|
||||
await Promise.all(
|
||||
children.map(async childBlock => {
|
||||
const childInfo = await this.getClipInfoOfBlockById(
|
||||
childBlock.id
|
||||
);
|
||||
blockInfo.children.push(childInfo);
|
||||
})
|
||||
);
|
||||
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
async getClipDataOfBlocksById(blockIds: string[]) {
|
||||
const clipInfos = await Promise.all(
|
||||
blockIds.map(blockId => this.getClipInfoOfBlockById(blockId))
|
||||
);
|
||||
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getClipInfoOfBlockBySelectInfo(selectBlockInfo: SelectBlock) {
|
||||
const block = await this._editor.getBlockById(selectBlockInfo.blockId);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block?.type,
|
||||
properties:
|
||||
await this._editor.blockHelper.getBlockPropertiesBySelectInfo(
|
||||
selectBlockInfo
|
||||
),
|
||||
// Editable has no children
|
||||
children: [],
|
||||
};
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
async getClipDataOfBlocksBySelectInfo(selectInfo: SelectInfo) {
|
||||
const clipInfos = await Promise.all(
|
||||
selectInfo.blocks.map(selectBlockInfo =>
|
||||
this.getClipInfoOfBlockBySelectInfo(selectBlockInfo)
|
||||
)
|
||||
);
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async convertTextValue2HtmlBySelectInfo(
|
||||
blockOrBlockId: AsyncBlock | string,
|
||||
selectBlockInfo?: SelectBlock
|
||||
) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const selectedProperties =
|
||||
await this._editor.blockHelper.getEditableBlockPropertiesBySelectInfo(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
return this._editor.blockHelper.convertTextValue2Html(
|
||||
block.id,
|
||||
selectedProperties.text.value
|
||||
);
|
||||
}
|
||||
async convertBlock2HtmlBySelectInfos(
|
||||
blockOrBlockId: AsyncBlock | string,
|
||||
selectBlockInfos?: SelectBlock[]
|
||||
) {
|
||||
if (!selectBlockInfos) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const children = await block?.children();
|
||||
return (
|
||||
await Promise.all(
|
||||
children.map(async childBlock => {
|
||||
const blockView = this._editor.getView(childBlock.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block: childBlock,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
}
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
selectBlockInfos.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block,
|
||||
selectInfo: selectBlockInfo,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
}
|
||||
|
||||
async convertHTMLString2Blocks(html: string): Promise<ClipBlockInfo[]> {
|
||||
const htmlEl = document.createElement('html');
|
||||
htmlEl.innerHTML = html;
|
||||
htmlEl.querySelector('head')?.remove();
|
||||
|
||||
return this.convertHtml2Blocks(htmlEl);
|
||||
}
|
||||
async convertHtml2Blocks(element: Element): Promise<ClipBlockInfo[]> {
|
||||
const editableViews = this._editor.getEditableViews();
|
||||
// 如果block能够捕捉htmlElement则返回block的html2block
|
||||
const [clipBlockInfos] = (
|
||||
await Promise.all(
|
||||
editableViews.map(editableView => {
|
||||
return editableView?.html2block?.({
|
||||
editor: this._editor,
|
||||
element: element,
|
||||
});
|
||||
})
|
||||
)
|
||||
).filter(v => v && v.length);
|
||||
|
||||
if (clipBlockInfos) {
|
||||
return clipBlockInfos;
|
||||
}
|
||||
return (
|
||||
await Promise.all(
|
||||
Array.from(element.children).map(async childElement => {
|
||||
const clipBlockInfos = await this.convertHtml2Blocks(
|
||||
childElement
|
||||
);
|
||||
|
||||
if (clipBlockInfos && clipBlockInfos.length) {
|
||||
return clipBlockInfos;
|
||||
}
|
||||
|
||||
return this.commonHTML2Block(childElement);
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
}
|
||||
|
||||
commonHTML2Block = commonHTML2Block;
|
||||
|
||||
commonHTML2Text = commonHTML2Text;
|
||||
|
||||
textToBlock(clipData = ''): ClipBlockInfo[] {
|
||||
return clipData.split('\n').map((str: string) => {
|
||||
return {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [{ text: str }] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async page2html() {
|
||||
const rootBlockId = this._editor.getRootBlockId();
|
||||
if (!rootBlockId) {
|
||||
return '';
|
||||
}
|
||||
const rootBlock = await this._editor.getBlockById(rootBlockId);
|
||||
const blockView = this._editor.getView(rootBlock.type);
|
||||
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block: rootBlock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,13 @@ 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';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
class Copy {
|
||||
private _editor: Editor;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
|
||||
private _utils: ClipboardUtils;
|
||||
constructor(editor: Editor) {
|
||||
this._editor = editor;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
this.handleCopy = this.handleCopy.bind(this);
|
||||
}
|
||||
public async handleCopy(e: ClipboardEvent) {
|
||||
@@ -22,7 +18,6 @@ class Copy {
|
||||
if (!clips.length) {
|
||||
return;
|
||||
}
|
||||
// TODO: is not compatible with safari
|
||||
const success = this._copyToClipboardFromPc(clips);
|
||||
if (!success) {
|
||||
// This way, not compatible with firefox
|
||||
@@ -49,24 +44,113 @@ class Copy {
|
||||
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));
|
||||
const textClip = await this._getTextClip();
|
||||
clips.push(textClip);
|
||||
|
||||
const htmlClip = await this._getHtmlClip();
|
||||
|
||||
clips.push(htmlClip);
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
private async _getHtmlClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
const htmlStr = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block,
|
||||
selectInfo: selectBlockInfo,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlStr);
|
||||
}
|
||||
|
||||
private async _getAffineClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
return getClipDataOfBlocksById(
|
||||
this._editor,
|
||||
if (selectInfo.type === 'Range') {
|
||||
return this._utils.getClipDataOfBlocksBySelectInfo(selectInfo);
|
||||
}
|
||||
|
||||
// The only remaining case is that selectInfo.type === 'Block'
|
||||
return this._utils.getClipDataOfBlocksById(
|
||||
selectInfo.blocks.map(block => block.blockId)
|
||||
);
|
||||
}
|
||||
|
||||
private async _getTextClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
if (selectInfo.type === 'Range') {
|
||||
const text = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
const block2Text = await blockView.block2Text(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
|
||||
return (
|
||||
block2Text ||
|
||||
this._editor.blockHelper.getBlockTextBetweenSelection(
|
||||
selectBlockInfo.blockId,
|
||||
false
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
).join('\n');
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, text);
|
||||
}
|
||||
|
||||
// The only remaining case is that selectInfo.type === 'Block'
|
||||
const selectedBlocks = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(selectBlockInfo =>
|
||||
this._editor.blockHelper.getFlatBlocksUnderParent(
|
||||
selectBlockInfo.blockId,
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
const blockText = (
|
||||
await Promise.all(
|
||||
selectedBlocks.map(async block => {
|
||||
const blockView = this._editor.getView(block.type);
|
||||
const block2Text = await blockView.block2Text(block);
|
||||
return (
|
||||
block2Text ||
|
||||
this._editor.blockHelper.getBlockText(block.id)
|
||||
);
|
||||
})
|
||||
)
|
||||
).join('\n');
|
||||
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, blockText);
|
||||
}
|
||||
|
||||
// TODO: Optimization
|
||||
// TODO: is not compatible with safari
|
||||
private _copyToClipboardFromPc(clips: any[]) {
|
||||
let success = false;
|
||||
const tempElem = document.createElement('textarea');
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Clipboard } from './clipboard';
|
||||
export type { ClipBlockInfo, HTML2BlockResult } from './types';
|
||||
@@ -6,17 +6,11 @@ import {
|
||||
} 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 { services } from '@toeverything/datasource/db-service';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
const SUPPORT_MARKDOWN_PASTE = true;
|
||||
|
||||
import { escape } from 'html-escaper';
|
||||
import { marked } from 'marked';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
type TextValueItem = {
|
||||
text: string;
|
||||
[key: string]: any;
|
||||
@@ -25,93 +19,89 @@ type TextValueItem = {
|
||||
export class Paste {
|
||||
private _editor: Editor;
|
||||
private _markdownParse: MarkdownParser;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
|
||||
constructor(
|
||||
editor: Editor,
|
||||
clipboardParse: ClipboardParse,
|
||||
markdownParse: MarkdownParser
|
||||
) {
|
||||
this._markdownParse = markdownParse;
|
||||
this._clipboardParse = clipboardParse;
|
||||
private _utils: ClipboardUtils;
|
||||
constructor(editor: Editor) {
|
||||
this._markdownParse = new MarkdownParser();
|
||||
this._editor = editor;
|
||||
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
this.handlePaste = this.handlePaste.bind(this);
|
||||
}
|
||||
private static _optimalMimeType: string[] = [
|
||||
// The event handler will get the most needed clipboard data based on this array order
|
||||
private static _optimalMimeTypes: string[] = [
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
||||
];
|
||||
public handlePaste(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
public async handlePaste(e: ClipboardEvent) {
|
||||
e.stopPropagation();
|
||||
|
||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||
|
||||
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||
if (isPureFile) {
|
||||
this._pasteFile(clipboardData);
|
||||
} else {
|
||||
this._pasteContent(clipboardData);
|
||||
}
|
||||
const blocks = await this.clipboardEvent2Blocks(e);
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
public getOptimalClip(clipboardData: any) {
|
||||
const mimeTypeArr = Paste._optimalMimeType;
|
||||
|
||||
for (let i = 0; i < mimeTypeArr.length; i++) {
|
||||
const data =
|
||||
clipboardData[mimeTypeArr[i]] ||
|
||||
clipboardData.getData(mimeTypeArr[i]);
|
||||
public async clipboardEvent2Blocks(e: ClipboardEvent) {
|
||||
const clipboardData = e.clipboardData;
|
||||
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||
|
||||
if (isPureFile) {
|
||||
return this._file2Blocks(clipboardData);
|
||||
}
|
||||
return this._clipboardData2Blocks(clipboardData);
|
||||
}
|
||||
// Get the most needed clipboard data based on `_optimalMimeTypes` order
|
||||
public getOptimalClip(clipboardData: ClipboardEvent['clipboardData']) {
|
||||
for (let i = 0; i < Paste._optimalMimeTypes.length; i++) {
|
||||
const mimeType = Paste._optimalMimeTypes[i];
|
||||
const data = clipboardData.getData(mimeType);
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
type: mimeTypeArr[i],
|
||||
type: mimeType,
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
private _pasteContent(clipboardData: any) {
|
||||
const originClip: { data: any; type: any } = this.getOptimalClip(
|
||||
clipboardData
|
||||
) as { data: any; type: any };
|
||||
private async _clipboardData2Blocks(
|
||||
clipboardData: ClipboardEvent['clipboardData']
|
||||
): Promise<ClipBlockInfo[]> {
|
||||
const optimalClip = this.getOptimalClip(clipboardData);
|
||||
if (
|
||||
optimalClip?.type ===
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED
|
||||
) {
|
||||
const clipInfo: InnerClipInfo = JSON.parse(optimalClip.data);
|
||||
return clipInfo.data;
|
||||
}
|
||||
|
||||
const originTextClipData = clipboardData.getData(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
||||
const textClipData = escape(
|
||||
clipboardData.getData(OFFICE_CLIPBOARD_MIMETYPE.TEXT)
|
||||
);
|
||||
|
||||
let clipData = originClip['data'];
|
||||
const shouldConvertMarkdown =
|
||||
this._markdownParse.checkIfTextContainsMd(textClipData);
|
||||
|
||||
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
||||
clipData = Paste._excapeHtml(clipData);
|
||||
if (
|
||||
optimalClip?.type === OFFICE_CLIPBOARD_MIMETYPE.HTML &&
|
||||
!shouldConvertMarkdown
|
||||
) {
|
||||
return this._utils.convertHTMLString2Blocks(optimalClip.data);
|
||||
}
|
||||
|
||||
switch (originClip['type']) {
|
||||
/** Protocol paste */
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
|
||||
this._firePasteEditAction(clipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
|
||||
this._pasteHtml(clipData, originTextClipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
|
||||
this._pasteText(clipData, originTextClipData);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
if (shouldConvertMarkdown) {
|
||||
const md2html = marked.parse(textClipData);
|
||||
return this._utils.convertHTMLString2Blocks(md2html);
|
||||
}
|
||||
|
||||
return this._utils.textToBlock(textClipData);
|
||||
}
|
||||
private async _firePasteEditAction(clipboardData: any) {
|
||||
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
||||
clipInfo && this._insertBlocks(clipInfo.data, clipInfo.select);
|
||||
}
|
||||
private async _pasteFile(clipboardData: any) {
|
||||
|
||||
private async _file2Blocks(clipboardData: any): Promise<ClipBlockInfo[]> {
|
||||
const file = Paste._getImageFile(clipboardData);
|
||||
if (file) {
|
||||
const result = await services.api.file.create({
|
||||
@@ -130,8 +120,9 @@ export class Paste {
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
await this._insertBlocks([blockInfo]);
|
||||
return [blockInfo];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
private static _isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
@@ -144,32 +135,14 @@ export class Paste {
|
||||
);
|
||||
}
|
||||
|
||||
private static _isTextEditBlock(type: BlockFlavorKeys) {
|
||||
return (
|
||||
type === Protocol.Block.Type.page ||
|
||||
type === Protocol.Block.Type.text ||
|
||||
type === Protocol.Block.Type.heading1 ||
|
||||
type === Protocol.Block.Type.heading2 ||
|
||||
type === Protocol.Block.Type.heading3 ||
|
||||
type === Protocol.Block.Type.quote ||
|
||||
type === Protocol.Block.Type.todo ||
|
||||
type === Protocol.Block.Type.code ||
|
||||
type === Protocol.Block.Type.callout ||
|
||||
type === Protocol.Block.Type.numbered ||
|
||||
type === Protocol.Block.Type.bullet
|
||||
);
|
||||
}
|
||||
|
||||
private async _insertBlocks(
|
||||
blocks: ClipBlockInfo[],
|
||||
pasteSelect?: SelectInfo
|
||||
) {
|
||||
private async _insertBlocks(blocks: ClipBlockInfo[]) {
|
||||
if (blocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentSelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
// TODO: Logic of insert blocks maybe should declare in blockHelper
|
||||
// When the selection is in one of the blocks, select?.type === 'Range'
|
||||
// Currently the selection does not support cross-blocking, so this case is not considered
|
||||
if (currentSelectInfo.type === 'Range') {
|
||||
@@ -177,15 +150,16 @@ export class Paste {
|
||||
const selectedBlock = await this._editor.getBlockById(
|
||||
currentSelectInfo.blocks[0].blockId
|
||||
);
|
||||
const isSelectedBlockCanEdit = Paste._isTextEditBlock(
|
||||
selectedBlock.type
|
||||
const isSelectedBlockCanEdit = this._editor.isEditableView(
|
||||
selectedBlock?.type
|
||||
);
|
||||
|
||||
const blockView = this._editor.getView(selectedBlock.type);
|
||||
const isSelectedBlockEmpty = blockView.isEmpty(selectedBlock);
|
||||
if (isSelectedBlockCanEdit && !isSelectedBlockEmpty) {
|
||||
const shouldSplitBlock =
|
||||
blocks.length > 1 ||
|
||||
!Paste._isTextEditBlock(blocks[0].type);
|
||||
!this._editor.isEditableView(blocks[0].type);
|
||||
const pureText = !shouldSplitBlock
|
||||
? blocks[0].properties.text.value
|
||||
: [{ text: '' }];
|
||||
@@ -405,7 +379,7 @@ export class Paste {
|
||||
}
|
||||
private async _setEndSelectToBlock(blockId: string) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
const isBlockCanEdit = Paste._isTextEditBlock(block.type);
|
||||
const isBlockCanEdit = this._editor.isEditableView(block.type);
|
||||
if (!isBlockCanEdit) {
|
||||
return;
|
||||
}
|
||||
@@ -450,34 +424,6 @@ export class Paste {
|
||||
);
|
||||
}
|
||||
|
||||
private async _pasteHtml(clipData: any, originTextClipData: any) {
|
||||
if (SUPPORT_MARKDOWN_PASTE) {
|
||||
const hasMarkdown =
|
||||
this._markdownParse.checkIfTextContainsMd(originTextClipData);
|
||||
if (hasMarkdown) {
|
||||
try {
|
||||
const convertedDataObj =
|
||||
this._markdownParse.md2Html(originTextClipData);
|
||||
if (convertedDataObj.isConverted) {
|
||||
clipData = convertedDataObj.text;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
clipData = originTextClipData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = this._clipboardParse.html2blocks(clipData);
|
||||
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
|
||||
private async _pasteText(clipData: any, originTextClipData: any) {
|
||||
const blocks = this._clipboardParse.text2blocks(clipData);
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
|
||||
private static _getImageFile(clipboardData: any) {
|
||||
const files = clipboardData.files;
|
||||
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
||||
@@ -485,16 +431,4 @@ export class Paste {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private static _excapeHtml(data: any, onlySpace?: any) {
|
||||
if (!onlySpace) {
|
||||
// TODO:
|
||||
// data = string.htmlEscape(data);
|
||||
// data = data.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// data = data.replace(/ /g, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { SelectInfo } from '../selection';
|
||||
|
||||
// export type ClipBlockInfo = Partial<ReturnEditorBlock> & {
|
||||
// children?: ClipBlockInfo[];
|
||||
// };
|
||||
export const OFFICE_CLIPBOARD_MIMETYPE = {
|
||||
DOCS_DOCUMENT_SLICE_CLIP_WRAPPED: 'affine/x-c+w',
|
||||
HTML: 'text/html',
|
||||
@@ -20,8 +23,9 @@ export const OFFICE_CLIPBOARD_MIMETYPE = {
|
||||
export interface ClipBlockInfo {
|
||||
type: BlockFlavorKeys;
|
||||
properties?: Partial<DefaultColumnsValue>;
|
||||
children: ClipBlockInfo[];
|
||||
children?: ClipBlockInfo[];
|
||||
}
|
||||
export type HTML2BlockResult = ClipBlockInfo[] | null;
|
||||
|
||||
export interface InnerClipInfo {
|
||||
select: SelectInfo;
|
||||
|
||||
@@ -1,52 +1,150 @@
|
||||
import { Editor } from '../editor';
|
||||
import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types';
|
||||
import { Clip } from './clip';
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
|
||||
import { ClipBlockInfo } from './types';
|
||||
|
||||
export const shouldHandlerContinue = (event: Event, editor: Editor) => {
|
||||
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
const getIsLink = (htmlElement: HTMLElement) => {
|
||||
return ['A', 'IMG'].includes(htmlElement.tagName);
|
||||
};
|
||||
const getTextStyle = (htmlElement: HTMLElement) => {
|
||||
const tagName = htmlElement.tagName;
|
||||
const textStyle: { [key: string]: any } = {};
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
const style = (htmlElement.getAttribute('style') || '')
|
||||
.split(';')
|
||||
.reduce((style: { [key: string]: any }, styleString) => {
|
||||
const [key, value] = styleString.split(':');
|
||||
if (key && value) {
|
||||
style[key] = value;
|
||||
}
|
||||
return style;
|
||||
}, {});
|
||||
|
||||
if (
|
||||
style['font-weight'] === 'bold' ||
|
||||
Number(style['font-weight']) > 400 ||
|
||||
['STRONG', 'B', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(
|
||||
htmlElement.tagName
|
||||
)
|
||||
) {
|
||||
textStyle['bold'] = true;
|
||||
}
|
||||
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||
return false;
|
||||
if (getIsLink(htmlElement)) {
|
||||
textStyle['type'] = 'link';
|
||||
textStyle['url'] =
|
||||
htmlElement.getAttribute('href') || htmlElement.getAttribute('src');
|
||||
textStyle['id'] = getRandomString('link');
|
||||
}
|
||||
|
||||
return editor.selectionManager.currentSelectInfo.type !== 'None';
|
||||
if (tagName === 'EM' || style['fontStyle'] === 'italic') {
|
||||
textStyle['italic'] = true;
|
||||
}
|
||||
if (
|
||||
tagName === 'U' ||
|
||||
(style['text-decoration'] &&
|
||||
style['text-decoration'].indexOf('underline') !== -1) ||
|
||||
style['border-bottom']
|
||||
) {
|
||||
textStyle['underline'] = true;
|
||||
}
|
||||
if (tagName === 'CODE') {
|
||||
textStyle['inlinecode'] = true;
|
||||
}
|
||||
if (
|
||||
tagName === 'S' ||
|
||||
tagName === 'DEL' ||
|
||||
(style['text-decoration'] &&
|
||||
style['text-decoration'].indexOf('line-through') !== -1)
|
||||
) {
|
||||
textStyle['strikethrough'] = true;
|
||||
}
|
||||
|
||||
return textStyle;
|
||||
};
|
||||
|
||||
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[],
|
||||
export const commonHTML2Block = (
|
||||
element: HTMLElement | Node,
|
||||
type: BlockFlavorKeys = Protocol.Block.Type.text,
|
||||
ignoreEmptyElement = true
|
||||
): ClipBlockInfo => {
|
||||
const textValue = commonHTML2Text(element, {}, ignoreEmptyElement);
|
||||
if (!textValue.length && ignoreEmptyElement) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
properties: {
|
||||
text: { value: textValue },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
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);
|
||||
const getSingleLabelHTMLElementContent = (htmlElement: HTMLElement) => {
|
||||
if (htmlElement.tagName === 'IMG') {
|
||||
return (
|
||||
htmlElement.getAttribute('alt') || htmlElement.getAttribute('src')
|
||||
);
|
||||
}
|
||||
return blockInfo;
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getClipDataOfBlocksById = async (
|
||||
editor: Editor,
|
||||
blockIds: string[]
|
||||
export const commonHTML2Text = (
|
||||
element: HTMLElement | Node,
|
||||
textStyle: { [key: string]: any } = {},
|
||||
ignoreEmptyText = true
|
||||
) => {
|
||||
const clipInfos = await Promise.all(
|
||||
blockIds.map(blockId => getClipInfoOfBlockById(editor, blockId))
|
||||
);
|
||||
if (element instanceof Text) {
|
||||
return element.textContent.split('\n').map(text => {
|
||||
return { text: text, ...textStyle };
|
||||
});
|
||||
}
|
||||
const htmlElement = element as HTMLElement;
|
||||
const childNodes = Array.from(htmlElement.childNodes);
|
||||
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
const isLink = getIsLink(htmlElement);
|
||||
const currentTextStyle = getTextStyle(htmlElement);
|
||||
|
||||
if (!childNodes.length) {
|
||||
const singleLabelContent =
|
||||
getSingleLabelHTMLElementContent(htmlElement);
|
||||
if (isLink && singleLabelContent) {
|
||||
return [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: singleLabelContent,
|
||||
},
|
||||
],
|
||||
...currentTextStyle,
|
||||
},
|
||||
];
|
||||
}
|
||||
return ignoreEmptyText ? [] : [{ text: '', ...currentTextStyle }];
|
||||
}
|
||||
|
||||
const childTexts = childNodes
|
||||
.reduce((result, childNode) => {
|
||||
const textBlocks = commonHTML2Text(
|
||||
childNode,
|
||||
isLink
|
||||
? textStyle
|
||||
: {
|
||||
...textStyle,
|
||||
...currentTextStyle,
|
||||
},
|
||||
ignoreEmptyText
|
||||
);
|
||||
result.push(...textBlocks);
|
||||
return result;
|
||||
}, [])
|
||||
.filter(v => v);
|
||||
|
||||
if (isLink && childTexts.length) {
|
||||
return [
|
||||
{
|
||||
children: childTexts,
|
||||
...currentTextStyle,
|
||||
},
|
||||
];
|
||||
}
|
||||
return childTexts;
|
||||
};
|
||||
|
||||
@@ -13,8 +13,7 @@ import HotKeys from 'hotkeys-js';
|
||||
import type { WorkspaceAndBlockId } from './block';
|
||||
import { AsyncBlock } from './block';
|
||||
import { BlockHelper } from './block/block-helper';
|
||||
import { BrowserClipboard } from './clipboard/browser-clipboard';
|
||||
import { ClipboardPopulator } from './clipboard/clipboard-populator';
|
||||
import { Clipboard } from './clipboard';
|
||||
import { EditorCommands } from './commands';
|
||||
import { EditorConfig } from './config';
|
||||
import { DragDropManager } from './drag-drop';
|
||||
@@ -46,7 +45,7 @@ export class Editor implements Virgo {
|
||||
private _cacheManager = new Map<string, Promise<AsyncBlock | null>>();
|
||||
public mouseManager = new MouseManager(this);
|
||||
public selectionManager = new SelectionManager(this);
|
||||
public keyboardManager = new KeyboardManager(this);
|
||||
public keyboardManager: KeyboardManager;
|
||||
public scrollManager = new ScrollManager(this);
|
||||
public dragDropManager = new DragDropManager(this);
|
||||
public commands = new EditorCommands(this);
|
||||
@@ -63,8 +62,9 @@ export class Editor implements Virgo {
|
||||
readonly = false;
|
||||
private _rootBlockId: string;
|
||||
private storage_manager?: StorageManager;
|
||||
private clipboard?: BrowserClipboard;
|
||||
private clipboard_populator?: ClipboardPopulator;
|
||||
private _clipboard: Clipboard;
|
||||
// private clipboardActionDispacher?: ClipboardEventDispatcher;
|
||||
// private clipboard_populator?: ClipboardPopulator;
|
||||
public reactRenderRoot: {
|
||||
render: PatchNode;
|
||||
has: (key: string) => boolean;
|
||||
@@ -78,10 +78,13 @@ export class Editor implements Virgo {
|
||||
this._rootBlockId = props.rootBlockId;
|
||||
this.hooks = new Hooks();
|
||||
this.plugin_manager = new PluginManager(this, this.hooks);
|
||||
this._clipboard = new Clipboard(this, this.ui_container);
|
||||
this.plugin_manager.registerAll(props.plugins);
|
||||
if (props.isEdgeless) {
|
||||
this.isEdgeless = true;
|
||||
}
|
||||
// Wait for rootId/isEdgeless set
|
||||
this.keyboardManager = new KeyboardManager(this);
|
||||
for (const [name, block] of Object.entries(props.views)) {
|
||||
services.api.editorBlock.registerContentExporter(
|
||||
this.workspace,
|
||||
@@ -136,13 +139,17 @@ export class Editor implements Virgo {
|
||||
|
||||
public set container(v: HTMLDivElement) {
|
||||
this.ui_container = v;
|
||||
this._initClipboard();
|
||||
this._clipboard.clipboardTarget = v;
|
||||
}
|
||||
|
||||
public get container() {
|
||||
return this.ui_container;
|
||||
}
|
||||
|
||||
public get clipboard() {
|
||||
return this._clipboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use it discreetly.
|
||||
* Preference to use {@link withBatch}
|
||||
@@ -189,26 +196,26 @@ export class Editor implements Virgo {
|
||||
};
|
||||
}
|
||||
|
||||
private _disposeClipboard() {
|
||||
this.clipboard?.dispose();
|
||||
this.clipboard_populator?.disposeInternal();
|
||||
}
|
||||
// private _disposeClipboard() {
|
||||
// this.clipboard?.dispose();
|
||||
// this.clipboard_populator?.disposeInternal();
|
||||
// }
|
||||
|
||||
private _initClipboard() {
|
||||
this._disposeClipboard();
|
||||
if (this.ui_container && !this._isDisposed) {
|
||||
this.clipboard = new BrowserClipboard(
|
||||
this.ui_container,
|
||||
this.hooks,
|
||||
this
|
||||
);
|
||||
this.clipboard_populator = new ClipboardPopulator(
|
||||
this,
|
||||
this.hooks,
|
||||
this.selectionManager
|
||||
);
|
||||
}
|
||||
}
|
||||
// private _initClipboard() {
|
||||
// this._disposeClipboard();
|
||||
// if (this.ui_container && !this._isDisposed) {
|
||||
// this.clipboardActionDispacher = new ClipboardEventDispatcher({
|
||||
// clipboardTarget: this.ui_container,
|
||||
// hooks: this.hooks,
|
||||
// editor: this,
|
||||
// });
|
||||
// this.clipboard_populator = new ClipboardPopulator(
|
||||
// this,
|
||||
// this.hooks,
|
||||
// this.selectionManager
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Root Block Id */
|
||||
getRootBlockId() {
|
||||
@@ -249,6 +256,14 @@ export class Editor implements Virgo {
|
||||
getView(type: string) {
|
||||
return this.views[type];
|
||||
}
|
||||
getEditableViews() {
|
||||
return Object.values(this.views)
|
||||
.map(view => (view.editable ? view : null))
|
||||
.filter(v => v);
|
||||
}
|
||||
isEditableView(type: string) {
|
||||
return this.views[type].editable;
|
||||
}
|
||||
|
||||
private async _initBlock(
|
||||
blockData: ReturnEditorBlock
|
||||
@@ -333,6 +348,12 @@ export class Editor implements Virgo {
|
||||
return await this.getBlock({ workspace: this.workspace, id: blockId });
|
||||
}
|
||||
|
||||
async getBlockByIds(ids: string[]): Promise<Awaited<AsyncBlock | null>[]> {
|
||||
return await Promise.all(
|
||||
ids.map(id => this.getBlock({ workspace: this.workspace, id }))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: to be optimized
|
||||
* get block`s dom by block`s id
|
||||
@@ -477,6 +498,13 @@ export class Editor implements Virgo {
|
||||
return await services.api.editorBlock.query(this.workspace, query);
|
||||
}
|
||||
|
||||
async queryByPageId(pageId: string) {
|
||||
return await services.api.editorBlock.get({
|
||||
workspace: this.workspace,
|
||||
ids: [pageId],
|
||||
});
|
||||
}
|
||||
|
||||
/** Hooks */
|
||||
|
||||
public getHooks(): HooksRunner & PluginHooks {
|
||||
@@ -496,12 +524,7 @@ export class Editor implements Virgo {
|
||||
}
|
||||
|
||||
public async page2html(): Promise<string> {
|
||||
const parse = this.clipboard?.getClipboardParse();
|
||||
if (!parse) {
|
||||
return '';
|
||||
}
|
||||
const html_str = await parse.page2html();
|
||||
return html_str;
|
||||
return this.clipboard?.clipboardUtils?.page2html?.();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -517,6 +540,6 @@ export class Editor implements Virgo {
|
||||
this.plugin_manager.dispose();
|
||||
this.selectionManager.dispose();
|
||||
this.dragDropManager.dispose();
|
||||
this._disposeClipboard();
|
||||
this._clipboard?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import ClipboardParseInner from './clipboard/clipboard-parse';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ClipboardParse = ClipboardParseInner;
|
||||
|
||||
export { AsyncBlock } from './block';
|
||||
export * from './commands/types';
|
||||
export { Editor as BlockEditor } from './editor';
|
||||
export * from './selection';
|
||||
export { BlockDropPlacement, HookType, GroupDirection } from './types';
|
||||
export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
|
||||
export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
|
||||
export { BaseView } from './views/base-view';
|
||||
export type { ChildrenView, CreateView } from './views/base-view';
|
||||
export { getClipDataOfBlocksById } from './clipboard/utils';
|
||||
export type { HTML2BlockResult, ClipBlockInfo } from './clipboard';
|
||||
|
||||
@@ -2,19 +2,19 @@ import HotKeys from 'hotkeys-js';
|
||||
|
||||
import { uaHelper } from '@toeverything/utils';
|
||||
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BlockEditor } from '../..';
|
||||
import { supportChildren } from '../../utils';
|
||||
import { SelectionManager } from '../selection';
|
||||
import {
|
||||
HotKeyTypes,
|
||||
HotkeyMap,
|
||||
MacHotkeyMap,
|
||||
WinHotkeyMap,
|
||||
GlobalHotkeyMap,
|
||||
GlobalMacHotkeyMap,
|
||||
GlobalWinHotkeyMap,
|
||||
HotkeyMap,
|
||||
HotKeyTypes,
|
||||
MacHotkeyMap,
|
||||
WinHotkeyMap,
|
||||
} from './hotkey-map';
|
||||
import { supportChildren } from '../../utils';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
type KeyboardEventHandler = (event: KeyboardEvent) => void;
|
||||
export class KeyboardManager {
|
||||
private _editor: BlockEditor;
|
||||
@@ -22,8 +22,20 @@ export class KeyboardManager {
|
||||
private hotkeys: HotkeyMap;
|
||||
private global_hotkeys: GlobalHotkeyMap;
|
||||
private handler_map: { [k: string]: Array<KeyboardEventHandler> };
|
||||
/**
|
||||
* Every editor should have its own hotkey scope,
|
||||
* but edgeless had multiple editor instances,
|
||||
* all edgeless editors should have shared scope
|
||||
*/
|
||||
private hotkeyScope: string;
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
if (editor.isEdgeless) {
|
||||
// edgeless editors should have shared scope
|
||||
this.hotkeyScope = 'whiteboard';
|
||||
} else {
|
||||
this.hotkeyScope = 'editor_' + editor.getRootBlockId();
|
||||
}
|
||||
this._editor = editor;
|
||||
this.selection_manager = this._editor.selectionManager;
|
||||
if (uaHelper.isMacOs) {
|
||||
@@ -35,7 +47,7 @@ export class KeyboardManager {
|
||||
}
|
||||
this.handler_map = {};
|
||||
|
||||
HotKeys.setScope('editor');
|
||||
HotKeys.setScope(this.hotkeyScope);
|
||||
|
||||
// this.init_common_shortcut_cb();
|
||||
this.bind_hot_key_handlers();
|
||||
@@ -44,43 +56,63 @@ export class KeyboardManager {
|
||||
private bind_hot_key_handlers() {
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.selectAll,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_select_all
|
||||
);
|
||||
|
||||
this.bind_hotkey(this.hotkeys.undo, 'editor', this.handle_undo);
|
||||
this.bind_hotkey(this.hotkeys.redo, 'editor', this.handle_redo);
|
||||
this.bind_hotkey(this.hotkeys.remove, 'editor', this.handle_remove);
|
||||
this.bind_hotkey(this.hotkeys.undo, this.hotkeyScope, this.handle_undo);
|
||||
this.bind_hotkey(this.hotkeys.redo, this.hotkeyScope, this.handle_redo);
|
||||
this.bind_hotkey(this.hotkeys.remove, 'all', this.handle_remove);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.checkUncheck,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_check_uncheck
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.preExpendSelect,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_pre_expend_select
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.nextExpendSelect,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_next_expend_select
|
||||
);
|
||||
this.bind_hotkey(this.hotkeys.up, 'editor', this.handle_click_up);
|
||||
this.bind_hotkey(this.hotkeys.down, 'editor', this.handleClickDown);
|
||||
this.bind_hotkey(this.hotkeys.left, 'editor', this.handle_click_up);
|
||||
this.bind_hotkey(this.hotkeys.right, 'editor', this.handleClickDown);
|
||||
this.bind_hotkey(this.hotkeys.mergeGroup, 'editor', this.mergeGroup);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.up,
|
||||
this.hotkeyScope,
|
||||
this.handle_click_up
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.down,
|
||||
this.hotkeyScope,
|
||||
this.handleClickDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.left,
|
||||
this.hotkeyScope,
|
||||
this.handle_click_up
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.right,
|
||||
this.hotkeyScope,
|
||||
this.handleClickDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroup,
|
||||
this.hotkeyScope,
|
||||
this.mergeGroup
|
||||
);
|
||||
this.bind_hotkey(this.hotkeys.enter, 'all', this.handleEnter);
|
||||
this.bind_hotkey(this.global_hotkeys.search, 'all', this.handle_search);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroupDown,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.mergeGroupDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroupUp,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.mergeGroupUp
|
||||
);
|
||||
}
|
||||
@@ -93,24 +125,12 @@ export class KeyboardManager {
|
||||
handler.forEach(h => {
|
||||
HotKeys(key, scope, h);
|
||||
if (!this.handler_map[key]) {
|
||||
this.handler_map[key] = [h];
|
||||
} else {
|
||||
this.handler_map[key].push(h);
|
||||
this.handler_map[key] = [];
|
||||
}
|
||||
this.handler_map[key].push(h);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* bind global shortcut event
|
||||
* @param {HotkeyMapKeys} type
|
||||
* @param {KeyboardEventHandler} handler
|
||||
* @memberof KeyboardManager
|
||||
*/
|
||||
public bind(type: HotKeyTypes, handler: KeyboardEventHandler) {
|
||||
this.bind_hotkey(this.hotkeys[type], 'editor', handler);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* emit a shortcut event,
|
||||
@@ -127,21 +147,12 @@ export class KeyboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* unbind keyboard event
|
||||
* @param {HotKeyTypes} key
|
||||
* @param {KeyboardEventHandler} cb
|
||||
* @memberof KeyboardManager
|
||||
*/
|
||||
public unbind(key: HotKeyTypes, cb: KeyboardEventHandler) {
|
||||
HotKeys.unbind(key, 'editor', cb);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
Object.keys(this.handler_map).map(key => HotKeys.unbind(key, 'editor'));
|
||||
|
||||
Object.entries(this.handler_map).forEach(([key, fns]) =>
|
||||
fns.forEach(fn => HotKeys.unbind(key, fn))
|
||||
);
|
||||
this.handler_map = {};
|
||||
HotKeys.deleteScope(this.hotkeyScope);
|
||||
}
|
||||
|
||||
private handle_select_all = (event: KeyboardEvent) => {
|
||||
@@ -228,20 +239,20 @@ export class KeyboardManager {
|
||||
);
|
||||
} else {
|
||||
// suspend(true)
|
||||
let textBlock = await this._editor.createBlock('text');
|
||||
const textBlock = await this._editor.createBlock('text');
|
||||
await selectedNode.after(textBlock);
|
||||
this._editor.selectionManager.setActivatedNodeId(textBlock.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
private mergeGroup = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
this._editor.commands.blockCommands.mergeGroup(...selectedGroup);
|
||||
};
|
||||
private mergeGroupDown = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
if (selectedGroup.length) {
|
||||
let nextGroup = await selectedGroup[
|
||||
const nextGroup = await selectedGroup[
|
||||
selectedGroup.length - 1
|
||||
].nextSibling();
|
||||
if (nextGroup?.type === Protocol.Block.Type.group) {
|
||||
@@ -253,9 +264,9 @@ export class KeyboardManager {
|
||||
}
|
||||
};
|
||||
private mergeGroupUp = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
if (selectedGroup.length) {
|
||||
let preGroup = await selectedGroup[0].previousSibling();
|
||||
const preGroup = await selectedGroup[0].previousSibling();
|
||||
if (preGroup?.type === Protocol.Block.Type.group) {
|
||||
this._editor.commands.blockCommands.mergeGroup(
|
||||
preGroup,
|
||||
|
||||
@@ -116,12 +116,15 @@ export class Hooks implements HooksRunner, PluginHooks {
|
||||
this._runHook(HookType.ON_SEARCH);
|
||||
}
|
||||
|
||||
public beforeCopy(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.BEFORE_COPY, e);
|
||||
public onCopy(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_COPY, e);
|
||||
}
|
||||
|
||||
public beforeCut(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.BEFORE_CUT, e);
|
||||
public onCut(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_CUT, e);
|
||||
}
|
||||
public onPaste(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_PASTE, e);
|
||||
}
|
||||
|
||||
public onRootNodeScroll(e: React.UIEvent): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, type UIEvent } from 'react';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { type UIEvent } from 'react';
|
||||
|
||||
import { domToRect, Rect } from '@toeverything/utils';
|
||||
import type { Editor as BlockEditor } from '../editor';
|
||||
@@ -164,22 +164,9 @@ export class ScrollManager {
|
||||
if (!block.dom) {
|
||||
return console.warn(`Block is not exist.`);
|
||||
}
|
||||
const containerRect = domToRect(this._scrollContainer);
|
||||
const blockRect = domToRect(block.dom);
|
||||
|
||||
const blockRelativeTopToEditor =
|
||||
blockRect.top - containerRect.top - containerRect.height / 4;
|
||||
const blockRelativeLeftToEditor = blockRect.left - containerRect.left;
|
||||
|
||||
this.scrollTo({
|
||||
left: blockRelativeLeftToEditor,
|
||||
top: blockRelativeTopToEditor,
|
||||
behavior,
|
||||
});
|
||||
this._updateScrollInfo(
|
||||
blockRelativeLeftToEditor,
|
||||
blockRelativeTopToEditor
|
||||
);
|
||||
/* use dom primary ability */
|
||||
block.dom.scrollIntoView({ block: 'start', behavior });
|
||||
}
|
||||
|
||||
public async keepBlockInView(
|
||||
@@ -209,11 +196,11 @@ export class ScrollManager {
|
||||
private _getKeepInViewParams(blockRect: Rect) {
|
||||
if (this.scrollContainer == null) return 0;
|
||||
const { top, bottom } = domToRect(this._scrollContainer);
|
||||
if (blockRect.top <= top + blockRect.height * 3) {
|
||||
if (blockRect.top <= top + blockRect.height) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (blockRect.bottom >= bottom - blockRect.height * 3) {
|
||||
if (blockRect.bottom >= bottom - blockRect.height) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
* 6. Dependencies between plugins are not supported for the time being
|
||||
*/
|
||||
import type { PatchNode } from '@toeverything/components/ui';
|
||||
import type { BlockFlavors } from '@toeverything/datasource/db-service';
|
||||
import type {
|
||||
BlockFlavors,
|
||||
ReturnEditorBlock,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Point } from '@toeverything/utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { AsyncBlock } from './block';
|
||||
@@ -66,6 +69,8 @@ export interface Virgo {
|
||||
) => Promise<AsyncBlock>;
|
||||
getRootBlockId: () => string;
|
||||
getBlockById(blockId: string): Promise<AsyncBlock | null>;
|
||||
getBlockByIds(blockId: string[]): Promise<(AsyncBlock | null)[]>;
|
||||
queryByPageId(pageId: string): Promise<(ReturnEditorBlock | null)[]>;
|
||||
setHotKeysScope(scope?: string): void;
|
||||
getBlockList: () => Promise<AsyncBlock[]>;
|
||||
getBlockListByLevelOrder: () => Promise<AsyncBlock[]>;
|
||||
@@ -174,8 +179,9 @@ export enum HookType {
|
||||
ON_ROOTNODE_DRAG_END = 'onRootNodeDragEnd',
|
||||
ON_ROOTNODE_DRAG_OVER_CAPTURE = 'onRootNodeDragOverCapture',
|
||||
ON_ROOTNODE_DROP = 'onRootNodeDrop',
|
||||
BEFORE_COPY = 'beforeCopy',
|
||||
BEFORE_CUT = 'beforeCut',
|
||||
ON_COPY = 'onCopy',
|
||||
ON_CUT = 'onCut',
|
||||
ON_PASTE = 'onPaste',
|
||||
ON_ROOTNODE_SCROLL = 'onRootNodeScroll',
|
||||
}
|
||||
|
||||
@@ -207,8 +213,9 @@ export interface HooksRunner {
|
||||
onRootNodeDragEnd: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDragLeave: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDrop: (e: React.DragEvent<Element>) => void;
|
||||
beforeCopy: (e: ClipboardEvent) => void;
|
||||
beforeCut: (e: ClipboardEvent) => void;
|
||||
onCopy: (e: ClipboardEvent) => void;
|
||||
onCut: (e: ClipboardEvent) => void;
|
||||
onPaste: (e: ClipboardEvent) => void;
|
||||
onRootNodeScroll: (e: React.UIEvent) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import type {
|
||||
Column,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { Column } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
ArrayOperation,
|
||||
BlockDecoration,
|
||||
MapOperation,
|
||||
} from '@toeverything/datasource/jwt';
|
||||
import { cloneDeep } from '@toeverything/utils';
|
||||
import { ComponentType, ReactElement } from 'react';
|
||||
import type { EventData } from '../block';
|
||||
import { AsyncBlock } from '../block';
|
||||
import { HTML2BlockResult } from '../clipboard';
|
||||
import type { Editor } from '../editor';
|
||||
import { SelectBlock } from '../selection';
|
||||
|
||||
export interface CreateView {
|
||||
block: AsyncBlock;
|
||||
editor: Editor;
|
||||
@@ -133,76 +129,27 @@ export abstract class BaseView {
|
||||
return result;
|
||||
}
|
||||
|
||||
getSelProperties(block: AsyncBlock, selectInfo: any): DefaultColumnsValue {
|
||||
return cloneDeep(block.getProperties());
|
||||
}
|
||||
|
||||
html2block(el: Element, parseEl: (el: Element) => any[]): any[] | null {
|
||||
async html2block(props: {
|
||||
element: HTMLElement | Node;
|
||||
editor: Editor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async block2html(
|
||||
async block2Text(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
// The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range
|
||||
selectInfo?: SelectBlock
|
||||
): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async block2html(props: {
|
||||
editor: Editor;
|
||||
block: AsyncBlock;
|
||||
// The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range
|
||||
selectInfo?: SelectBlock;
|
||||
}) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export const getTextProperties = (
|
||||
properties: DefaultColumnsValue,
|
||||
selectInfo: any
|
||||
) => {
|
||||
let text_value = properties.text.value;
|
||||
if (text_value.length === 0) {
|
||||
return properties;
|
||||
}
|
||||
if (selectInfo.endInfo) {
|
||||
text_value = text_value.slice(0, selectInfo.endInfo.arrayIndex + 1);
|
||||
text_value[text_value.length - 1].text = text_value[
|
||||
text_value.length - 1
|
||||
].text.substring(0, selectInfo.endInfo.offset);
|
||||
}
|
||||
if (selectInfo.startInfo) {
|
||||
text_value = text_value.slice(selectInfo.startInfo.arrayIndex);
|
||||
text_value[0].text = text_value[0].text.substring(
|
||||
selectInfo.startInfo.offset
|
||||
);
|
||||
}
|
||||
properties.text.value = text_value;
|
||||
return properties;
|
||||
};
|
||||
|
||||
export const getTextHtml = (block: AsyncBlock) => {
|
||||
const generate = (textList: any[]) => {
|
||||
let content = '';
|
||||
textList.forEach(text_obj => {
|
||||
let text = text_obj.text || '';
|
||||
if (text_obj.bold) {
|
||||
text = `<strong>${text}</strong>`;
|
||||
}
|
||||
if (text_obj.italic) {
|
||||
text = `<em>${text}</em>`;
|
||||
}
|
||||
if (text_obj.underline) {
|
||||
text = `<u>${text}</u>`;
|
||||
}
|
||||
if (text_obj.inlinecode) {
|
||||
text = `<code>${text}</code>`;
|
||||
}
|
||||
if (text_obj.strikethrough) {
|
||||
text = `<s>${text}</s>`;
|
||||
}
|
||||
if (text_obj.type === 'link') {
|
||||
text = `<a href='${text_obj.url}'>${generate(
|
||||
text_obj.children
|
||||
)}</a>`;
|
||||
}
|
||||
content += text;
|
||||
});
|
||||
return content;
|
||||
};
|
||||
const text_list: any[] = block.getProperty('text').value;
|
||||
return generate(text_list);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot';
|
||||
export * from './render-block';
|
||||
export * from './hooks';
|
||||
|
||||
export { RenderBlock } from './render-block';
|
||||
|
||||
export * from './recast-block';
|
||||
export * from './recast-block/types';
|
||||
|
||||
export * from './block-pendant';
|
||||
|
||||
export { useEditor } from './Contexts';
|
||||
export * from './editor';
|
||||
export * from './hooks';
|
||||
export * from './kanban';
|
||||
export * from './kanban/types';
|
||||
|
||||
export * from './recast-block';
|
||||
export * from './recast-block/types';
|
||||
export * from './render-block';
|
||||
export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot';
|
||||
export * from './utils';
|
||||
|
||||
export * from './editor';
|
||||
|
||||
export { useEditor } from './Contexts';
|
||||
|
||||
@@ -358,6 +358,7 @@ export const useKanban = () => {
|
||||
}
|
||||
card.append(newBlock);
|
||||
editor.selectionManager.activeNodeByNodeId(newBlock.id);
|
||||
return newBlock;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import type { AsyncBlock, BlockEditor } from '../editor';
|
||||
import type { RecastBlock } from '.';
|
||||
import { cloneRecastMetaTo, mergeRecastMeta } from './property';
|
||||
import type { RecastBlock } from './types';
|
||||
|
||||
const mergeGroupProperties = async (...groups: RecastBlock[]) => {
|
||||
const [headGroup, ...restGroups] = groups;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { useRecastBlock } from './Context';
|
||||
import { getHistory, removeHistory, setHistory } from './history';
|
||||
import type { RecastBlock, RecastItem, StatusProperty } from './types';
|
||||
import {
|
||||
META_PROPERTIES_KEY,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
SelectProperty,
|
||||
TABLE_VALUES_KEY,
|
||||
} from './types';
|
||||
import { getHistory, removeHistory, setHistory } from './history';
|
||||
|
||||
/**
|
||||
* Generate a unique id for a property
|
||||
@@ -275,7 +275,7 @@ const isSelectLikeProperty = (
|
||||
metaProperty?: RecastMetaProperty
|
||||
): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => {
|
||||
return (
|
||||
metaProperty &&
|
||||
!!metaProperty &&
|
||||
(metaProperty.type === PropertyType.Status ||
|
||||
metaProperty.type === PropertyType.Select ||
|
||||
metaProperty.type === PropertyType.MultiSelect)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import { RenderBlockProps } from './RenderBlock';
|
||||
|
||||
type BlockRenderProps = {
|
||||
blockRender: (args: RenderBlockProps) => JSX.Element;
|
||||
};
|
||||
|
||||
export const BlockRenderContext = createContext<BlockRenderProps>(
|
||||
genErrorObj(
|
||||
'Failed to get BlockChildrenContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
/**
|
||||
* CAUTION! DO NOT PROVIDE A DYNAMIC BLOCK RENDER!
|
||||
*/
|
||||
export const BlockRenderProvider = ({
|
||||
blockRender,
|
||||
children,
|
||||
}: PropsWithChildren<BlockRenderProps>) => {
|
||||
return (
|
||||
<BlockRenderContext.Provider value={{ blockRender }}>
|
||||
{children}
|
||||
</BlockRenderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useBlockRender = () => {
|
||||
const { blockRender } = useContext(BlockRenderContext);
|
||||
return {
|
||||
BlockRender: blockRender,
|
||||
};
|
||||
};
|
||||
|
||||
export const BlockRender = (props: RenderBlockProps) => {
|
||||
const { BlockRender } = useBlockRender();
|
||||
return <BlockRender {...props} />;
|
||||
};
|
||||
@@ -4,7 +4,13 @@ import { useCallback, useMemo } from 'react';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { useBlock } from '../hooks';
|
||||
|
||||
interface RenderBlockProps {
|
||||
/**
|
||||
* Render nothing
|
||||
*/
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
export const NullBlockRender = () => <></>;
|
||||
|
||||
export interface RenderBlockProps {
|
||||
blockId: string;
|
||||
hasContainer?: boolean;
|
||||
}
|
||||
@@ -17,7 +23,7 @@ export function RenderBlock({
|
||||
const { block } = useBlock(blockId);
|
||||
|
||||
const setRef = useCallback(
|
||||
(dom: HTMLElement) => {
|
||||
(dom: HTMLElement | null) => {
|
||||
if (block != null && dom != null) {
|
||||
block.dom = dom;
|
||||
}
|
||||
@@ -29,7 +35,7 @@ export function RenderBlock({
|
||||
if (block?.type) {
|
||||
return editor.getView(block.type).View;
|
||||
}
|
||||
return () => null;
|
||||
return (): null => null;
|
||||
}, [editor, block?.type]);
|
||||
|
||||
if (!block) {
|
||||
@@ -64,4 +70,5 @@ export function RenderBlock({
|
||||
|
||||
const BlockContainer = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { RenderBlock } from './RenderBlock';
|
||||
import { BlockRender } from './Context';
|
||||
import { NullBlockRender } from './RenderBlock';
|
||||
|
||||
interface RenderChildrenProps {
|
||||
export interface RenderChildrenProps {
|
||||
block: AsyncBlock;
|
||||
indent?: boolean;
|
||||
}
|
||||
|
||||
export const RenderBlockChildren = ({ block }: RenderChildrenProps) => {
|
||||
export const RenderBlockChildren = ({
|
||||
block,
|
||||
indent = true,
|
||||
}: RenderChildrenProps) => {
|
||||
if (BlockRender === NullBlockRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return block.childrenIds.length ? (
|
||||
<>
|
||||
<StyledIdentWrapper indent={indent}>
|
||||
{block.childrenIds.map(childId => {
|
||||
return <RenderBlock key={childId} blockId={childId} />;
|
||||
return <BlockRender key={childId} blockId={childId} />;
|
||||
})}
|
||||
</>
|
||||
</StyledIdentWrapper>
|
||||
) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
const StyledIdentWrapper = styled('div')<{ indent?: boolean }>(
|
||||
({ indent }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
...(indent && { marginLeft: '30px' }),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { useBlock } from '../hooks';
|
||||
import { BlockRenderProvider } from './Context';
|
||||
import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock';
|
||||
|
||||
/**
|
||||
* Render block without children.
|
||||
*/
|
||||
const BlockWithoutChildrenRender = ({ blockId }: RenderBlockProps) => {
|
||||
return (
|
||||
<BlockRenderProvider blockRender={NullBlockRender}>
|
||||
<RenderBlock blockId={blockId} />
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a block, but only one level of children.
|
||||
*/
|
||||
const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => {
|
||||
return (
|
||||
<BlockRenderProvider blockRender={BlockWithoutChildrenRender}>
|
||||
<RenderBlock blockId={blockId} />
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const KanbanParentBlockRender = ({
|
||||
blockId,
|
||||
active,
|
||||
}: RenderBlockProps & { active?: boolean }) => {
|
||||
return (
|
||||
<BlockBorder active={active}>
|
||||
<BlockWithoutChildrenRender blockId={blockId} />
|
||||
</BlockBorder>
|
||||
);
|
||||
};
|
||||
|
||||
const useBlockProgress = (block?: AsyncBlock) => {
|
||||
// Progress of the progress bar. The range is between 0 and 1.
|
||||
// Default progress is 1, that is 100%.
|
||||
const [progress, setProgress] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
const updateProgress = async () => {
|
||||
const children = await block.children();
|
||||
const todoChildren = children.filter(
|
||||
child => child.type === Protocol.Block.Type.todo
|
||||
);
|
||||
const checkedTodoChildren = todoChildren.filter(
|
||||
child => child.getProperty('checked')?.value === true
|
||||
);
|
||||
setProgress(checkedTodoChildren.length / todoChildren.length);
|
||||
};
|
||||
|
||||
updateProgress();
|
||||
|
||||
const unobserve = block.onUpdate(() => {
|
||||
updateProgress();
|
||||
}, 1);
|
||||
|
||||
return unobserve;
|
||||
}, [block]);
|
||||
|
||||
return progress;
|
||||
};
|
||||
|
||||
const KanbanChildrenRender = ({
|
||||
blockId,
|
||||
activeBlock,
|
||||
}: RenderBlockProps & { activeBlock?: string | null }) => {
|
||||
const { block } = useBlock(blockId);
|
||||
const progress = useBlockProgress(block);
|
||||
|
||||
if (!block || !block?.childrenIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockRenderProvider blockRender={NullBlockRender}>
|
||||
<ProgressBar progress={progress} />
|
||||
{block?.childrenIds.map(childId => (
|
||||
<ChildBorder key={childId} active={activeBlock === childId}>
|
||||
<RenderBlock blockId={childId} />
|
||||
</ChildBorder>
|
||||
))}
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const KanbanBlockRender = ({
|
||||
blockId,
|
||||
activeBlock,
|
||||
}: RenderBlockProps & { activeBlock?: string | null }) => {
|
||||
return (
|
||||
<BlockRenderProvider blockRender={NullBlockRender}>
|
||||
<KanbanParentBlockRender
|
||||
blockId={blockId}
|
||||
active={activeBlock === blockId}
|
||||
/>
|
||||
<KanbanChildrenRender blockId={blockId} activeBlock={activeBlock} />
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockBorder = styled('div')<{ active?: boolean }>(
|
||||
({ theme, active }) => ({
|
||||
borderRadius: '5px',
|
||||
padding: '0 4px',
|
||||
border: `1px solid ${
|
||||
active ? theme.affine.palette.primary : 'transparent'
|
||||
}`,
|
||||
})
|
||||
);
|
||||
|
||||
const ProgressBar = styled('div')<{ progress?: number }>(
|
||||
({ progress = 1 }) => ({
|
||||
height: '3px',
|
||||
width: '100%',
|
||||
background: '#CFE5FF',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '12px 0',
|
||||
|
||||
'::after': {
|
||||
content: '""',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
background: '#60A5FA',
|
||||
height: '100%',
|
||||
width: `${(progress * 100).toFixed(2)}%`,
|
||||
transition: 'ease 0.5s all',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ChildBorder = styled(BlockBorder)(({ active, theme }) => ({
|
||||
border: `1px solid ${active ? theme.affine.palette.primary : '#E0E6EB'}`,
|
||||
margin: '4px 0',
|
||||
}));
|
||||
+27
-82
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
RenderBlock,
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type {
|
||||
ComponentPropsWithoutRef,
|
||||
@@ -12,9 +5,10 @@ import type {
|
||||
CSSProperties,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { SCENE_CONFIG } from '../blocks/group/config';
|
||||
import { BlockContainer } from '../components/BlockContainer';
|
||||
import { forwardRef } from 'react';
|
||||
import { CreateView } from '../editor';
|
||||
import { useBlockRender } from './Context';
|
||||
import { NullBlockRender } from './RenderBlock';
|
||||
|
||||
type WithChildrenConfig = {
|
||||
indent: CSSProperties['marginLeft'];
|
||||
@@ -41,45 +35,6 @@ const TreeView = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
interface ChildrenViewProp {
|
||||
childrenIds: string[];
|
||||
handleCollapse: () => void;
|
||||
indent?: string | number;
|
||||
}
|
||||
|
||||
const ChildrenView = ({
|
||||
childrenIds,
|
||||
handleCollapse,
|
||||
indent,
|
||||
}: ChildrenViewProp) => {
|
||||
const [currentView] = useCurrentView();
|
||||
const isKanbanScene = currentView.type === SCENE_CONFIG.KANBAN;
|
||||
|
||||
return (
|
||||
<Children style={{ ...(!isKanbanScene && { marginLeft: indent }) }}>
|
||||
{childrenIds.map((childId, idx) => {
|
||||
if (isKanbanScene) {
|
||||
return (
|
||||
<StyledBorder key={childId}>
|
||||
<RenderBlock blockId={childId} />
|
||||
</StyledBorder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
key={childId}
|
||||
lastItem={idx === childrenIds.length - 1}
|
||||
onClick={handleCollapse}
|
||||
>
|
||||
<RenderBlock key={childId} blockId={childId} />
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</Children>
|
||||
);
|
||||
};
|
||||
|
||||
const CollapsedNode = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<'div'>
|
||||
@@ -104,15 +59,15 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (props: CreateView) => {
|
||||
const { block, editor } = props;
|
||||
const { block } = props;
|
||||
const { BlockRender } = useBlockRender();
|
||||
const collapsed = block.getProperty('collapsed')?.value;
|
||||
const childrenIds = block.childrenIds;
|
||||
const showChildren = !collapsed && childrenIds.length > 0;
|
||||
const showChildren =
|
||||
!collapsed &&
|
||||
childrenIds.length > 0 &&
|
||||
BlockRender !== NullBlockRender;
|
||||
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
const handleCollapse = () => {
|
||||
block.setProperty('collapsed', { value: true });
|
||||
};
|
||||
@@ -122,15 +77,8 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockContainer
|
||||
editor={props.editor}
|
||||
block={block}
|
||||
selected={isSelect}
|
||||
className={Wrapper.toString()}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<div>{creator(props)}</div>
|
||||
</BlockPendantProvider>
|
||||
<>
|
||||
{creator(props)}
|
||||
|
||||
{collapsed && (
|
||||
<CollapsedNode
|
||||
@@ -138,22 +86,24 @@ export const withTreeViewChildren = (
|
||||
style={{ marginLeft: config.indent }}
|
||||
/>
|
||||
)}
|
||||
{showChildren && (
|
||||
<ChildrenView
|
||||
childrenIds={childrenIds}
|
||||
handleCollapse={handleCollapse}
|
||||
indent={config.indent}
|
||||
/>
|
||||
)}
|
||||
</BlockContainer>
|
||||
{showChildren &&
|
||||
childrenIds.map((childId, idx) => {
|
||||
return (
|
||||
<TreeView
|
||||
key={childId}
|
||||
lastItem={idx === childrenIds.length - 1}
|
||||
onClick={handleCollapse}
|
||||
style={{ marginLeft: config.indent }}
|
||||
>
|
||||
<BlockRender key={childId} blockId={childId} />
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' });
|
||||
|
||||
const Children = Wrapper;
|
||||
|
||||
const TREE_COLOR = '#D5DFE6';
|
||||
// adjust left and right margins of the the tree line
|
||||
const TREE_LINE_LEFT_OFFSET = '-16px';
|
||||
@@ -163,6 +113,7 @@ const TREE_LINE_WIDTH = '12px';
|
||||
|
||||
const TreeWrapper = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledTreeView = styled('div')({
|
||||
@@ -228,9 +179,3 @@ const LastItemRadius = styled('div')({
|
||||
borderRadius: '0 0 0 3px',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
const StyledBorder = styled('div')({
|
||||
border: '1px solid #E0E6EB',
|
||||
borderRadius: '5px',
|
||||
margin: '4px',
|
||||
});
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './RenderBlock';
|
||||
export * from './RenderBlockChildren';
|
||||
export { BlockRender, BlockRenderProvider } from './Context';
|
||||
export { NullBlockRender, RenderBlock } from './RenderBlock';
|
||||
export { RenderBlockChildren } from './RenderBlockChildren';
|
||||
export { KanbanBlockRender } from './RenderKanbanBlock';
|
||||
export { withTreeViewChildren } from './WithTreeViewChildren';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"style9": "^0.13.3"
|
||||
"date-fns": "^2.29.2",
|
||||
"style9": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { containerFlavor } from '@toeverything/datasource/db-service';
|
||||
import { BlockDropPlacement, HookType } from '@toeverything/framework/virgo';
|
||||
import { domToRect, last, Point } from '@toeverything/utils';
|
||||
import { StrictMode } from 'react';
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
LeftMenuDraggable,
|
||||
LineInfoSubject,
|
||||
} from './LeftMenuDraggable';
|
||||
import { ignoreBlockTypes } from './menu-config';
|
||||
const DRAG_THROTTLE_DELAY = 60;
|
||||
export class LeftMenuPlugin extends BasePlugin {
|
||||
private _mousedown?: boolean;
|
||||
@@ -104,7 +104,7 @@ export class LeftMenuPlugin extends BasePlugin {
|
||||
const block = await this.editor.getBlockByPoint(
|
||||
new Point(event.clientX, event.clientY)
|
||||
);
|
||||
if (block == null || ignoreBlockTypes.includes(block.type)) return;
|
||||
if (block == null || containerFlavor.includes(block.type)) return;
|
||||
const { direction, block: targetBlock } =
|
||||
await this.editor.dragDropManager.checkBlockDragTypes(
|
||||
event,
|
||||
@@ -143,7 +143,7 @@ export class LeftMenuPlugin extends BasePlugin {
|
||||
const node = await this.editor.getBlockByPoint(
|
||||
new Point(event.clientX, event.clientY)
|
||||
);
|
||||
if (node == null || ignoreBlockTypes.includes(node.type)) {
|
||||
if (node == null || containerFlavor.includes(node.type)) {
|
||||
return;
|
||||
}
|
||||
if (node.dom) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
|
||||
import ShortTextIcon from '@mui/icons-material/ShortText';
|
||||
import TitleIcon from '@mui/icons-material/Title';
|
||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
|
||||
import ShortTextIcon from '@mui/icons-material/ShortText';
|
||||
import TitleIcon from '@mui/icons-material/Title';
|
||||
import {
|
||||
CodeBlockInlineIcon,
|
||||
PagesIcon,
|
||||
} from '@toeverything/components/common';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
export const MENU_WIDTH = 14;
|
||||
export const pageConvertIconSize = 24;
|
||||
type MenuItem = {
|
||||
@@ -93,12 +93,3 @@ const textTypeBlocks: MenuItem[] = [
|
||||
export const addMenuList = [...textTypeBlocks].filter(v => v);
|
||||
|
||||
export const textConvertMenuList = textTypeBlocks;
|
||||
|
||||
export const ignoreBlockTypes: BlockFlavorKeys[] = [
|
||||
Protocol.Block.Type.workspace,
|
||||
Protocol.Block.Type.page,
|
||||
Protocol.Block.Type.group,
|
||||
Protocol.Block.Type.title,
|
||||
Protocol.Block.Type.grid,
|
||||
Protocol.Block.Type.gridItem,
|
||||
];
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"jotai": "^1.7.4",
|
||||
"date-fns": "^2.29.2",
|
||||
"i18next": "^21.9.1",
|
||||
"jotai": "^1.8.1",
|
||||
"react-i18next": "^11.18.4",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"turndown": "7.1.1"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CloseIcon } from '@toeverything/components/common';
|
||||
import { IconButton, MuiSnackbar, styled } from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { useLocalTrigger } from '@toeverything/datasource/state';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const cleanupWorkspace = (workspace: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase(workspace);
|
||||
@@ -75,7 +75,7 @@ export const FileSystem = () => {
|
||||
setError(true);
|
||||
setTimeout(() => setError(false), 3000);
|
||||
}, []);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const apiSupported = useMemo(() => fsApiSupported(), []);
|
||||
|
||||
if (apiSupported && !selected) {
|
||||
@@ -105,7 +105,7 @@ export const FileSystem = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sync to Disk
|
||||
{t('Sync to Disk')}
|
||||
</StyledFileSystem>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useShowSettingsSidebar,
|
||||
} from '@toeverything/datasource/state';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
|
||||
import { FileSystem, fsApiSupported } from './FileSystem';
|
||||
import { CurrentPageTitle } from './Title';
|
||||
@@ -19,16 +20,17 @@ export const LayoutHeader = () => {
|
||||
const [isLocalWorkspace] = useLocalTrigger();
|
||||
const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } =
|
||||
useShowSettingsSidebar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const warningTips = useMemo(() => {
|
||||
if (!fsApiSupported()) {
|
||||
return 'Welcome to the AFFiNE demo. To begin saving changes you can SYNC DATA TO DISK with the latest version of Chromium based browser like Chrome/Edge';
|
||||
return t('WarningTips.IsNotfsApiSupported');
|
||||
} else if (!isLocalWorkspace) {
|
||||
return 'Welcome to the AFFiNE demo. To begin saving changes you can SYNC TO DISK.';
|
||||
return t('WarningTips.IsNotLocalWorkspace');
|
||||
} else {
|
||||
return 'AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data';
|
||||
return t('WarningTips.DoNotStore');
|
||||
}
|
||||
}, [isLocalWorkspace]);
|
||||
}, [isLocalWorkspace, t]);
|
||||
|
||||
const { currentEditors } = useCurrentEditors();
|
||||
|
||||
@@ -50,7 +52,7 @@ export const LayoutHeader = () => {
|
||||
<FlexContainer>
|
||||
<StyledHelper>
|
||||
<FileSystem />
|
||||
<StyledShare disabled={true}>Share</StyledShare>
|
||||
<StyledShare disabled={true}>{t('Share')}</StyledShare>
|
||||
<div style={{ margin: '0px 12px' }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
|
||||
@@ -153,9 +153,7 @@ function PageSettingPortal() {
|
||||
|
||||
const handleExportHtml = async () => {
|
||||
//@ts-ignore
|
||||
const htmlContent = await virgo.clipboard
|
||||
.getClipboardParse()
|
||||
.page2html();
|
||||
const htmlContent = await virgo.clipboard.clipboardUtils.page2html();
|
||||
const htmlTitle = pageBlock.title;
|
||||
|
||||
FileExporter.exportHtml(htmlTitle, htmlContent);
|
||||
@@ -163,9 +161,7 @@ function PageSettingPortal() {
|
||||
|
||||
const handleExportMarkdown = async () => {
|
||||
//@ts-ignore
|
||||
const htmlContent = await virgo.clipboard
|
||||
.getClipboardParse()
|
||||
.page2html();
|
||||
const htmlContent = await virgo.clipboard.clipboardUtils.page2html();
|
||||
const htmlTitle = pageBlock.title;
|
||||
FileExporter.exportMarkdown(htmlTitle, htmlContent);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user