cinny/src/app/components/editor/utils.ts
Ajay Bura 5058136737
support matrix.to links (#1849)
* support room via server params and eventId

* change copy link to matrix.to links

* display matrix.to links in messages as pill and stop generating url previews for them

* improve editor mention to include viaServers and eventId

* fix mention custom attributes

* always try to open room in current space

* jump to latest remove target eventId from url

* add create direct search options to open/create dm with url
2024-07-30 22:18:59 +10:00

275 lines
6.9 KiB
TypeScript

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;
};
export const headingLevel = (editor: Editor): HeadingLevel | undefined => {
const [nodeEntry] = Editor.nodes(editor, {
match: (node) => Element.isElement(node) && node.type === BlockType.Heading,
});
const [node] = nodeEntry ?? [];
if (!node) return undefined;
if ('level' in node) return node.level;
return undefined;
};
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,
eventId?: string,
viaServers?: string[]
): MentionElement => ({
type: BlockType.Mention,
id,
eventId,
viaServers,
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;
};