SlateJS: добавление изображений и ссылок
Ранее мы говорили о настройке простого текстового редактора SlateJS. Теперь мы собираемся добавить в наш текстовый редактор две новые функции - вставку изображения и ссылки.
Панель инструментов
Чтобы начать добавлять функциональные возможности форматированного текста, нам нужно создать компонент панели инструментов.
import { useSlateStatic } from 'slate-react';
...
const Toolbar = () => {
const editor = useSlateStatic()
const handleInsertImage = () => {
const url = prompt("Enter an Image URL"); // For simplicity
insertImage(editor, url); // will be implemented later
};
const handleInsertLink = () => {
const url = prompt("Enter a URL"); // For simplicity
insertLink(editor, url); // will be implemented later
};
return (
<div className="toolbar">
<button onClick={handleInsertImage}>Image</button>
<button onClick={handleInsertLink}>Link</button>
</div>
)
}
Здесь следует отметить следующие важные моменты:
useSlateStatic
: дает нам экземпляр нашего редактора, который не вызывает повторного рендеринга,insertImage
: вспомогательная функция, которая вставит изображение в наш редакторinsertLink
: вспомогательная функция, которая вставит ссылку в наш редактор
Затем мы можем использовать этот компонент как дочерний по отношению к нашему компоненту Slate
. Мы делаем это, чтобы мы могли использовать useSlateStatic
.
const Editor = () => {
...
return (
<Slate ...>
<Toolbar />
...
</Slate>
)
}
insertImage
Наша функция insertImage
будет обрабатывать то, как мы будем вставлять изображения в наш редактор. Придется установить некоторые правила.
- Если редактор не имеет фокуса, мы добавим изображение в конец редактора.
- Если редактор сосредоточен на пустом узле (например, узле изображения), мы заменим пустой узел изображением.
- Если редактор сосредоточен на непустом узле, мы добавим изображение после него.
const insertImage(editor, url) => {
if (!url) return;
const { selection } = editor;
const image = createImageNode("Image", url);
ReactEditor.focus(editor);
if (!!selection) {
const [parentNode, parentPath] = Editor.parent(
editor,
selection.focus?.path
);
if (editor.isVoid(parentNode) || Node.string(parentNode).length) {
// Вставьте новый узел изображения после пустого узла или узла с содержимым
Transforms.insertNodes(editor, image, {
at: Path.next(parentPath),
select: true
});
} else {
// Если узел пуст, замените его
Transforms.removeNodes(editor, { at: parentPath });
Transforms.insertNodes(editor, image, { at: parentPath, select: true });
}
} else {
// Вставьте новый узел изображения в нижней части редактора при выделении
Transforms.insertNodes(editor, image, { select: true });
}
}
insertLink
Теперь, когда мы можем вставлять изображения, давайте добавим функциональность для вставки ссылок. Подобно изображениям, нам нужно установить правила.
- Если редактор не сфокусирован, вставьте новую ссылку внутри абзаца в конце редактора.
- Если редактор сосредоточен на пустом узле (например, на узле изображения), вставьте новую ссылку внутри абзаца под пустым узлом.
- Если редактор находится внутри абзаца, вставьте новую ссылку в выбранное место.
- Если выделен диапазон текста, преобразуйте выделенный текст в ссылку.
- Если выделенный текст состоит из ссылки, удалите ссылку и следуйте Правилам № 3 и № 4.
const createLinkNode = (href, text) => ({
type: "link",
href,
children: [{ text }]
});
const removeLink = (editor, opts = {}) => {
Transforms.unwrapNodes(editor, {
...opts,
match: (n) =>
!Editor.isEditor(n) && Element.isElement(n) && n.type === "link"
});
};
const insertLink = (editor, url) => {
if (!url) return;
const { selection } = editor;
const link = createLinkNode(url, "New Link");
ReactEditor.focus(editor);
if (!!selection) {
const [parentNode, parentPath] = Editor.parent(
editor,
selection.focus?.path
);
if (parentNode.type === "link") {
removeLink(editor);
}
if (editor.isVoid(parentNode)) {
Transforms.insertNodes(editor, createParagraphNode([link]), {
at: Path.next(parentPath),
select: true
});
} else if (Range.isCollapsed(selection)) {
Transforms.insertNodes(editor, link, { select: true });
} else {
Transforms.wrapNodes(editor, link, { split: true });
Transforms.collapse(editor, { edge: "end" });
}
} else {
Transforms.insertNodes(editor, createParagraphNode([link]));
}
};
Пользовательский тип Link
Теперь, когда мы можем вставлять ссылки, давайте убедимся, что мы можем правильно отображать ссылку.
const Link = ({ attributes, element, children }) => (
<a {...attributes} href={element.href}>
{children}
</a>
);
Затем мы можем обновить нашу функцию renderElement
, чтобы включить новый тип ссылки.
const renderElement = (props) => {
switch (props.element.type) {
case "image":
return <Image {...props} />;
case "link":
return <Link {...props} />;
default:
return <Paragraph {...props} />;
}
};
Всплывающее окно ссылки
Поскольку мы не можем точно сказать, какой URL у ссылки, мы можем создать простое всплывающее окно всякий раз, когда мы фокусируемся на ссылке. Мы можем сделать это, обновив наш компонент Link
.
const Link = ({ attributes, element, children }) => {
const editor = useSlateStatic();
const selected = useSelected();
const focused = useFocused();
return (
<div className="element-link">
<a {...attributes} href={element.href}>
{children}
</a>
{selected && focused && (
<div className="popup" contentEditable={false}>
<a href={element.href} rel="noreferrer" target="_blank">
<FontAwesomeIcon icon={faExternalLinkAlt} />
{element.href}
</a>
<button onClick={() => removeLink(editor)}>
<FontAwesomeIcon icon={faUnlink} />
</button>
</div>
)}
</div>
);
};
Заключение
Когда наш форматированный текст обретает форму, мы начинаем видеть мощь Slate и то, как вы можете реализовать его так, как вы хотите.