mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
Merge branch 'develop' into feature/remove-react-fc
This commit is contained in:
@@ -152,6 +152,7 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
const onDrop = (event: React.DragEvent<Element>) => {
|
||||
event.preventDefault();
|
||||
editor.dragDropManager.handlerEditorDrop(event);
|
||||
editor.getHooks().onRootNodeDrop(event);
|
||||
};
|
||||
|
||||
@@ -233,7 +233,8 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
|
||||
startPointRef.current &&
|
||||
endPointRef.current &&
|
||||
scrollManager.scrollContainer &&
|
||||
scrollContainerRect.current
|
||||
scrollContainerRect.current &&
|
||||
mouseType.current === 'down'
|
||||
) {
|
||||
const xSign = DIRECTION_VALUE_MAP[direction[0]] || 0;
|
||||
const ySign = DIRECTION_VALUE_MAP[direction[1]] || 0;
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
Popover,
|
||||
type PopoverProps,
|
||||
PopperHandler,
|
||||
Tag,
|
||||
type PopperProps,
|
||||
} from '@toeverything/components/ui';
|
||||
import { TagsIcon } from '@toeverything/components/icons';
|
||||
|
||||
import { CreatePendantPanel } from './pendant-operation-panel';
|
||||
import { IconButton } from './StyledComponent';
|
||||
import { AsyncBlock } from '../editor';
|
||||
@@ -13,17 +17,21 @@ type Props = {
|
||||
block: AsyncBlock;
|
||||
onSure?: () => void;
|
||||
iconStyle?: CSSProperties;
|
||||
useAddIcon?: boolean;
|
||||
} & Omit<PopoverProps, 'content'>;
|
||||
export const AddPendantPopover = ({
|
||||
block,
|
||||
onSure,
|
||||
iconStyle,
|
||||
useAddIcon = true,
|
||||
...popoverProps
|
||||
}: Props) => {
|
||||
const popoverHandlerRef = useRef<PopperHandler>();
|
||||
const popperRef = useRef<any>();
|
||||
return (
|
||||
<Popover
|
||||
ref={popoverHandlerRef}
|
||||
popperHandlerRef={popoverHandlerRef}
|
||||
popperRef={popperRef}
|
||||
content={
|
||||
<CreatePendantPanel
|
||||
block={block}
|
||||
@@ -31,6 +39,9 @@ export const AddPendantPopover = ({
|
||||
popoverHandlerRef.current?.setVisible(false);
|
||||
onSure?.();
|
||||
}}
|
||||
onTypeChange={() => {
|
||||
popperRef.current?.update?.();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
placement="bottom-start"
|
||||
@@ -38,9 +49,25 @@ export const AddPendantPopover = ({
|
||||
style={{ padding: 0 }}
|
||||
{...popoverProps}
|
||||
>
|
||||
<IconButton style={{ marginRight: 12, ...iconStyle }}>
|
||||
<Add sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
{useAddIcon ? (
|
||||
<IconButton style={{ marginRight: 12, ...iconStyle }}>
|
||||
<Add sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tag
|
||||
style={{
|
||||
background: '#F5F7F8',
|
||||
color: '#98ACBD',
|
||||
marginRight: 12,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
startElement={
|
||||
<TagsIcon style={{ fontSize: 14, marginRight: 4 }} />
|
||||
}
|
||||
>
|
||||
Tag App
|
||||
</Tag>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -98,8 +98,8 @@ export const pendantConfig: { [key: string]: PendantConfig } = {
|
||||
},
|
||||
[PendantTypes.Status]: {
|
||||
iconName: IconNames.STATUS,
|
||||
background: ['#C5FBE0', '#FFF5AB', '#FFCECE', '#E3DEFF'],
|
||||
color: ['#05683D', '#896406', '#AF1212', '#511AAB'],
|
||||
background: ['#FFCECE', '#FFF5AB', '#C5FBE0', '#E3DEFF'],
|
||||
color: ['#AF1212', '#896406', '#05683D', '#511AAB'],
|
||||
},
|
||||
[PendantTypes.Select]: {
|
||||
iconName: IconNames.SINGLE_SELECT,
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ export const PendantHistoryPanel = ({
|
||||
return (
|
||||
<Popover
|
||||
key={item.id}
|
||||
ref={ref => {
|
||||
popperHandlerRef={ref => {
|
||||
popoverHandlerRef.current[item.id] = ref;
|
||||
}}
|
||||
placement="bottom-start"
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ModifyPanelProps = {
|
||||
iconConfig?: PendantConfig;
|
||||
isStatusSelect?: boolean;
|
||||
property?: RecastMetaProperty;
|
||||
onTypeChange?: (type: PendantTypes) => void;
|
||||
};
|
||||
|
||||
export type ModifyPanelContentProps = {
|
||||
|
||||
+6
@@ -24,9 +24,11 @@ import { useOnCreateSure } from './hooks';
|
||||
export const CreatePendantPanel = ({
|
||||
block,
|
||||
onSure,
|
||||
onTypeChange,
|
||||
}: {
|
||||
block: AsyncBlock;
|
||||
onSure?: () => void;
|
||||
onTypeChange?: (option: PendantOptions) => void;
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] = useState<PendantOptions>();
|
||||
const [fieldName, setFieldName] = useState<string>('');
|
||||
@@ -37,6 +39,10 @@ export const CreatePendantPanel = ({
|
||||
setFieldName(generateRandomFieldName(selectedOption.type));
|
||||
}, [selectedOption]);
|
||||
|
||||
useEffect(() => {
|
||||
onTypeChange?.(selectedOption);
|
||||
}, [selectedOption, onTypeChange]);
|
||||
|
||||
return (
|
||||
<StyledPopoverWrapper>
|
||||
<StyledOperationTitle>Add Field</StyledOperationTitle>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PendantPopover = (
|
||||
const popoverHandlerRef = useRef<PopperHandler>();
|
||||
return (
|
||||
<Popover
|
||||
ref={popoverHandlerRef}
|
||||
popperHandlerRef={popoverHandlerRef}
|
||||
pointerEnterDelay={300}
|
||||
pointerLeaveDelay={200}
|
||||
placement="bottom-start"
|
||||
@@ -26,13 +26,13 @@ export const PendantPopover = (
|
||||
block={block}
|
||||
endElement={
|
||||
<AddPendantPopover
|
||||
container={popoverProps.container}
|
||||
block={block}
|
||||
onSure={() => {
|
||||
popoverHandlerRef.current?.setVisible(false);
|
||||
}}
|
||||
offset={[0, -30]}
|
||||
trigger="click"
|
||||
useAddIcon={false}
|
||||
/>
|
||||
}
|
||||
onClose={() => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
ref={ref => {
|
||||
popperHandlerRef={ref => {
|
||||
popoverHandlerRef.current[id] = ref;
|
||||
}}
|
||||
container={blockRenderContainerRef.current}
|
||||
@@ -107,7 +107,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
|
||||
iconStyle={{ marginTop: 4 }}
|
||||
trigger="click"
|
||||
// trigger={isKanbanView ? 'hover' : 'click'}
|
||||
container={blockRenderContainerRef.current}
|
||||
// container={blockRenderContainerRef.current}
|
||||
/>
|
||||
</div>
|
||||
</MuiFade>
|
||||
|
||||
@@ -88,7 +88,7 @@ export const generateInitialOptions = (
|
||||
) => {
|
||||
if (type === PendantTypes.Status) {
|
||||
return [
|
||||
generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
|
||||
generateBasicOption({ index: 0, iconConfig, name: 'Not Started' }),
|
||||
generateBasicOption({
|
||||
index: 1,
|
||||
iconConfig,
|
||||
|
||||
@@ -3,7 +3,13 @@ import type {
|
||||
SlateUtils,
|
||||
TextAlignOptions,
|
||||
} from '@toeverything/components/common';
|
||||
import { Point, Selection as SlateSelection } from 'slate';
|
||||
import {
|
||||
BaseRange,
|
||||
Node,
|
||||
Path,
|
||||
Point,
|
||||
Selection as SlateSelection,
|
||||
} from 'slate';
|
||||
import { Editor } from '../editor';
|
||||
|
||||
type TextUtilsFunctions =
|
||||
@@ -28,7 +34,10 @@ type TextUtilsFunctions =
|
||||
| 'removeSelection'
|
||||
| 'insertReference'
|
||||
| 'isCollapsed'
|
||||
| 'blur';
|
||||
| 'blur'
|
||||
| 'setSelection'
|
||||
| 'insertNodes'
|
||||
| 'getNodeByPath';
|
||||
|
||||
type ExtendedTextUtils = SlateUtils & {
|
||||
setLinkModalVisible: (visible: boolean) => void;
|
||||
@@ -193,6 +202,59 @@ export class BlockHelper {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* set selection of a text input
|
||||
* @param {string} blockId
|
||||
* @param {BaseRange} selection
|
||||
* @return {*}
|
||||
* @memberof BlockHelper
|
||||
*/
|
||||
public setSelection(blockId: string, selection: BaseRange) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
return text_utils.setSelection(selection);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* insert nodes in text
|
||||
* @param {string} blockId
|
||||
* @param {Array<Node>} nodes
|
||||
* @param {Parameters<TextUtils['insertNodes']>[1]} options
|
||||
* @return {*}
|
||||
* @memberof BlockHelper
|
||||
*/
|
||||
public insertNodes(
|
||||
blockId: string,
|
||||
nodes: Array<Node>,
|
||||
options?: Parameters<TextUtils['insertNodes']>[1]
|
||||
) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
return text_utils.insertNodes(nodes, options);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get text(slate node) by path
|
||||
* @param {string} blockId
|
||||
* @param {Path} path
|
||||
* @return {*}
|
||||
* @memberof BlockHelper
|
||||
*/
|
||||
public getNodeByPath(blockId: string, path: Path) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
return text_utils.getNodeByPath(path);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
|
||||
public transformPoint(
|
||||
blockId: string,
|
||||
...restArgs: Parameters<TextUtils['transformPoint']>
|
||||
|
||||
@@ -14,403 +14,126 @@ import {
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
import { Paste } from './paste';
|
||||
// todo needs to be a switch
|
||||
const support_markdown_paste = true;
|
||||
|
||||
enum ClipboardAction {
|
||||
COPY = 'copy',
|
||||
CUT = 'cut',
|
||||
PASTE = 'paste',
|
||||
}
|
||||
class BrowserClipboard {
|
||||
private event_target: Element;
|
||||
private hooks: HooksRunner;
|
||||
private editor: Editor;
|
||||
private clipboard_parse: ClipboardParse;
|
||||
private markdown_parse: MarkdownParser;
|
||||
|
||||
private static optimal_mime_type: string[] = [
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
||||
];
|
||||
//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.event_target = eventTarget;
|
||||
this.hooks = hooks;
|
||||
this.editor = editor;
|
||||
this.clipboard_parse = new ClipboardParse(editor);
|
||||
this.markdown_parse = new MarkdownParser();
|
||||
this.initialize();
|
||||
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.clipboard_parse;
|
||||
return this._clipboardParse;
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.handle_copy = this.handle_copy.bind(this);
|
||||
this.handle_cut = this.handle_cut.bind(this);
|
||||
this.handle_paste = this.handle_paste.bind(this);
|
||||
private _initialize() {
|
||||
this._handleCopy = this._handleCopy.bind(this);
|
||||
this._handleCut = this._handleCut.bind(this);
|
||||
|
||||
document.addEventListener(ClipboardAction.COPY, this.handle_copy);
|
||||
document.addEventListener(ClipboardAction.CUT, this.handle_cut);
|
||||
document.addEventListener(ClipboardAction.PASTE, this.handle_paste);
|
||||
this.event_target.addEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this.handle_copy
|
||||
);
|
||||
this.event_target.addEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this.handle_cut
|
||||
);
|
||||
this.event_target.addEventListener(
|
||||
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this.handle_paste
|
||||
this._paste.handlePaste
|
||||
);
|
||||
}
|
||||
|
||||
private handle_copy(e: Event) {
|
||||
//@ts-ignore
|
||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatch_clipboard_event(
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.COPY,
|
||||
e as ClipboardEvent
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
}
|
||||
|
||||
private handle_cut(e: Event) {
|
||||
//@ts-ignore
|
||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
this.dispatch_clipboard_event(ClipboardAction.CUT, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private handle_paste(e: Event) {
|
||||
//@ts-ignore TODO should be handled more scientifically here, whether to trigger the paste time, also need some whitelist mechanism
|
||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
||||
private _handleCopy(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||
|
||||
const isPureFile = this.is_pure_file_in_clipboard(clipboardData);
|
||||
|
||||
if (!isPureFile) {
|
||||
this.paste_content(clipboardData);
|
||||
} else {
|
||||
this.paste_file(clipboardData);
|
||||
}
|
||||
// this.editor.selectionManager
|
||||
// .getSelectInfo()
|
||||
// .then(selectionInfo => console.log(selectionInfo));
|
||||
|
||||
e.stopPropagation();
|
||||
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private paste_content(clipboardData: any) {
|
||||
const originClip: { data: any; type: any } = this.getOptimalClip(
|
||||
clipboardData
|
||||
) as { data: any; type: any };
|
||||
const originTextClipData = clipboardData.getData(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
||||
);
|
||||
|
||||
let clipData = originClip['data'];
|
||||
|
||||
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
||||
clipData = this.excape_html(clipData);
|
||||
}
|
||||
|
||||
switch (originClip['type']) {
|
||||
/** Protocol paste */
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
|
||||
this.fire_paste_edit_action(clipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
|
||||
this.paste_html(clipData, originTextClipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
|
||||
this.paste_text(clipData, originTextClipData);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private paste_html(clipData: any, originTextClipData: any) {
|
||||
if (support_markdown_paste) {
|
||||
const has_markdown =
|
||||
this.markdown_parse.checkIfTextContainsMd(originTextClipData);
|
||||
if (has_markdown) {
|
||||
try {
|
||||
const convertedDataObj =
|
||||
this.markdown_parse.md2Html(originTextClipData);
|
||||
if (convertedDataObj.isConverted) {
|
||||
clipData = convertedDataObj.text;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
clipData = originTextClipData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = this.clipboard_parse.html2blocks(clipData);
|
||||
this.insert_blocks(blocks);
|
||||
}
|
||||
|
||||
private paste_text(clipData: any, originTextClipData: any) {
|
||||
const blocks = this.clipboard_parse.text2blocks(clipData);
|
||||
this.insert_blocks(blocks);
|
||||
}
|
||||
|
||||
private async paste_file(clipboardData: any) {
|
||||
const file = this.get_image_file(clipboardData);
|
||||
if (file) {
|
||||
const result = await services.api.file.create({
|
||||
workspace: this.editor.workspace,
|
||||
file: file,
|
||||
});
|
||||
const block_info: ClipBlockInfo = {
|
||||
type: 'image',
|
||||
properties: {
|
||||
image: {
|
||||
value: result.id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
},
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
this.insert_blocks([block_info]);
|
||||
}
|
||||
}
|
||||
|
||||
private get_image_file(clipboardData: any) {
|
||||
const files = clipboardData.files;
|
||||
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
||||
return files[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private excape_html(data: any, onlySpace?: any) {
|
||||
if (!onlySpace) {
|
||||
// TODO:
|
||||
// data = string.htmlEscape(data);
|
||||
// data = data.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// data = data.replace(/ /g, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
return data;
|
||||
}
|
||||
|
||||
public getOptimalClip(clipboardData: any) {
|
||||
const mimeTypeArr = BrowserClipboard.optimal_mime_type;
|
||||
|
||||
for (let i = 0; i < mimeTypeArr.length; i++) {
|
||||
const data =
|
||||
clipboardData[mimeTypeArr[i]] ||
|
||||
clipboardData.getData(mimeTypeArr[i]);
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
type: mimeTypeArr[i],
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private is_pure_file_in_clipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
|
||||
const res =
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private async fire_paste_edit_action(clipboardData: any) {
|
||||
const clip_info: InnerClipInfo = JSON.parse(clipboardData);
|
||||
clip_info && this.insert_blocks(clip_info.data, clip_info.select);
|
||||
}
|
||||
|
||||
private can_edit_text(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
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: cursor positioning problem
|
||||
private async insert_blocks(blocks: ClipBlockInfo[], select?: SelectInfo) {
|
||||
if (blocks.length === 0) {
|
||||
private _handleCut(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cur_select_info =
|
||||
await this.editor.selectionManager.getSelectInfo();
|
||||
if (cur_select_info.blocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let begin_index = 0;
|
||||
const cur_node_id =
|
||||
cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId;
|
||||
let cur_block = await this.editor.getBlockById(cur_node_id);
|
||||
const block_view = this.editor.getView(cur_block.type);
|
||||
if (
|
||||
cur_select_info.type === 'Range' &&
|
||||
cur_block.type === 'text' &&
|
||||
block_view.isEmpty(cur_block)
|
||||
) {
|
||||
cur_block.setType(blocks[0].type);
|
||||
cur_block.setProperties(blocks[0].properties);
|
||||
this.paste_children(cur_block, blocks[0].children);
|
||||
begin_index = 1;
|
||||
} else if (
|
||||
select?.type === 'Range' &&
|
||||
cur_select_info.type === 'Range' &&
|
||||
this.can_edit_text(cur_block.type) &&
|
||||
this.can_edit_text(blocks[0].type)
|
||||
) {
|
||||
if (
|
||||
cur_select_info.blocks.length > 0 &&
|
||||
cur_select_info.blocks[0].startInfo
|
||||
) {
|
||||
const start_info = cur_select_info.blocks[0].startInfo;
|
||||
const end_info = cur_select_info.blocks[0].endInfo;
|
||||
const cur_text_value = cur_block.getProperty('text').value;
|
||||
const pre_cur_text_value = cur_text_value.slice(
|
||||
0,
|
||||
start_info.arrayIndex
|
||||
);
|
||||
const last_cur_text_value = cur_text_value.slice(
|
||||
end_info.arrayIndex + 1
|
||||
);
|
||||
const pre_text = cur_text_value[
|
||||
start_info.arrayIndex
|
||||
].text.substring(0, start_info.offset);
|
||||
const last_text = cur_text_value[
|
||||
end_info.arrayIndex
|
||||
].text.substring(end_info.offset);
|
||||
|
||||
let last_block: ClipBlockInfo = blocks[blocks.length - 1];
|
||||
if (!this.can_edit_text(last_block.type)) {
|
||||
last_block = { type: 'text', children: [] };
|
||||
blocks.push(last_block);
|
||||
}
|
||||
const last_values = last_block.properties?.text?.value;
|
||||
last_text && last_values.push({ text: last_text });
|
||||
last_values.push(...last_cur_text_value);
|
||||
last_block.properties = {
|
||||
text: { value: last_values },
|
||||
};
|
||||
|
||||
const insert_info = blocks[0].properties.text;
|
||||
pre_text && pre_cur_text_value.push({ text: pre_text });
|
||||
pre_cur_text_value.push(...insert_info.value);
|
||||
this.editor.blockHelper.setBlockBlur(cur_node_id);
|
||||
setTimeout(async () => {
|
||||
const cur_block = await this.editor.getBlockById(
|
||||
cur_node_id
|
||||
);
|
||||
cur_block.setProperties({
|
||||
text: { value: pre_cur_text_value },
|
||||
});
|
||||
this.paste_children(cur_block, blocks[0].children);
|
||||
}, 0);
|
||||
begin_index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = begin_index; i < blocks.length; i++) {
|
||||
const next_block = await this.editor.createBlock(blocks[i].type);
|
||||
next_block.setProperties(blocks[i].properties);
|
||||
if (cur_block.type === 'page') {
|
||||
cur_block.prepend(next_block);
|
||||
} else {
|
||||
cur_block.after(next_block);
|
||||
}
|
||||
|
||||
this.paste_children(next_block, blocks[i].children);
|
||||
cur_block = next_block;
|
||||
}
|
||||
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private async paste_children(parent: AsyncBlock, children: any[]) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const next_block = await this.editor.createBlock(children[i].type);
|
||||
next_block.setProperties(children[i].properties);
|
||||
parent.append(next_block);
|
||||
await this.paste_children(next_block, children[i].children);
|
||||
}
|
||||
}
|
||||
|
||||
private pre_copy_cut(action: ClipboardAction, e: ClipboardEvent) {
|
||||
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||
switch (action) {
|
||||
case ClipboardAction.COPY:
|
||||
this.hooks.beforeCopy(e);
|
||||
this._hooks.beforeCopy(e);
|
||||
break;
|
||||
|
||||
case ClipboardAction.CUT:
|
||||
this.hooks.beforeCut(e);
|
||||
this._hooks.beforeCut(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private dispatch_clipboard_event(
|
||||
private _dispatchClipboardEvent(
|
||||
action: ClipboardAction,
|
||||
e: ClipboardEvent
|
||||
) {
|
||||
this.pre_copy_cut(action, e);
|
||||
this._preCopyCut(action, e);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
document.removeEventListener(ClipboardAction.COPY, this.handle_copy);
|
||||
document.removeEventListener(ClipboardAction.CUT, this.handle_cut);
|
||||
document.removeEventListener(ClipboardAction.PASTE, this.handle_paste);
|
||||
this.event_target.removeEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this.handle_copy
|
||||
);
|
||||
this.event_target.removeEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this.handle_cut
|
||||
);
|
||||
this.event_target.removeEventListener(
|
||||
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this.handle_paste
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this.clipboard_parse.dispose();
|
||||
this.clipboard_parse = null;
|
||||
this.event_target = null;
|
||||
this.hooks = null;
|
||||
this.editor = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,10 +115,8 @@ export default class ClipboardParse {
|
||||
const block_utils = this.editor.getView(
|
||||
ClipboardParse.block_types[i]
|
||||
);
|
||||
const blocks =
|
||||
block_utils &&
|
||||
block_utils.html2block &&
|
||||
block_utils.html2block(el, this.parse_dom);
|
||||
const blocks = block_utils?.html2block?.(el, this.parse_dom);
|
||||
|
||||
if (blocks) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ClipboardPopulator {
|
||||
}
|
||||
|
||||
// TODO: is not compatible with safari
|
||||
const success = this.copy_to_cliboard_from_pc(clips);
|
||||
const success = this._copyToClipboardFromPc(clips);
|
||||
if (!success) {
|
||||
// This way, not compatible with firefox
|
||||
const clipboardData = e.clipboardData;
|
||||
@@ -65,7 +65,7 @@ class ClipboardPopulator {
|
||||
}
|
||||
};
|
||||
|
||||
private copy_to_cliboard_from_pc(clips: any[]) {
|
||||
private _copyToClipboardFromPc(clips: any[]) {
|
||||
let success = false;
|
||||
const tempElem = document.createElement('textarea');
|
||||
tempElem.value = 'temp';
|
||||
@@ -96,56 +96,55 @@ class ClipboardPopulator {
|
||||
return success;
|
||||
}
|
||||
|
||||
private async get_clip_block_info(selBlock: SelectBlock) {
|
||||
private async _getClipBlockInfo(selBlock: SelectBlock) {
|
||||
const block = await this._editor.getBlockById(selBlock.blockId);
|
||||
const block_view = this._editor.getView(block.type);
|
||||
assert(block_view);
|
||||
const block_info: ClipBlockInfo = {
|
||||
const blockView = this._editor.getView(block.type);
|
||||
assert(blockView);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block.type,
|
||||
properties: block_view.getSelProperties(block, selBlock),
|
||||
properties: blockView.getSelProperties(block, selBlock),
|
||||
children: [] as any[],
|
||||
};
|
||||
|
||||
for (let i = 0; i < selBlock.children.length; i++) {
|
||||
const child_info = await this.get_clip_block_info(
|
||||
const childInfo = await this._getClipBlockInfo(
|
||||
selBlock.children[i]
|
||||
);
|
||||
block_info.children.push(child_info);
|
||||
blockInfo.children.push(childInfo);
|
||||
}
|
||||
|
||||
return block_info;
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
private async get_inner_clip(): Promise<InnerClipInfo> {
|
||||
private async _getInnerClip(): Promise<InnerClipInfo> {
|
||||
const clips: ClipBlockInfo[] = [];
|
||||
const select_info: SelectInfo =
|
||||
const selectInfo: SelectInfo =
|
||||
await this._selectionManager.getSelectInfo();
|
||||
for (let i = 0; i < select_info.blocks.length; i++) {
|
||||
const sel_block = select_info.blocks[i];
|
||||
const clip_block_info = await this.get_clip_block_info(sel_block);
|
||||
clips.push(clip_block_info);
|
||||
for (let i = 0; i < selectInfo.blocks.length; i++) {
|
||||
const selBlock = selectInfo.blocks[i];
|
||||
const clipBlockInfo = await this._getClipBlockInfo(selBlock);
|
||||
clips.push(clipBlockInfo);
|
||||
}
|
||||
const clipInfo: InnerClipInfo = {
|
||||
select: select_info,
|
||||
return {
|
||||
select: selectInfo,
|
||||
data: clips,
|
||||
};
|
||||
return clipInfo;
|
||||
}
|
||||
|
||||
async getClips() {
|
||||
const clips: any[] = [];
|
||||
|
||||
const inner_clip = await this.get_inner_clip();
|
||||
const innerClip = await this._getInnerClip();
|
||||
clips.push(
|
||||
new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify(inner_clip)
|
||||
JSON.stringify(innerClip)
|
||||
)
|
||||
);
|
||||
|
||||
const html_clip = await this._clipboardParse.generateHtml();
|
||||
html_clip &&
|
||||
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip));
|
||||
const htmlClip = await this._clipboardParse.generateHtml();
|
||||
htmlClip &&
|
||||
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip));
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
/* eslint-disable max-lines */
|
||||
import {
|
||||
OFFICE_CLIPBOARD_MIMETYPE,
|
||||
InnerClipInfo,
|
||||
ClipBlockInfo,
|
||||
} from './types';
|
||||
import { Editor } from '../editor';
|
||||
import { AsyncBlock } from '../block';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { SelectInfo } from '../selection';
|
||||
import {
|
||||
Protocol,
|
||||
BlockFlavorKeys,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
const SUPPORT_MARKDOWN_PASTE = true;
|
||||
|
||||
type TextValueItem = {
|
||||
text: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
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;
|
||||
this._editor = editor;
|
||||
this.handlePaste = this.handlePaste.bind(this);
|
||||
}
|
||||
private static _optimalMimeType: 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;
|
||||
}
|
||||
e.stopPropagation();
|
||||
|
||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||
|
||||
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||
if (isPureFile) {
|
||||
this._pasteFile(clipboardData);
|
||||
} else {
|
||||
this._pasteContent(clipboardData);
|
||||
}
|
||||
}
|
||||
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]);
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
type: mimeTypeArr[i],
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _pasteContent(clipboardData: any) {
|
||||
const originClip: { data: any; type: any } = this.getOptimalClip(
|
||||
clipboardData
|
||||
) as { data: any; type: any };
|
||||
|
||||
const originTextClipData = clipboardData.getData(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
||||
);
|
||||
|
||||
let clipData = originClip['data'];
|
||||
|
||||
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
||||
clipData = Paste._excapeHtml(clipData);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
private async _firePasteEditAction(clipboardData: any) {
|
||||
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
||||
clipInfo && this._insertBlocks(clipInfo.data, clipInfo.select);
|
||||
}
|
||||
private async _pasteFile(clipboardData: any) {
|
||||
const file = Paste._getImageFile(clipboardData);
|
||||
if (file) {
|
||||
const result = await services.api.file.create({
|
||||
workspace: this._editor.workspace,
|
||||
file: file,
|
||||
});
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: 'image',
|
||||
properties: {
|
||||
image: {
|
||||
value: result.id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
},
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
await this._insertBlocks([blockInfo]);
|
||||
}
|
||||
}
|
||||
private static _isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
if (blocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentSelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
// 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') {
|
||||
// 当 currentSelectInfo.type === 'Range' 时,光标选中的block必然只有一个
|
||||
const selectedBlock = await this._editor.getBlockById(
|
||||
currentSelectInfo.blocks[0].blockId
|
||||
);
|
||||
const isSelectedBlockEdit = Paste._isTextEditBlock(
|
||||
selectedBlock.type
|
||||
);
|
||||
if (isSelectedBlockEdit) {
|
||||
const shouldSplitBlock =
|
||||
blocks.length > 1 ||
|
||||
!Paste._isTextEditBlock(blocks[0].type);
|
||||
const pureText = !shouldSplitBlock
|
||||
? blocks[0].properties.text.value
|
||||
: [{ text: '' }];
|
||||
const { startInfo, endInfo } = currentSelectInfo.blocks[0];
|
||||
|
||||
// Text content of the selected current editable block
|
||||
const currentTextValue =
|
||||
selectedBlock.getProperty('text').value;
|
||||
// When the cursor selection spans different styles of text
|
||||
if (startInfo?.arrayIndex !== endInfo?.arrayIndex) {
|
||||
if (shouldSplitBlock) {
|
||||
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
|
||||
const newTextValue = currentTextValue.reduce(
|
||||
(
|
||||
newTextValue: TextValueItem[],
|
||||
textStore: TextValueItem,
|
||||
i: number
|
||||
) => {
|
||||
if (i < startInfo?.arrayIndex) {
|
||||
newTextValue.push(textStore);
|
||||
}
|
||||
const { text, ...props } = textStore;
|
||||
|
||||
if (i === startInfo?.arrayIndex) {
|
||||
newTextValue.push({
|
||||
text: text.slice(0, startInfo?.offset),
|
||||
...props,
|
||||
});
|
||||
}
|
||||
return newTextValue;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const nextTextValue = currentTextValue.reduce(
|
||||
(
|
||||
newTextValue: TextValueItem[],
|
||||
textStore: TextValueItem,
|
||||
i: number
|
||||
) => {
|
||||
if (i > endInfo?.arrayIndex) {
|
||||
newTextValue.push(textStore);
|
||||
}
|
||||
const { text, ...props } = textStore;
|
||||
|
||||
if (i === endInfo?.arrayIndex) {
|
||||
newTextValue.push({
|
||||
text: text.slice(endInfo?.offset),
|
||||
...props,
|
||||
});
|
||||
}
|
||||
return newTextValue;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
selectedBlock.setProperties({
|
||||
text: {
|
||||
value: newTextValue,
|
||||
},
|
||||
});
|
||||
const pasteBlocks = await this._createBlocks(blocks);
|
||||
pasteBlocks.forEach(block => {
|
||||
selectedBlock.after(block);
|
||||
});
|
||||
const nextBlock = await this._editor.createBlock(
|
||||
selectedBlock?.type
|
||||
);
|
||||
nextBlock.setProperties({
|
||||
text: {
|
||||
value: nextTextValue,
|
||||
},
|
||||
});
|
||||
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
|
||||
|
||||
this._setEndSelectToBlock(
|
||||
pasteBlocks[pasteBlocks.length - 1].id
|
||||
);
|
||||
} else {
|
||||
this._editor.blockHelper.insertNodes(
|
||||
selectedBlock.id,
|
||||
pureText,
|
||||
{ select: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
// When the cursor selection does not span different styles of text
|
||||
if (startInfo?.arrayIndex === endInfo?.arrayIndex) {
|
||||
if (shouldSplitBlock) {
|
||||
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
|
||||
const newTextValue = currentTextValue.reduce(
|
||||
(
|
||||
newTextValue: TextValueItem[],
|
||||
textStore: TextValueItem,
|
||||
i: number
|
||||
) => {
|
||||
if (i < startInfo?.arrayIndex) {
|
||||
newTextValue.push(textStore);
|
||||
}
|
||||
const { text, ...props } = textStore;
|
||||
|
||||
if (i === startInfo?.arrayIndex) {
|
||||
newTextValue.push({
|
||||
text: `${text.slice(
|
||||
0,
|
||||
startInfo?.offset
|
||||
)}`,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
return newTextValue;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const nextTextValue = currentTextValue.reduce(
|
||||
(
|
||||
nextTextValue: TextValueItem[],
|
||||
textStore: TextValueItem,
|
||||
i: number
|
||||
) => {
|
||||
if (i > endInfo?.arrayIndex) {
|
||||
nextTextValue.push(textStore);
|
||||
}
|
||||
const { text, ...props } = textStore;
|
||||
|
||||
if (i === endInfo?.arrayIndex) {
|
||||
nextTextValue.push({
|
||||
text: `${text.slice(endInfo?.offset)}`,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
return nextTextValue;
|
||||
},
|
||||
[]
|
||||
);
|
||||
selectedBlock.setProperties({
|
||||
text: {
|
||||
value: newTextValue,
|
||||
},
|
||||
});
|
||||
const pasteBlocks = await this._createBlocks(blocks);
|
||||
pasteBlocks.forEach((block: AsyncBlock) => {
|
||||
selectedBlock.after(block);
|
||||
});
|
||||
const nextBlock = await this._editor.createBlock(
|
||||
selectedBlock?.type
|
||||
);
|
||||
nextBlock.setProperties({
|
||||
text: {
|
||||
value: nextTextValue,
|
||||
},
|
||||
});
|
||||
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
|
||||
|
||||
this._setEndSelectToBlock(
|
||||
pasteBlocks[pasteBlocks.length - 1].id
|
||||
);
|
||||
} else {
|
||||
this._editor.blockHelper.insertNodes(
|
||||
selectedBlock.id,
|
||||
pureText,
|
||||
{ select: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pasteBlocks = await this._createBlocks(blocks);
|
||||
pasteBlocks.forEach(block => {
|
||||
selectedBlock.after(block);
|
||||
});
|
||||
this._setEndSelectToBlock(
|
||||
pasteBlocks[pasteBlocks.length - 1].id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSelectInfo.type === 'Block') {
|
||||
const selectedBlock = await this._editor.getBlockById(
|
||||
currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1]
|
||||
.blockId
|
||||
);
|
||||
const pasteBlocks = await this._createBlocks(blocks);
|
||||
|
||||
let groupBlock: AsyncBlock;
|
||||
if (
|
||||
selectedBlock?.type === 'group' ||
|
||||
selectedBlock?.type === 'page'
|
||||
) {
|
||||
groupBlock = await this._editor.createBlock('group');
|
||||
pasteBlocks.forEach(block => {
|
||||
groupBlock.append(block);
|
||||
});
|
||||
await selectedBlock.after(groupBlock);
|
||||
} else {
|
||||
pasteBlocks.forEach(block => {
|
||||
selectedBlock.after(block);
|
||||
});
|
||||
}
|
||||
this._setEndSelectToBlock(pasteBlocks[pasteBlocks.length - 1].id);
|
||||
}
|
||||
}
|
||||
|
||||
private _setEndSelectToBlock(blockId: string) {
|
||||
setTimeout(() => {
|
||||
this._editor.selectionManager.activeNodeByNodeId(blockId, 'end');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private async _createBlocks(blocks: ClipBlockInfo[], parentId?: string) {
|
||||
return Promise.all(
|
||||
blocks.map(async clipBlockInfo => {
|
||||
const block = await this._editor.createBlock(
|
||||
clipBlockInfo.type
|
||||
);
|
||||
block?.setProperties(clipBlockInfo.properties);
|
||||
await this._createBlocks(clipBlockInfo.children, block?.id);
|
||||
return block;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return files[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private static _excapeHtml(data: any, onlySpace?: any) {
|
||||
if (!onlySpace) {
|
||||
// TODO:
|
||||
// data = string.htmlEscape(data);
|
||||
// data = data.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// data = data.replace(/ /g, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor } from '../editor';
|
||||
|
||||
export const shouldHandlerContinue = (event: Event, editor: Editor) => {
|
||||
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.selectionManager.currentSelectInfo.type !== 'None';
|
||||
};
|
||||
@@ -32,11 +32,11 @@ export class BlockCommands {
|
||||
) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
if (block) {
|
||||
const next_block = await this._editor.createBlock(type);
|
||||
if (next_block) {
|
||||
await block.after(next_block);
|
||||
this._editor.selectionManager.activeNodeByNodeId(next_block.id);
|
||||
return next_block;
|
||||
const nextBlock = await this._editor.createBlock(type);
|
||||
if (nextBlock) {
|
||||
await block.after(nextBlock);
|
||||
this._editor.selectionManager.activeNodeByNodeId(nextBlock.id);
|
||||
return nextBlock;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -101,73 +101,70 @@ export class DragDropManager {
|
||||
if (this._blockDragDirection !== BlockDropPlacement.none) {
|
||||
const blockId = event.dataTransfer.getData(this._blockIdKey);
|
||||
if (!(await this._canBeDrop(event))) return;
|
||||
if (this._blockDragDirection === BlockDropPlacement.bottom) {
|
||||
this._editor.commands.blockCommands.moveBlockAfter(
|
||||
blockId,
|
||||
this._blockDragTargetId
|
||||
);
|
||||
}
|
||||
if (
|
||||
[BlockDropPlacement.left, BlockDropPlacement.right].includes(
|
||||
this._blockDragDirection
|
||||
)
|
||||
) {
|
||||
const dropType =
|
||||
this._blockDragDirection === BlockDropPlacement.left
|
||||
? GridDropType.left
|
||||
: GridDropType.right;
|
||||
// if target is a grid item create grid item
|
||||
if (targetBlock.type !== Protocol.Block.Type.gridItem) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
switch (this._blockDragDirection) {
|
||||
case BlockDropPlacement.bottom: {
|
||||
this._editor.commands.blockCommands.moveBlockAfter(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
);
|
||||
} else {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
this._blockDragTargetId
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
[
|
||||
BlockDropPlacement.outerLeft,
|
||||
BlockDropPlacement.outerRight,
|
||||
].includes(this._blockDragDirection)
|
||||
) {
|
||||
if (targetBlock.type !== Protocol.Block.Type.grid) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
this._blockDragDirection ===
|
||||
BlockDropPlacement.outerLeft
|
||||
case BlockDropPlacement.left:
|
||||
case BlockDropPlacement.right: {
|
||||
const dropType =
|
||||
this._blockDragDirection === BlockDropPlacement.left
|
||||
? GridDropType.left
|
||||
: GridDropType.right
|
||||
);
|
||||
}
|
||||
if (targetBlock.type === Protocol.Block.Type.grid) {
|
||||
const gridItems = await targetBlock.children();
|
||||
if (
|
||||
BlockDropPlacement.outerRight ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
: GridDropType.right;
|
||||
// if target is a grid item create grid item
|
||||
if (targetBlock.type !== Protocol.Block.Type.gridItem) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
);
|
||||
} else {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[gridItems.length - 1].id
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
);
|
||||
}
|
||||
if (
|
||||
BlockDropPlacement.outerLeft ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
break;
|
||||
}
|
||||
case BlockDropPlacement.outerLeft:
|
||||
case BlockDropPlacement.outerRight: {
|
||||
if (targetBlock.type !== Protocol.Block.Type.grid) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
gridItems[0].id,
|
||||
GridDropType.right
|
||||
this._blockDragTargetId,
|
||||
this._blockDragDirection ===
|
||||
BlockDropPlacement.outerLeft
|
||||
? GridDropType.left
|
||||
: GridDropType.right
|
||||
);
|
||||
}
|
||||
if (targetBlock.type === Protocol.Block.Type.grid) {
|
||||
const gridItems = await targetBlock.children();
|
||||
if (
|
||||
BlockDropPlacement.outerRight ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[gridItems.length - 1].id
|
||||
);
|
||||
}
|
||||
if (
|
||||
BlockDropPlacement.outerLeft ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[0].id,
|
||||
GridDropType.right
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,7 +421,6 @@ export class DragDropManager {
|
||||
}
|
||||
|
||||
public handlerEditorDrop(event: React.DragEvent<Element>) {
|
||||
event.preventDefault();
|
||||
// IMP: can not use Decorators now may use decorators is right
|
||||
if (this.isEnabled()) {
|
||||
if (this.isDragBlock(event)) {
|
||||
|
||||
@@ -207,6 +207,7 @@ 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) {
|
||||
return -1;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from './types';
|
||||
import { isLikeBlockListIds } from './utils';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { Editor } from 'slate';
|
||||
// IMP: maybe merge active and select into single function
|
||||
|
||||
export type SelectionInfo = InstanceType<
|
||||
@@ -47,7 +48,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
private _scrollDelay = 150;
|
||||
private _selectEndDelayTime = 500;
|
||||
private _hasEmitEndPending = false;
|
||||
|
||||
private _scrollTimer: number | null = null;
|
||||
/**
|
||||
*
|
||||
* the selection info before current
|
||||
@@ -77,9 +78,6 @@ export class SelectionManager implements VirgoSelection {
|
||||
type: 'None',
|
||||
info: null,
|
||||
};
|
||||
// IMP: to delete
|
||||
// @ts-ignore
|
||||
window['selectionManager'] = this;
|
||||
this._initWindowSelectionChangeListen();
|
||||
}
|
||||
|
||||
@@ -683,6 +681,9 @@ export class SelectionManager implements VirgoSelection {
|
||||
*/
|
||||
public async activeNodeByNodeId(nodeId: string, position?: CursorTypes) {
|
||||
try {
|
||||
if (this._scrollTimer) {
|
||||
clearTimeout(this._scrollTimer);
|
||||
}
|
||||
const node = await this._editor.getBlockById(nodeId);
|
||||
if (node) {
|
||||
this._activatedNodeId = nodeId;
|
||||
@@ -691,8 +692,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
this.emit(nodeId, SelectEventTypes.active, this.lastPoint);
|
||||
// TODO: Optimize the related logic after implementing the scroll bar
|
||||
setTimeout(() => {
|
||||
// this._editor.scrollManager.keepBlockInView(node);
|
||||
this._scrollTimer = window.setTimeout(() => {
|
||||
this._editor.scrollManager.keepBlockInView(node);
|
||||
}, this._scrollDelay);
|
||||
} else {
|
||||
console.warn('Can not find node by this id');
|
||||
@@ -1050,4 +1051,25 @@ export class SelectionManager implements VirgoSelection {
|
||||
this._windowSelectionChangeHandler
|
||||
);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* move active selection to the new position
|
||||
* @param {number} index
|
||||
* @param {string} blockId
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
public async moveCursor(
|
||||
nowRange: any,
|
||||
index: number,
|
||||
blockId: string
|
||||
): Promise<void> {
|
||||
let preRang = document.createRange();
|
||||
preRang.setStart(nowRange.startContainer, index);
|
||||
preRang.setEnd(nowRange.endContainer, index);
|
||||
let prePosition = preRang.getClientRects().item(0);
|
||||
this.activeNodeByNodeId(
|
||||
blockId,
|
||||
new Point(prePosition.left, prePosition.bottom)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { noop, Point } from '@toeverything/utils';
|
||||
import { Point } from '@toeverything/utils';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEditor } from './Contexts';
|
||||
import {
|
||||
@@ -62,7 +62,7 @@ export const useBlock = (blockId: string) => {
|
||||
return undefined;
|
||||
}
|
||||
let valid = true;
|
||||
let offUpdate = noop;
|
||||
let offUpdate: () => void | undefined = undefined;
|
||||
editor.getBlockById(blockId).then(node => {
|
||||
if (!valid) {
|
||||
return;
|
||||
@@ -79,7 +79,7 @@ export const useBlock = (blockId: string) => {
|
||||
|
||||
return () => {
|
||||
valid = false;
|
||||
offUpdate();
|
||||
offUpdate?.();
|
||||
};
|
||||
}, [blockId, editor, requestReRender]);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
// import { RenderChildren } from './RenderChildren';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { useBlock } from '../hooks';
|
||||
|
||||
@@ -10,19 +9,21 @@ interface RenderBlockProps {
|
||||
hasContainer?: boolean;
|
||||
}
|
||||
|
||||
export const RenderBlock = ({
|
||||
export function RenderBlock({
|
||||
blockId,
|
||||
hasContainer = true,
|
||||
}: RenderBlockProps) => {
|
||||
}: RenderBlockProps) {
|
||||
const { editor, editorElement } = useEditor();
|
||||
const { block } = useBlock(blockId);
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (block && blockRef.current) {
|
||||
block.dom = blockRef.current;
|
||||
}
|
||||
});
|
||||
const setRef = useCallback(
|
||||
(dom: HTMLElement) => {
|
||||
if (block != null && dom != null) {
|
||||
block.dom = dom;
|
||||
}
|
||||
},
|
||||
[block]
|
||||
);
|
||||
|
||||
const blockView = useMemo(() => {
|
||||
if (block?.type) {
|
||||
@@ -54,17 +55,13 @@ export const RenderBlock = ({
|
||||
) : null;
|
||||
|
||||
return hasContainer ? (
|
||||
<BlockContainer
|
||||
block-id={blockId}
|
||||
ref={blockRef}
|
||||
data-block-id={blockId}
|
||||
>
|
||||
<BlockContainer block-id={blockId} ref={setRef} data-block-id={blockId}>
|
||||
{view}
|
||||
</BlockContainer>
|
||||
) : (
|
||||
<> {view}</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const BlockContainer = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
|
||||
Reference in New Issue
Block a user