mirror of
https://github.com/array-in-a-matrix/xinny.git
synced 2024-06-11 17:07:12 -04:00
195 lines
4.9 KiB
TypeScript
195 lines
4.9 KiB
TypeScript
|
import { BasePoint, BaseRange, Editor, Element, Point, Range, Transforms } from 'slate';
|
||
|
import { BlockType, MarkType } from './Elements';
|
||
|
import { EmoticonElement, FormattedText, HeadingLevel, LinkElement, MentionElement } from './slate';
|
||
|
|
||
|
export const isMarkActive = (editor: Editor, format: MarkType) => {
|
||
|
const marks = Editor.marks(editor);
|
||
|
return marks ? marks[format] === true : false;
|
||
|
};
|
||
|
|
||
|
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 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) => {
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
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 replaceWithElement = (editor: Editor, selectRange: BaseRange, element: Element) => {
|
||
|
Transforms.select(editor, selectRange);
|
||
|
Transforms.insertNodes(editor, element);
|
||
|
};
|
||
|
|
||
|
export const moveCursor = (editor: Editor, withSpace?: boolean) => {
|
||
|
// without timeout it works properly when we select autocomplete with Tab or Space
|
||
|
setTimeout(() => {
|
||
|
Transforms.move(editor);
|
||
|
if (withSpace) editor.insertText(' ');
|
||
|
}, 1);
|
||
|
};
|
||
|
|
||
|
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);
|
||
|
};
|