merge branch develop into branch feat/doublelink220820

This commit is contained in:
xiaodong zuo
2022-09-05 11:45:19 +08:00
175 changed files with 6576 additions and 4627 deletions
+10 -50
View File
@@ -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 -4
View File
@@ -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>
/>
);
}
);
+1 -1
View File
@@ -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"
/>
+53 -27
View File
@@ -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() {
+1 -1
View File
@@ -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 }) => {
@@ -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 -2
View File
@@ -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"
}
}
+20 -8
View File
@@ -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>
);
};
/**
+41 -41
View File
@@ -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, '&nbsp;');
// data = data.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
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);
};
+7 -15
View File
@@ -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',
}));
@@ -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';
+2 -2
View File
@@ -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,
];
+4 -2
View File
@@ -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