mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-10 09:10:29 +03:00
Editor Commands (#1450)
* add commands hook * add commands in editor * add command auto complete menu * add commands in room input * remove old reply code from room input * fix video component css * do not auto focus input on android or ios * fix crash on enable block after selection * fix circular deps in editor * fix autocomplete return focus move editor cursor * remove unwanted keydown from room input * fix emoji alignment in editor * test ipad user agent * refactor isAndroidOrIOS to mobileOrTablet * update slate & slate-react * downgrade slate-react to 0.98.4 0.99.0 has breaking changes with ReactEditor.focus * add sql to readable ext mimetype * fix empty editor formatting gets saved as draft * add option to use enter for newline * remove empty msg draft from atom family * prevent msg ctx menu from open on text selection
This commit is contained in:
parent
4d0b6b93bc
commit
613e6d6503
34 changed files with 620 additions and 131 deletions
261
src/app/components/editor/utils.ts
Normal file
261
src/app/components/editor/utils.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { BasePoint, BaseRange, Editor, Element, Point, Range, Text, Transforms } from 'slate';
|
||||
import { BlockType, MarkType } from './types';
|
||||
import {
|
||||
CommandElement,
|
||||
EmoticonElement,
|
||||
FormattedText,
|
||||
HeadingLevel,
|
||||
LinkElement,
|
||||
MentionElement,
|
||||
} from './slate';
|
||||
|
||||
const ALL_MARK_TYPE: MarkType[] = [
|
||||
MarkType.Bold,
|
||||
MarkType.Code,
|
||||
MarkType.Italic,
|
||||
MarkType.Spoiler,
|
||||
MarkType.StrikeThrough,
|
||||
MarkType.Underline,
|
||||
];
|
||||
|
||||
export const isMarkActive = (editor: Editor, format: MarkType) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format] === true : false;
|
||||
};
|
||||
|
||||
export const isAnyMarkActive = (editor: Editor) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks && !!ALL_MARK_TYPE.find((type) => marks[type] === true);
|
||||
};
|
||||
|
||||
export const toggleMark = (editor: Editor, format: MarkType) => {
|
||||
const isActive = isMarkActive(editor, format);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeAllMark = (editor: Editor) => {
|
||||
ALL_MARK_TYPE.forEach((mark) => {
|
||||
if (isMarkActive(editor, mark)) Editor.removeMark(editor, mark);
|
||||
});
|
||||
};
|
||||
|
||||
export const isBlockActive = (editor: Editor, format: BlockType) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: (node) => Element.isElement(node) && node.type === format,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
type BlockOption = { level: HeadingLevel };
|
||||
const NESTED_BLOCK = [
|
||||
BlockType.OrderedList,
|
||||
BlockType.UnorderedList,
|
||||
BlockType.BlockQuote,
|
||||
BlockType.CodeBlock,
|
||||
];
|
||||
|
||||
export const toggleBlock = (editor: Editor, format: BlockType, option?: BlockOption) => {
|
||||
Transforms.collapse(editor, {
|
||||
edge: 'end',
|
||||
});
|
||||
const isActive = isBlockActive(editor, format);
|
||||
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: (node) => Element.isElement(node) && NESTED_BLOCK.includes(node.type),
|
||||
split: true,
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.Paragraph,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.OrderedList || format === BlockType.UnorderedList) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.ListItem,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
if (format === BlockType.CodeBlock) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.CodeLine,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.BlockQuote) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.QuoteLine,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.Heading) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: format,
|
||||
level: option?.level ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
Transforms.setNodes(editor, {
|
||||
type: format,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetEditor = (editor: Editor) => {
|
||||
Transforms.delete(editor, {
|
||||
at: {
|
||||
anchor: Editor.start(editor, []),
|
||||
focus: Editor.end(editor, []),
|
||||
},
|
||||
});
|
||||
|
||||
toggleBlock(editor, BlockType.Paragraph);
|
||||
removeAllMark(editor);
|
||||
};
|
||||
|
||||
export const resetEditorHistory = (editor: Editor) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
editor.history = {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const createMentionElement = (
|
||||
id: string,
|
||||
name: string,
|
||||
highlight: boolean
|
||||
): MentionElement => ({
|
||||
type: BlockType.Mention,
|
||||
id,
|
||||
highlight,
|
||||
name,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const createEmoticonElement = (key: string, shortcode: string): EmoticonElement => ({
|
||||
type: BlockType.Emoticon,
|
||||
key,
|
||||
shortcode,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const createLinkElement = (
|
||||
href: string,
|
||||
children: string | FormattedText[]
|
||||
): LinkElement => ({
|
||||
type: BlockType.Link,
|
||||
href,
|
||||
children: typeof children === 'string' ? [{ text: children }] : children,
|
||||
});
|
||||
|
||||
export const createCommandElement = (command: string): CommandElement => ({
|
||||
type: BlockType.Command,
|
||||
command,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const replaceWithElement = (editor: Editor, selectRange: BaseRange, element: Element) => {
|
||||
Transforms.select(editor, selectRange);
|
||||
Transforms.insertNodes(editor, element);
|
||||
Transforms.collapse(editor, {
|
||||
edge: 'end',
|
||||
});
|
||||
};
|
||||
|
||||
export const moveCursor = (editor: Editor, withSpace?: boolean) => {
|
||||
Transforms.move(editor);
|
||||
if (withSpace) editor.insertText(' ');
|
||||
};
|
||||
|
||||
interface PointUntilCharOptions {
|
||||
match: (char: string) => boolean;
|
||||
reverse?: boolean;
|
||||
}
|
||||
export const getPointUntilChar = (
|
||||
editor: Editor,
|
||||
cursorPoint: BasePoint,
|
||||
options: PointUntilCharOptions
|
||||
): BasePoint | undefined => {
|
||||
let targetPoint: BasePoint | undefined;
|
||||
let prevPoint: BasePoint | undefined;
|
||||
let char: string | undefined;
|
||||
|
||||
const pointItr = Editor.positions(editor, {
|
||||
at: {
|
||||
anchor: Editor.start(editor, []),
|
||||
focus: Editor.point(editor, cursorPoint, { edge: 'start' }),
|
||||
},
|
||||
unit: 'character',
|
||||
reverse: options.reverse,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const point of pointItr) {
|
||||
if (!Point.equals(point, cursorPoint) && prevPoint) {
|
||||
char = Editor.string(editor, { anchor: point, focus: prevPoint });
|
||||
|
||||
if (options.match(char)) break;
|
||||
targetPoint = point;
|
||||
}
|
||||
prevPoint = point;
|
||||
}
|
||||
return targetPoint;
|
||||
};
|
||||
|
||||
export const getPrevWorldRange = (editor: Editor): BaseRange | undefined => {
|
||||
const { selection } = editor;
|
||||
if (!selection || !Range.isCollapsed(selection)) return undefined;
|
||||
const [cursorPoint] = Range.edges(selection);
|
||||
const worldStartPoint = getPointUntilChar(editor, cursorPoint, {
|
||||
reverse: true,
|
||||
match: (char) => char === ' ',
|
||||
});
|
||||
return worldStartPoint && Editor.range(editor, worldStartPoint, cursorPoint);
|
||||
};
|
||||
|
||||
export const isEmptyEditor = (editor: Editor): boolean => {
|
||||
const firstChildren = editor.children[0];
|
||||
if (firstChildren && Element.isElement(firstChildren)) {
|
||||
const isEmpty = editor.children.length === 1 && Editor.isEmpty(editor, firstChildren);
|
||||
return isEmpty;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getBeginCommand = (editor: Editor): string | undefined => {
|
||||
const lineBlock = editor.children[0];
|
||||
if (!Element.isElement(lineBlock)) return undefined;
|
||||
if (lineBlock.type !== BlockType.Paragraph) return undefined;
|
||||
|
||||
const [firstInline, secondInline] = lineBlock.children;
|
||||
const isEmptyText = Text.isText(firstInline) && firstInline.text.trim() === '';
|
||||
if (!isEmptyText) return undefined;
|
||||
if (Element.isElement(secondInline) && secondInline.type === BlockType.Command)
|
||||
return secondInline.command;
|
||||
return undefined;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue