ํ์ฅ
Inkio ํ์ฅ์ ๋ ์ธต์ผ๋ก ๋๋ฉ๋๋ค.
@inkio/core- callout
- details/toggle
- table
- keyboard shortcuts
@inkio/advanced- slash command
- block handle
- mention
- hashtag
- wiki link
- bookmark
- comment
๊ถ์ฅ ์์์
- classic editor๊ฐ ํ์ํ๋ฉด
@inkio/simple - notion-like editor๊ฐ ํ์ํ๋ฉด
@inkio/editor - low-level ์กฐํฉ์ด ํ์ํ๋ฉด
@inkio/core๋๋@inkio/advanced
์กฐํฉ ์์
import { Editor } from '@inkio/core';
import { getDefaultExtensions } from '@inkio/advanced';
import '@inkio/core/minimal.css';
import '@inkio/advanced/style.css';
const extensions = getDefaultExtensions({
hashtagItems: ({ query }) => [{ id: query || 'inkio', label: `#${query || 'inkio'}` }],
});
export function Page() {
return <Editor extensions={extensions} initialContent="<p>Hello Inkio</p>" />;
}๋ฉ์ (Mention)
@ ํธ๋ฆฌ๊ฑฐ๋ก ์ฌ์ฉ์ ๋ฑ์ ๋ฉ์
ํฉ๋๋ค. mentionItems๋ ({ query }) => MentionItem[] ํํ์ ํจ์๋ก,
์ฌ์ฉ์๊ฐ @ ๋ค์ ์
๋ ฅํ query์ ๋ง๋ ํญ๋ชฉ์ ๋ฐํํฉ๋๋ค. ๋๊ธฐ/๋น๋๊ธฐ ๋ชจ๋ ๊ฐ๋ฅํฉ๋๋ค.
interface MentionItem {
id: string;
label: string;
[key: string]: unknown;
}import { Editor } from '@inkio/editor';
<Editor
mentionItems={async ({ query }) => {
const users = await searchUsers(query);
return users.map((user) => ({ id: user.id, label: user.name }));
}}
/>ํด์ํ๊ทธ๋ ๋์ผํ ํจํด์ด๋ฉฐ, # ํธ๋ฆฌ๊ฑฐ์ hashtagItems๋ฅผ ์ฌ์ฉํฉ๋๋ค.
<Editor
hashtagItems={({ query }) => [
{ id: query || 'inkio', label: `#${query || 'inkio'}` },
]}
/>@inkio/core + @inkio/advanced ์ง์ ์กฐํฉ ์์๋ getDefaultExtensions์ ๋์ผํ ์ต์
์ ์ ๋ฌํฉ๋๋ค.
const extensions = getDefaultExtensions({
mentionItems: async ({ query }) => searchUsers(query),
});์ฌ๋์ ์ปค๋งจ๋ (Slash Command)
/๋ฅผ ์
๋ ฅํ๋ฉด ๋ช
๋ น ๋ฉ๋ด๊ฐ ์ด๋ฆฝ๋๋ค. ๊ธฐ๋ณธ ๋ช
๋ น์๋ ํค๋ฉ, ๋ฆฌ์คํธ, ํ, ์ฝ๋ ๋ธ๋ก, ์ฝ์์,
ํ ๊ธ ๋ฆฌ์คํธ, ๋ชฉ์ฐจ, ์ด๋ฏธ์ง ๋ฑ์ด ํฌํจ๋ฉ๋๋ค. ๋ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ์ปค์คํฐ๋ง์ด์ฆํ ์ ์์ต๋๋ค.
transformSlashCommands๋ ๊ธฐ๋ณธ ๋ช
๋ น ๋ฐฐ์ด์ ๋ฐ์ ๊ฐ๊ณตํ ๋ค ๋ฐํํฉ๋๋ค. ์ผ๋ถ ๋ช
๋ น๋ง ์ถ๊ฐํ๊ฑฐ๋
์ ๊ฑฐํ ๋ ์ ํฉํฉ๋๋ค. ๋ฐํ๋ ํญ๋ชฉ์ ์ดํ query๋ก ํํฐ๋ง๋ฉ๋๋ค.
import { Editor } from '@inkio/editor';
import type { SlashCommandItem } from '@inkio/advanced';
<Editor
transformSlashCommands={(defaults, { editor }) => {
const custom: SlashCommandItem = {
id: 'today',
label: 'Today',
description: '์ค๋ ๋ ์ง ์ฝ์
',
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertContent(new Date().toLocaleDateString())
.run();
},
};
return [custom, ...defaults];
}}
/>slashCommands๋ ๊ธฐ๋ณธ ๋ช
๋ น์ ๋ฌด์ํ๊ณ ๋ช
๋ น ๋ชฉ๋ก ์ ์ฒด๋ฅผ ์ง์ ์ ๊ณตํฉ๋๋ค.
์๊ทธ๋์ฒ๋ (query: string) => SlashCommandItem[] | Promise<SlashCommandItem[]>์ด๋ฉฐ,
์ด ๊ฒฝ์ฐ ํํฐ๋ง๋ ์ง์ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
<Editor
slashCommands={(query) =>
myCommands.filter((item) =>
item.label.toLowerCase().includes(query.toLowerCase()),
)
}
/>SlashCommandItem์ isAvailable?: (editor) => boolean์ผ๋ก ํน์ ๋ช
๋ น์ ๋
ธ์ถ ์กฐ๊ฑด์ ์ง์ ํ ์ ์์ต๋๋ค.
์ํค ๋งํฌ (Wiki Link)
[[ํ์ด์ง ์ด๋ฆ]] ๋ฌธ๋ฒ์ผ๋ก ์ธ๋ผ์ธ ์ํค ๋งํฌ๋ฅผ ๋ง๋ญ๋๋ค. ๋ซ๋ ]]๋ฅผ ์
๋ ฅํ๋ ์๊ฐ
์
๋ ฅ ๊ท์น(input rule)์ด ๋ฐ๋ํด ํ
์คํธ๊ฐ ์ํค ๋งํฌ ๋
ธ๋๋ก ์นํ๋๋ฉฐ, [[...]]๊ฐ ํฌํจ๋ ํ
์คํธ๋ฅผ
๋ถ์ฌ๋ฃ์ด๋ ๋์ผํ๊ฒ ๋ณํ๋ฉ๋๋ค.
@inkio/editor์์๋ wikiLink prop์ผ๋ก ์ผ๊ณ ๋๋ฉฐ ๊ธฐ๋ณธ๊ฐ์ true์
๋๋ค.
๋งํฌ ํด๋ฆญ ๋์์ onWikiLinkClick ์ฝ๋ฐฑ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค. ์ฝ๋ฐฑ์ ์ง์ ํ์ง ์์ผ๋ฉด ๋งํฌ๋
ํด๋ฆญํ ์ ์๋ ํ
์คํธ๋ก๋ง ํ์๋ฉ๋๋ค.
import { Editor } from '@inkio/editor';
<Editor
wikiLink
onWikiLinkClick={(href) => {
router.push(`/wiki/${encodeURIComponent(href)}`);
}}
/>href๋ [[...]] ์์ ์
๋ ฅํ ํ์ด์ง ์ด๋ฆ ๋ฌธ์์ด์
๋๋ค. ์์ ํ์ง ์์ URL์ ํด๋ฆญ์ด ๋ฌด์๋ฉ๋๋ค.
๋ถ๋งํฌ (Bookmark)
URL์ ๋งํฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นด๋๋ก ๋ ๋๋งํ๋ ๋ธ๋ก ๋
ธ๋์
๋๋ค. @inkio/editor์์๋ bookmark prop์ผ๋ก
์ค์ ํ๋ฉฐ, false๋ฅผ ์ ๋ฌํ๋ฉด ๋นํ์ฑํ๋ฉ๋๋ค.
bookmark?: false | { onResolveBookmark?: (url: string) => Promise<BookmarkPreview> };๋ถ๋งํฌ๋ setBookmark ์ปค๋งจ๋๋ก ์ฝ์
ํฉ๋๋ค.
editor.commands.setBookmark({ url: 'https://example.com' });onResolveBookmark๋ URL์ ๋ฐ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ์ดํฐ(BookmarkPreview)๋ฅผ ๋ฐํํฉ๋๋ค.
๋ถ๋งํฌ ๋
ธ๋๋ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ์ดํฐ๊ฐ ์์ง ์์ ๋ ํ ๋ฒ ์ด ํจ์๋ฅผ ํธ์ถํ๊ณ , ๋ฐํ๋ ๊ฐ์ผ๋ก ์นด๋๋ฅผ ์ฑ์๋๋ค.
์ง์ ํ์ง ์์ผ๋ฉด ๋ถ๋งํฌ๋ ๋จ์ ๋งํฌ๋ก๋ง ํ์๋ฉ๋๋ค.
interface BookmarkPreview {
title?: string;
description?: string;
image?: string;
favicon?: string;
}<Editor
bookmark={{
onResolveBookmark: async (url) => {
const meta = await fetchOpenGraph(url); // ์ง์ ๊ตฌํํ๋ ๋ฉํ๋ฐ์ดํฐ ์กฐํ
return {
title: meta.title,
description: meta.description,
image: meta.ogImage,
favicon: meta.favicon,
};
},
}}
/>
onResolveBookmark๋ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ก์ง๋ง ๋ด๋นํฉ๋๋ค. Open Graph ์คํฌ๋ํ ๊ฐ์ ์ค์ ์กฐํ๋ ์ง์ ๊ตฌํํด์ผ ํฉ๋๋ค(๋ณดํต ์๋ฒ ์ธก ์๋ํฌ์ธํธ).
Image editor
import { Editor } from '@inkio/editor';
import { ImageEditorModal } from '@inkio/image-editor';
import '@inkio/image-editor/style.css';
export function Page() {
return (
<Editor
onImageUpload={async (file) => URL.createObjectURL(file)}
imageBlock={{ imageEditor: ImageEditorModal }}
/>
);
}์์ธํ ๋ด์ฉ์ ์ด๋ฏธ์ง ์๋ํฐ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
TocBlock (์ธ๋ผ์ธ ๋ชฉ์ฐจ)
@inkio/core์ ๊ธฐ๋ณธ ํฌํจ๋ฉ๋๋ค. ๋ฌธ์ ์์ Notion ์คํ์ผ์ ์ธ๋ผ์ธ ๋ชฉ์ฐจ ๋ธ๋ก์ ์ฝ์
ํฉ๋๋ค.
์ฐธ๊ณ : ์๋ํฐ ์ธ๋ถ์ ์ค๋ฒ๋ ์ด ๋ฏธ๋๋งต ํํ๋ก ํ์ํ๋ ค๋ฉด
<ToC>์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ธ์. ์ปดํฌ๋ํธ ๋ฌธ์ ์ฐธ๊ณ
/toc์ฌ๋์ ์ปค๋งจ๋๋ก ์ฝ์ (@inkio/advancedSlashCommand ํ์)- ํค๋ฉ์ด ์ถ๊ฐ/์์ /์ญ์ ๋๋ฉด ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค
- ํญ๋ชฉ ํด๋ฆญ ์ ํด๋น ํค๋ฉ์ผ๋ก ์คํฌ๋กค๋ฉ๋๋ค
- ๋ฐฐ๊ฒฝ/ํ ๋๋ฆฌ ์๋ Notion ์คํ์ผ ๋ฏธ๋๋ฉ ๋์์ธ (ํ ์คํธ ๋งํฌ๋ง)
- ํค๋ฉ์ด ์์ผ๋ฉด muted ํ๋ ์ด์คํ๋ ํ์: โAdd headings to see the table of contents.โ
๋นํ์ฑํํ๋ ค๋ฉด getExtensions์ tocBlock: false๋ฅผ ์ ๋ฌํฉ๋๋ค:
import { getExtensions } from '@inkio/core';
const extensions = getExtensions({ tocBlock: false });CodeBlock (์ฝ๋ ๋ธ๋ก)
@inkio/core์ ๊ธฐ๋ณธ ํฌํจ๋๋ฉฐ, lowlight ๊ธฐ๋ฐ ๊ตฌ๋ฌธ ๊ฐ์กฐ๋ฅผ ์ ๊ณตํฉ๋๋ค. 37๊ฐ์ ์ผ๋ฐ์ ์ธ ์ธ์ด๋ฅผ ์ง์ํฉ๋๋ค.
- ์๋ํฐ ๋ชจ๋: ํ๋กํ ์ธ์ด ์ ํ๊ธฐ(๋๋กญ๋ค์ด)๋ก ์ธ์ด ๋ณ๊ฒฝ
- ๋ทฐ์ด ๋ชจ๋: ๋ณต์ฌ ๋ฒํผ + ์ธ์ด ๋ฐฐ์ง ํ์
tsx/jsx๋ ๋ชจ๋typescript์ ๋ณ์นญ์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค
์ฝ๋ ๋ธ๋ก์ ๋นํ์ฑํํ๋ ค๋ฉด getExtensions์ codeBlock: false๋ฅผ ์ ๋ฌํฉ๋๋ค.
์ปค์คํ ํ์ฅ
extensions prop์ผ๋ก Tiptap ํ์ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ๊ต์ฒดํ ์ ์์ต๋๋ค.
ํ์
type ExtensionsInput =
| Extension[]
| { items: Extension[]; replace?: boolean };- ๋ฐฐ์ด ์ ๋ฌ: ๊ธฐ๋ณธ ํ์ฅ์ ์ถ๊ฐํฉ๋๋ค. ๋์ผํ ์ด๋ฆ์ ํ์ฅ์ด ์์ผ๋ฉด ๊ต์ฒด๋ฉ๋๋ค.
{ items, replace: true }์ ๋ฌ: ๊ธฐ๋ณธ ํ์ฅ์ ์ ๋ถ ๊ต์ฒดํฉ๋๋ค.
์์: ํ์ฅ ์ถ๊ฐ
import { Editor } from '@inkio/editor';
import CharacterCount from '@tiptap/extension-character-count';
<Editor
extensions={[CharacterCount.configure({ limit: 1000 })]}
/>์์: ๊ธฐ๋ณธ ํ์ฅ ์ ์ฒด ๊ต์ฒด
import { Editor } from '@inkio/editor';
import { Document, Paragraph, Text } from '@tiptap/starter-kit';
<Editor
extensions={{ items: [Document, Paragraph, Text], replace: true }}
/>๊ธฐ๋ฅ ํ ๊ธ (@inkio/editor ์ ์ฉ)
@inkio/editor์ Editor ์ปดํฌ๋ํธ๋ ์ฃผ์ ํ์ฅ์ prop์ผ๋ก ์ผ๊ณ ๋ ์ ์์ต๋๋ค.
| Prop | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|
blockHandle | true | ๋ธ๋ก ๋๋๊ทธ ํธ๋ค |
wikiLink | true | [[์ํค๋งํฌ]] ๋ฌธ๋ฒ |
table | true | ํ |
callout | true | ์ฝ์์ ๋ธ๋ก |
toggleList | true | ํ ๊ธ(์ ๊ธฐ/ํผ์น๊ธฐ) ๋ฆฌ์คํธ |
<Editor
blockHandle={false}
wikiLink={false}
/>CSS
@inkio/simple/minimal.css@inkio/editor/minimal.css@inkio/advanced/style.cssโ@inkio/core+@inkio/advanced์ง์ ์กฐํฉ ์@inkio/image-editor/style.css