Skip to content

Commit

Permalink
feat: support shortcuts on web (AppFlowy-IO#6474)
Browse files Browse the repository at this point in the history
* fix: add tab and shift+tab test cases on web

* fix: support BIUS on web

* fix: add BIUS test cases

* fix: add markdown to block test cases

* fix: add markdown to block test cases
  • Loading branch information
qinluhe authored Oct 7, 2024
1 parent 87408fd commit 19a3df6
Show file tree
Hide file tree
Showing 45 changed files with 1,993 additions and 172 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/appflowy_web_app/cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// https://on.cypress.io/configuration
// ***********************************************************
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
import 'cypress-real-events';

// Import commands.js using ES2015 syntax:
import '@cypress/code-coverage/support';
Expand Down
2 changes: 2 additions & 0 deletions frontend/appflowy_web_app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dexie-react-hooks": "^1.1.7",
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.2.1",
"escape-string-regexp": "^5.0.0",
"events": "^3.3.0",
"google-protobuf": "^3.15.12",
"highlight.js": "^11.10.0",
Expand Down Expand Up @@ -150,6 +151,7 @@
"cross-env": "^7.0.3",
"cypress": "^13.7.2",
"cypress-image-snapshot": "^4.0.1",
"cypress-real-events": "^1.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
Expand Down
19 changes: 19 additions & 0 deletions frontend/appflowy_web_app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function generateId () {
export function withTestingYjsEditor (editor: Editor, doc: Y.Doc) {
const yjdEditor = withYjs(editor, doc, {
localOrigin: CollabOrigin.Local,
readOnly: true,
});

return yjdEditor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BlockType } from '@/application/types';

/** List block types */
export const ListBlockTypes = [BlockType.TodoListBlock, BlockType.BulletedListBlock, BlockType.NumberedListBlock];

/** Container block types */
export const CONTAINER_BLOCK_TYPES = [
BlockType.ToggleListBlock,
BlockType.TodoListBlock,
BlockType.Paragraph,
BlockType.QuoteBlock,
BlockType.BulletedListBlock,
BlockType.NumberedListBlock,
BlockType.Page,
];
export const SOFT_BREAK_TYPES = [BlockType.CalloutBlock, BlockType.CodeBlock];
186 changes: 143 additions & 43 deletions frontend/appflowy_web_app/src/application/slate-yjs/command/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { YjsEditor } from '@/application/slate-yjs/plugins/withYjs';
import { EditorMarkFormat } from '@/application/slate-yjs/types';
import {
dataStringTOJson,
executeOperations,
getBlock, getBlockEntry, getSelectionOrThrow, getSharedRoot,
handleCollapsedBreak, handleDeleteEntireDocument, handleMergeBlockBackward, handleNonParagraphBlockBackspace,
handleRangeBreak, handleShouldLiftBlockBackspace,
handleMergeBlockForward,
removeRange,
getBlock,
getBlockEntry,
getSelectionOrThrow,
getSharedRoot,
handleCollapsedBreakWithTxn,
handleDeleteEntireDocumentWithTxn,
handleIndentBlockWithTxn,
handleLiftBlockOnBackspaceAndEnterWithTxn,
handleLiftBlockOnTabWithTxn,
handleMergeBlockBackwardWithTxn,
handleMergeBlockForwardWithTxn,
handleNonParagraphBlockBackspaceAndEnterWithTxn,
handleRangeBreak,
removeRangeWithTxn,
turnToBlock,
} from '@/application/slate-yjs/utils/yjsOperations';
import { BlockData, BlockType, InlineBlockType, Mention, MentionType, YjsEditorKey } from '@/application/types';
import { FormulaNode } from '@/components/editor/editor.type';
import {
BlockData,
BlockType,
MentionType,
TodoListBlockData,
ToggleListBlockData,
YjsEditorKey,
} from '@/application/types';
import { renderDate } from '@/utils/time';
import { BaseRange, Editor, Element, Node, NodeEntry, Range, Text, Transforms } from 'slate';
import isEqual from 'lodash-es/isEqual';
import { BasePoint, BaseRange, Editor, Element, Node, NodeEntry, Path, Range, Text, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';

export const CustomEditor = {
Expand All @@ -25,19 +43,6 @@ export const CustomEditor = {
},
// Get the text content of a block node, including the text content of its children and formula nodes
getBlockTextContent (node: Node): string {
if (Element.isElement(node)) {
if (node.type === InlineBlockType.Formula) {
return (node as FormulaNode).data || '';
}

if (node.type === InlineBlockType.Mention && (node.data as Mention)?.type === MentionType.Date) {
const date = (node.data as Mention).date || '';
const isUnix = date?.length === 10;

return renderDate(date, 'MMM DD, YYYY', isUnix);
}
}

if (Text.isText(node)) {
if (node.formula) {
return node.formula;
Expand All @@ -63,9 +68,8 @@ export const CustomEditor = {
},

setBlockData<T = BlockData> (editor: YjsEditor, blockId: string, updateData: T, select?: boolean) {
const readonly = editor.isElementReadOnly(editor);

if (readonly) {
if (editor.readOnly) {
return;
}

Expand All @@ -75,25 +79,36 @@ export const CustomEditor = {
...oldData,
...updateData,
};
const operations = [() => {
block.set(YjsEditorKey.block_data, JSON.stringify(newData));
}];
const entry = Editor.above(editor, {

const newProperties = {
data: newData,
} as Partial<Element>;
const [entry] = editor.nodes({
at: [],
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId === blockId,
});

executeOperations(editor.sharedRoot, operations, 'setBlockData');

if (!select) return;

if (!entry) {
Transforms.select(editor, Editor.start(editor, [0]));
console.error('Block not found');
return;
}

const nodePath = entry[1];
const [, path] = entry;
let atChild = false;
const { selection } = editor;

if (selection && Path.isAncestor(path, selection.anchor.path)) {
atChild = true;
}

Transforms.setNodes(editor, newProperties, { at: path });

if (!select) return;

if (atChild) {
Transforms.select(editor, Editor.start(editor, path));
}

Transforms.select(editor, Editor.start(editor, nodePath));
},
// Insert break line at the specified path
insertBreak (editor: YjsEditor, at?: BaseRange) {
Expand All @@ -103,7 +118,7 @@ export const CustomEditor = {
const isCollapsed = Range.isCollapsed(newAt);

if (isCollapsed) {
handleCollapsedBreak(editor, sharedRoot, newAt);
handleCollapsedBreakWithTxn(editor, sharedRoot, newAt);
} else {
handleRangeBreak(editor, sharedRoot, newAt);
}
Expand All @@ -126,18 +141,18 @@ export const CustomEditor = {
const blockType = block.get(YjsEditorKey.block_type) as BlockType;

if (blockType !== BlockType.Paragraph) {
handleNonParagraphBlockBackspace(sharedRoot, block);
handleNonParagraphBlockBackspaceAndEnterWithTxn(sharedRoot, block);
return;
}

if (path.length > 1 && handleShouldLiftBlockBackspace(editor, sharedRoot, block, point)) {
if (path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) {
return;
}

handleMergeBlockBackward(editor, node, point);
handleMergeBlockBackwardWithTxn(editor, node, point);
} else {
Transforms.collapse(editor, { edge: 'start' });
removeRange(editor, sharedRoot, newAt);
removeRangeWithTxn(editor, sharedRoot, newAt);
}
},

Expand All @@ -154,18 +169,103 @@ export const CustomEditor = {

const [node] = blockEntry as NodeEntry<Element>;

handleMergeBlockForward(editor, node, point);
handleMergeBlockForwardWithTxn(editor, node, point);
} else {
Transforms.collapse(editor, { edge: 'start' });
removeRange(editor, sharedRoot, newAt);
removeRangeWithTxn(editor, sharedRoot, newAt);
}
},

deleteEntireDocument (editor: YjsEditor) {
handleDeleteEntireDocument(editor);
handleDeleteEntireDocumentWithTxn(editor);
},

removeRange (editor: YjsEditor, at: BaseRange) {
removeRange(editor, getSharedRoot(editor), at);
removeRangeWithTxn(editor, getSharedRoot(editor), at);
},

tabForward (editor: YjsEditor, point: BasePoint) {
const sharedRoot = getSharedRoot(editor);
const [node] = getBlockEntry(editor, point);

const block = getBlock(node.blockId as string, sharedRoot);

handleIndentBlockWithTxn(editor, sharedRoot, block, point);
},

tabBackward (editor: YjsEditor, point: BasePoint) {
const sharedRoot = getSharedRoot(editor);
const [node] = getBlockEntry(editor, point);

const block = getBlock(node.blockId as string, sharedRoot);

handleLiftBlockOnTabWithTxn(editor, sharedRoot, block, point);
},

toggleToggleList (editor: YjsEditor, blockId: string) {
const sharedRoot = getSharedRoot(editor);
const data = dataStringTOJson(getBlock(blockId, sharedRoot).get(YjsEditorKey.block_data)) as ToggleListBlockData;
const { selection } = editor;

if (!selection) return;

if (Range.isExpanded(selection)) {
Transforms.collapse(editor, { edge: 'start' });
}

const point = Editor.start(editor, selection);

const [node] = getBlockEntry(editor, point);

CustomEditor.setBlockData(editor, blockId, {
collapsed: !data.collapsed,
}, node.blockId !== blockId);
},

toggleTodoList (editor: YjsEditor, blockId: string) {
const sharedRoot = getSharedRoot(editor);
const data = dataStringTOJson(getBlock(blockId, sharedRoot).get(YjsEditorKey.block_data)) as TodoListBlockData;

CustomEditor.setBlockData(editor, blockId, {
checked: !data.checked,
}, false);
},

toggleMark (editor: ReactEditor, {
key, value,
}: {
key: EditorMarkFormat, value: boolean | string
}) {
if (editor.marks && Object.keys(editor.marks).includes(key)) {
editor.removeMark(key);
} else {
editor.addMark(key, value);
}
},

addMark (editor: ReactEditor, {
key, value,
}: {
key: EditorMarkFormat, value: boolean | string
}) {
editor.addMark(key, value);
},

turnToBlock<T extends BlockData> (editor: YjsEditor, blockId: string, type: BlockType, data: T) {
const operations: (() => void)[] = [];
const sharedRoot = getSharedRoot(editor);
const sourceBlock = getBlock(blockId, sharedRoot);
const sourceType = sourceBlock.get(YjsEditorKey.block_type) as BlockType;
const oldData = dataStringTOJson(sourceBlock.get(YjsEditorKey.block_data));

if (sourceType === type && isEqual(oldData, data)) {
return;
}

operations.push(() => {
turnToBlock(sharedRoot, sourceBlock, type, data);
});

executeOperations(sharedRoot, operations, 'turnToBlock');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function withYHistory<T extends YjsEditor> (
): T & YHistoryEditor {
const e = editor as T & YHistoryEditor;

if (Editor.isElementReadOnly(e, e)) {
if (e.readOnly) {
return e;
}

Expand All @@ -52,6 +52,7 @@ export function withYHistory<T extends YjsEditor> (

e.onChange = () => {
onChange();

const selection = e.selection;

try {
Expand Down
Loading

0 comments on commit 19a3df6

Please sign in to comment.