Introduction
ํ๋ก์ค๋ฏธ๋ฌ๋ ๋ฆฌ์นํ
์คํธ ์๋ํฐ๋ฅผ ์ํ ํด์ ์ ๊ณตํ๋ค. WISWIG(what-you-see-is-what-you-get) ์๋ํฐ๋ก๋ถํฐ ์๊ฐ์ ๋ฐ์ UI๋ฅผ ์ฌ์ฉํ๋ฉด์๋ WISWIG์ ํธ์ง ์คํ์ผ๋ก ์ธํ ๋จ์ ๋ค์ ๋ณด์ํ๋ ค ๋
ธ๋ ฅํ๋ค.
WISWIG์ ํธ์ง ์คํ์ผ๋ก ์ธํ ๋จ์ , ์๋ฅผ ๋ค์ด ๋ญ๊ฐ ์์๊น?
ํ๋ก์ค๋ฏธ๋ฌ์ ๊ฐ์ฅ ์ค์ํ ์์น์ ๊ฐ๋ฐ์๊ฐ ๋ํ๋จผํธ์ ๋ํ ์์ ํ ํต์ ๊ถ์ ์ป๊ฒํ๋ ๊ฒ์ด๋ค. ์ฌ๊ธฐ์ ๋ํ๋จผํธ๋ HTML์ blob ๊ฐ์ฒด๊ฐ ์๋๋ผ, ์ฌ์ฉ์๊ฐ ์ ์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋งํ๋ค. ๋ํ๋จผํธ๋ ๊ฐ๋ฐ์๊ฐ ์ ์ํ ๊ด๊ณ ๋ด์์ ๋ช
์ํ ์๋ฆฌ๋จผํธ๋ง ํฌํจํ ์ ์๋ค. ์ด์ ๋ํด์๋ ํ์ โ์คํค๋งโ๋ฅผ ๋ค๋ฃจ๋ฉฐ ๋ ์์ธํ ์๊ธฐ๊ฐ ๋์จ๋ค.
๊ทธ๋ฆฌ๊ณ ๋ชจ๋ ์
๋ฐ์ดํธ๋ ์ฑ๊ธ ํฌ์ธํธ๋ฅผ ํต๊ณผํด์ผํ๋ฉฐ, ์ด๋ฅผ ํตํด ํ ๊ณณ์์ ์
๋ฐ์ดํธ๋ฅผ ๊ฒ์ฌํ๊ณ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ๊ด๋ฆฌํ ์ ์๋ค.
ํ๋ก์ค๋ฏธ๋ฌ์ ์ฝ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํธ๋ฆฌํ ๋๋์ธ ์ปดํฌ๋ํธ๊ฐ ์๋๋ค. ๋จ์ํจ๋ณด๋ค ๋ชจ๋์ฑ๊ณผ ์ปค์คํฐ๋ง์ด์ง์ ์ฐ์ ์ํ๋ค. ๋ฐ๋ผ์ ํ๋ก์ค๋ฏธ๋ฌ๋ ์๋์ฐจ ์์ ํ๋ณด๋ค๋ ๋ ๊ณ ์ธํธ์ ๋ ๊ฐ๊น๋ค.
ํ๋ก์ค๋ฏธ๋ฌ์๋ 4๊ฐ์ง ํ์ ๋ชจ๋์ด ์๋ค. ๋ฌด์์ ํธ์งํ๋ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ํ์ํ๊ณ , ์ฝ์ดํ์ผ๋ก๋ถํฐ ์๋ง์ Extension ๋ชจ๋๋ค์ด ๊ด๋ฆฌ๋๋ค. ์ด ๋ชจ๋๋ค์ ์ ์ฉํ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง๋ง ํ์์๋ ๊ธฐ๋ฅ์ ๋นผ๋ฒ๋ฆฌ๊ฑฐ๋ ๋น์ทํ ๊ธฐ๋ฅ์ ๊ตฌํํด ๋์ฒดํ ์ ์๋ค.
์๋ํฐ์ ๋ํ๋จผํธ ๋ชจ๋ธ์ ๋ํด ์ ์ํ๋ค. ๋ํ๋จผํธ ๋ชจ๋ธ์ ์์์ ๋งํ๋ฏ์ด ์๋ํฐ์ ์ฝํ
์ธ ๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋งํ๋ค.
์๋ํฐ์ ๋ชจ๋ state๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ๊ณตํ๋ค. ์ฌ๊ธฐ์ state๋ selection(์ฌ์ฉ์์ ์ปค์๊ฐ ์์นํ๋ ๋ถ๋ถ), ๊ทธ๋ฆฌ๊ณ transaction(์ผ๋ จ์ ์ํ ์
๋ฐ์ดํธ ์์
๋ค) ์์คํ
๋ค์ ํฌํจํ๋ค.
ํ๋ก์ค๋ฏธ๋ฌ ๋ทฐ ๋ชจ๋์ DOM์ ์๋ํฐ state๋ฅผ ํ์ํ๊ณ , ์ ์ ์ด๋ฒคํธ๋ฅผ ํธ๋ค๋งํ๋ ์ญํ ์ ํ๋ค.
๋ํ๋จผํธ๋ฅผ ์์ ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์ด ๋ณ๊ฒฝ์ฌํญ๋ค์ state ๋ชจ๋์ ํธ๋์ญ์
์์คํ
์ ํตํด ๊ธฐ๋ก๋๊ณ ์ฌ์๋ ์ ์๋ค. ์ด ๋๋ถ์ ํ๋ก์ค๋ฏธ๋ฌ ์๋ํฐ๋ ์ฝ๋ผ๋ณด๋ ์ดํ
์๋ํฐ๋ ๋๋๋ฆฌ๊ธฐ(undo history) ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ค.
์ด ๋ฟ๋ง ์๋๋ผ ๊นํ์์ ProseMirror Organization์๋ basic editing commands,ย binding keys,ย undo history,ย input macros,ย collaborative editing ๋ฑ ์ฌ๋ฌ ๋ชจ๋๋ค์ด ๋ ์กด์ฌํ๋ค.
My first editor
import {schema} from "prosemirror-schema-basic"
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
let state = EditorState.create({schema})
let view = new EditorView(document.body, {state})
JavaScript
๋ณต์ฌ
์ฒซ๋ฒ์งธ๋ก ํ ์ผ์ โ์คํค๋ง๋ฅผ ์ ์ํ๋ ๊ฒโ์ด๋ค. ๋ํ๋จผํธ๊ฐ ๋ฌด์์ผ๋ก ๊ตฌ์ฑ๋์ด์ผ ํ๋์ง ์์์ผํ๊ธฐ ๋๋ฌธ์ด๋ค.
๊ทธ ๋ค์ ๋๋ฒ์งธ๋ก๋ ์คํค๋ง๋ฅผ ํตํด ์๋ํฐ์ State๋ฅผ ์์ฑํด์ผ ํ๋ค.
ํ๋ก์ค๋ฏธ๋ฌ๋ ์ด๋ ๊ฒ ์์ฑ๋ state๋ฅผ ๊ฐ์ง๊ณ view ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๋ค.
1. Schema
ํ๋ก์ค๋ฏธ๋ฌ์ ๊ฐ ๋ํ๋จผํธ๋ ๊ฐ์์ โ์คํค๋งโ๋ฅผ ๊ฐ๊ณ ์๋ค. ์คํค๋ง๋ ๋ํ๋จผํธ์์ ์ด๋ค ์ข
๋ฅ์ ๋
ธ๋๋ค์ด ํฌํจ๋๊ณ , ๋ ๊ทธ ๋
ธ๋๋ค์ด ์ด๋ป๊ฒ ์ค์ฒฉ๋ ์ ์๋์ง ์ค๋ช
ํ๋ ๋ฐ์ดํฐ์ด๋ค. ์๋ฅผ ๋ค์ด, ์ต์์ ๋
ธ๋๋ ํ๋ ์ด์์ ๋ธ๋ก์ ํฌํจํ ์ ์๊ณ paragraph ๋
ธ๋๋ mark๊ฐ ์ ์ฉ๋ ์ฌ๋ฌ ๊ฐ์ ์ธ๋ผ์ธ ๋
ธ๋๋ฅผ ํฌํจํ ์ ์์์ ํ์ํ ์ ์๋ค.
import { Schema } from 'prosemirror-model'
const nodes = {
doc: {
content: 'block+', // ํ๋ ์ด์์ ๋ธ๋ก ๋
ธ๋๋ฅผ ํฌํจ
},
paragraph: {
content: 'inline*', // 0๊ฐ ์ด์์ ์ธ๋ผ์ธ ๋
ธ๋(text ๋ฑ)
group: 'block', // paragraph๋ block ๊ทธ๋ฃน
parseDOM: [{ tag: 'p' }],
toDOM() {
return ['p', 0] // 0์ p ํ๊ทธ์ ์ฝํ
์ธ ๊ฐ ์ฝ์
๋ ์์น
},
},
text: {
group: 'inline', // text๋ inline ๊ทธ๋ฃน
},
}
const marks = {
bold: {
parseDOM: [{ tag: 'strong' }, { style: 'font-weight=bold' }],
toDOM() {
return ['strong', 0]
},
},
italic: {
parseDOM: [{ tag: 'em' }, { style: 'font-style=italic' }],
toDOM() {
return ['em', 0]
},
},
}
export const schema = new Schema({ nodes, marks })
JavaScript
๋ณต์ฌ
Node Types
๋ํ๋จผํธ์ ๋ชจ๋ ๋
ธ๋๋ type์ ๊ฐ์ง๋ค. type์ ๋
ธ๋๊ฐ ๊ฐ์ง ์์ฑ์ด๋ ์๋ฏธ๋ฅผ ํํํ๋ฉฐ ์๋ํฐ์ ๋ ๋๋ง๋๋ค. ์คํค๋ง๋ฅผ ์ ์ํ ๋ ๋
ธ๋ ํ์
์ ํตํด ์คํค๋ง๊ฐ ์ด๋ค ๋
ธ๋๋ค์ด ์ด๋ค ๊ท์น์ผ๋ก ๊ตฌ์ฑ๋๋์ง ์ค๋ช
ํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ชจ๋ ์คํค๋ง๋ ์ต์ํ ํ๋์ ์ต์์ ๋
ธ๋ ํ์
์ ๊ฐ์ ธ์ผ ํ๊ณ , ๊ธฐ๋ณธ์ ์ผ๋ก ์ต์์ ๋
ธ๋ ํ์
์ ์ด๋ฆ์ "doc" ์ด๋ค. ๋ฌผ๋ก ์ด ๋ํ ์ค์ ์ผ๋ก ๋ฐ๊ฟ ์ ์๊ธด ํ๋ค. ๋ํ, ์ธ๋ผ์ธ ๋
ธ๋๋ค๋ก ๊ฐ์ฃผ๋๋ ๋
ธ๋ ํ์
๋ค์ inline ์์ฑ์ผ๋ก ์ ์ธํด์ผ ํ๋ค. text ํ์
์ ๊ฒฝ์ฐ ๋ด๋ถ์ ์ผ๋ก ์ธ๋ผ์ธ ๋
ธ๋๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ๋ช
์ํด์ค ํ์๋ ์๋ค.
Content Expressions
์คํค๋ง์ contentย ํ๋๋ content expressions(์ฝํ
์ธ ํํ์)์ผ๋ก ํํ๋๋ค. ์ด๋ ์์๋
ธ๋์ ์์๋ฅผ ์ ์ดํ๊ณ ์๋ํฐ๊ฐ ์ ํจํ ๋
ธ๋ ํ์
์ผ๋ก ๊ตฌ์ฑ๋์๋์ง ํ๋จํ๋ ๊ธฐ์ค์ด ๋๋ค.
์๋ฅผ ๋ค์ด paragraph๋ โํ ๋จ๋ฝโ์ ์๋ฏธํ๋ฉฐ, paragraph+๋ โํ ๊ฐ ์ด์์ ๋จ๋ฝโ์ ํํํ๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก paragraph*๋ โ0๊ฐ ์ด์์ ๋จ๋ฝโ์ ์๋ฏธํ๋ฉฐ, caption?๋ โ0๊ฐ ๋๋ 1๊ฐ์ ์บก์
๋
ธ๋โ๋ฅผ ์๋ฏธํ๋ค.
๋
ธ๋ ์ด๋ฆ ๋ค์ ์ ๊ท ํํ์ ์ ์ฌ ๋ฒ์ ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ์ ์๋๋ฐ, ์๋ฅผ ๋ค์ด {2}๋ โ์ ํํ ๋ ๊ฐโ, {1, 5}๋ โ1๊ฐ์์ 5๊ฐ๊น์งโ, {2,}๋ โ๋ ๊ฐ ์ด์โ์ ์๋ฏธํ๋ค.
์ด๋ฌํ ํํ์์ ๊ฒฐํฉํ์ฌ ์ํ์ค๋ฅผ ๋ง๋ค ์ ์๋ค. ์๋ฅผ ๋ค์ด โheading paragraph+โ๋ โ๋จผ์ ์ ๋ชฉ, ๊ทธ ๋ค์ ํ๋ ์ด์์ ๋จ๋ฝโ์ ์๋ฏธํ๋ค. ๋ ํํ์ ์ฌ์ด์ ์ ํ์ ํ์ํ๋ ค๋ฉด ํ์ดํ | ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ์๋ฅผ ๋ค์ด โ(paragraph | blockquote)+โ๋ โํ ๊ฐ ์ด์์ ๋จ๋ฝ ๋๋ ์ธ์ฉ๋ฌธโ์ ์๋ฏธํ๋ค.
group
group์ ์คํค๋ง์์ ์ฌ๋ฌ ๋ฒ ๋ํ๋ ์ ์๋ค. ์๋ฅผ ๋ค์ด โ๋ธ๋กโ ๋
ธ๋๋ผ๋ ๊ฐ๋
์ด ์ต์์ ์์ค์ ๋ํ๋ ์ ์์ง๋ง blockquote ๋ด๋ถ์ ์ค์ฒฉ๋์ด ์์ ์๋ ์๋ค. ์ด๋ด ๋, ๋
ธ๋ ์ฌ์์ group ์์ฑ์ ์ง์ ํ์ฌ ๋
ธ๋ ๊ทธ๋ฃน์ ์์ฑํ ์ ์์ผ๋ฉฐ, ํํ์์์ ํด๋น ๊ทธ๋ฃน์ ์ด๋ฆ์ ์ฐธ์กฐํ์ฌ ์ฌ์ฉํ ์ ์๋ค.
const groupSchema = new Schema({
nodes: {
doc: {content: "block+"},
paragraph: {group: "block", content: "text*"},
blockquote: {group: "block", content: "block+"},
text: {}
}
})
JavaScript
๋ณต์ฌ
์ด ์์์์ block+ ๋ (paragraph | blockquote)+ ๊ณผ ๊ฐ์ ์๋ฏธ์ด๋ค. paragraph์ blcokquote์ ๋ชจ๋ group ์์ฑ์ block ์ผ๋ก ์ง์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
๋ธ๋ก ์ฝํ
์ธ ๋ฅผ ํฌํจํ๋ ๋
ธ๋(์: ์ ์์์์์ doc ๋ฐ blockquote)์์๋ ํญ์ ์ต์ ํ ๊ฐ์ ์์ ๋
ธ๋๋ฅผ ํฌํจํ๋๋ก ๊ถ์ฅ๋๋ค. ๋ธ๋ผ์ฐ์ ๋ ํด๋น ๋
ธ๋๊ฐ ๋น ์ํ์ผ ๊ฒฝ์ฐ ๋
ธ๋๋ฅผ ์์ ํ ์ ๊ธฐ(collapse) ๋๋ฌธ์ ํธ์ง์ด ์๋นํ ์ด๋ ค์์ง๊ธฐ ๋๋ฌธ์ด๋ค. ์ฌ๊ธฐ์ ๋
ธ๋๋ฅผ collapseํ๋ค๋ ๊ฒ์ ํ๋ฉด์์์ ์์ ์ ๋ณด์ด๊ฒ ๋ง๋ค๊ฑฐ๋, ์ปค์์กฐ์ฐจ ๋์ ์ ์๋ ์ํ๋ก ๋ง๋ค์ด๋ฒ๋ฆฐ๋ค๋ ๋ป์ด๋ค.
์ฝํ ์ธ ํํ์์์ ์์๊ฐ ์ค์ํ ์ด์
๋ํ, ๋
ธ๋๊ฐ OR ํํ์ (|)์์ ๋ํ๋๋ ์์๋ ์ค์ํ๋ค. ์๋ฅผ ๋ค์ด, replace ๋จ๊ณ(๋ํ๋จผํธ์ ์ผ๋ถ๋ฅผ ์๋ก์ด ์ฝํ
์ธ ๋ก ๋์ฒดํ๋ ์์
) ์ดํ์๋ ๋ํ๋จผํธ๊ฐ ์คํค๋ง๋ฅผ ๊ณ์ ์งํค๋๋ก ํด์ผํ๋๋ฐ, ์ด๋ฅผ ์ํด ์ ํ๋์ง ์์ ๋
ธ๋์ ๊ธฐ๋ณธ ์ธ์คํด์ค๋ฅผ ์์ฑํ ๋, ํํ์์ ์ฒซ๋ฒ์งธ ํ์
์ด ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉ๋๋ค. ๋ง์ฝ์ ๋
ธ๋๊ฐ ์๋ ๊ทธ๋ฃน์ธ ๊ฒฝ์ฐ๋ ์ด๋จ๊น? ๊ทธ๋ฃน์์ paragraph์ blockquote ์ ์์๋ฅผ ๋ฐ๊ฟ blockquote๊ฐ ๋จผ์ ์ค๊ฒ ๋๋ค๋ฉด ์๋ํฐ๋ ๋ธ๋ก ๋
ธ๋๋ฅผ ์์ฑํ๋ ค๊ณ ์๋ํ ๋๋ง๋ค ์คํ์ค๋ฒํ๋ก์ฐ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค. ์ ๊ทธ๋ด๊น? blockquote ๋
ธ๋๋ block ํ์
์ ๋
ธ๋๋ฅผ ์ต์ ํ๋ ์ด์ ํ์๋ก ํ๊ธฐ ๋๋ฌธ์ ์ฝํ
์ธ ๋ก ๋ ๋ค๋ฅธ block ์ ์์ฑํ๋ ค ํ๊ณ , block ๊ทธ๋ฃน์ ๊ธฐ๋ณธ์ blockquote ์ด๋ฏ๋ก ๋ค์ blockquote ๋
ธ๋๋ฅผ ์์ฑํ๋ ค๊ณ ์๋ํ๊ณ , ์ด ๊ณผ์ ์ด ๋ฐ๋ณต๋๊ธฐ ๋๋ฌธ์ด๋ค ใทใท.
์ ํจ์ฑ ๊ฒ์ฌ
ProseMirror ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๋ ๋
ธ๋๋ฅผ ์กฐ์ํ๋ ๋ค์ํ ํจ์๋ค์ด ์์ง๋ง, ๊ทธ ๋ชจ๋ ํจ์๋ค์ด ์
๋ ฅ๊ฐ์ ์ ํจ์ฑ์ ๋ค ๊ฒ์ฌํด์ฃผ๋ ๊ฑด ์๋๋ค. transform ๊ฐ์ ๊ณ ์์ค(high-level) API๋ ์
๋ ฅ์ด ์ ํจํ์ง ์๋์ผ๋ก ๊ฒ์ฌํด์ฃผ์ง๋ง, NodeType.create ๊ฐ์ ์ ์์ค(primitive) ๋
ธ๋ ์์ฑ ๋ฉ์๋๋ค์ ์
๋ ฅ๊ฐ์ด ์ ํจํ์ง ํ์ธํ์ง ์๊ณ , ๊ทธ ์ฑ
์์ ์ฌ์ฉํ๋ ๊ฐ๋ฐ์์๊ฒ ๋งก๊ธด๋ค. ์๋ฅผ ๋ค์ด, NodeType.create๋ฅผ ์ด์ฉํ๋ฉด ์๋ชป๋ ๊ตฌ์กฐ์ ๋
ธ๋๋ ๋ง๋ค ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ slice์ ๊ฒฝ๊ณ(edge)๊ฐ ์ด๋ฆฐ ์ํ์ผ ๋(์: ๋ณต๋ถ์ ์ํ ๋ฌธ์ ์กฐ๊ฐ(slice) ๋ฑ)๋ ์ผ์์ ์ผ๋ก ์๋ชป๋ ๊ตฌ์กฐ์ ๋
ธ๋๋ฅผ ๋ง๋๋ ๊ฒ์ด ํฉ๋ฆฌ์ ์ผ ์๋ ์๋ค. ํ์ง๋ง ์ ์์ ์ธ ์ ์ฒด ๋
ธ๋๋ฅผ ์์ฑํ๊ฑฐ๋ ์ฌ์ฉํ ๋๋, createChecked()๋ผ๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํฌํจํ ์์ฑ ๋ฉ์๋๋ ์ด๋ฏธ ๋ง๋ ๋
ธ๋์ ๋ํด check()๋ฅผ ํธ์ถํด์ ๊ทธ ๋
ธ๋์ ๊ตฌ์กฐ๊ฐ ์ ํจํ์ง ๊ฒ์ฆํ ์ ์๋ค.
Marks
Marks๋ ์ธ๋ผ์ธ ์ฝํ
์ธ ์ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ ์คํ์ผ์ ๋ํ ๋ ์ฌ์ฉํ๋ค. ์คํค๋ง๋ ๋ฐ๋์ ์คํค๋ง์์ ํ์ฉํ๋ ๋งํฌ ํ์
์ ๋ชจ๋ ์ ์ธํด์ผ ํ๋ค. Mark types๋ ๋
ธ๋ ํ์
๊ณผ ๋น์ทํ ํํ์ ๊ฐ์ฒด์ด๋ฉฐ ๋งํฌ ๊ฐ์ฒด๋ฅผ ์๋ณํ๊ณ ๊ทธ์ ๋ํ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ์ธ๋ผ์ธ ์ฝํ
์ธ ๋ฅผ ํฌํจํ๋ ๋
ธ๋๋ ์คํค๋ง์ ์ ์๋ ๋ชจ๋ ๋งํฌ๋ฅผ ์์ ๋
ธ๋์ ์ ์ฉํ ์ ์๋ค. ํ์ง๋ง ์ด ๋์์ ๊ฐ ๋
ธ๋์ marks ์์ฑ์ ํตํด ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ค.
์์
paragraph ๋
ธ๋์ ํ
์คํธ์ strong ๊ณผ emphasis ๋งํฌ๋ค์ ์ง์ํ๋ ๊ฐ๋จํ ์คํค๋ง๋ค. ํ์ง๋ง heading์๋ ๋งํฌ๊ฐ ์ ์ฉ๋์ง ์๋๋ค.
const markSchema = new Schema({
nodes: {
doc: {content: "block+"},
paragraph: {group: "block", content: "text*", marks: "_"},
heading: {group: "block", content: "text*", marks: ""},
text: {inline: true}
},
marks: {
strong: {},
em: {}
}
})
JavaScript
๋ณต์ฌ
๋งํฌ์ ์งํฉ(marks ์์ฑ)์ ๋งํฌ ์ด๋ฆ์ด๋ ๋งํฌ ๊ทธ๋ฃน์ ๊ณต๋ฐฑ์ผ๋ก ๊ตฌ๋ถํ ๋ฌธ์์ด๋ก ํด์๋๋ค. "_"(์ธ๋์ค์ฝ์ด)๋ ์์ผ๋์นด๋๋ก ๋์ํ๋ฉฐ, ์คํค๋ง์ ์ ์๋ ๋ชจ๋ ๋งํฌ๋ฅผ ํ์ฉํ๋ค๋ ์๋ฏธ๋ค. "" (๋น ๋ฌธ์์ด)์ ๋งํฌ๋ฅผ ์ ํ ํ์ฉํ์ง ์๋๋ค๋ ์๋ฏธ๋ค.
์ ์์์์ paragraph ์ mark๋ _ ์ด๊ณ heading ์ mark๋ ๋น ๋ฌธ์์ด์ด๋ฏ๋ก paragraph๋ ์คํค๋ง ๋ด์ ๋ชจ๋ ๋งํฌ๋ฅผ ํ์ฉํ๊ณ , heading์ ์๋ฌด๊ฒ๋ ํ์ฉํ์ง ์๋๋ค. ๋ง์ฝ ์ผ๋ถ๋ง ํ์ฉํ๊ณ ์ถ๋ค๋ฉด marks: โstrong emโ ๊ณผ ๊ฐ์ด ์ฐ๋ฉด ๋๋ค. strong, em์ mark๋ง ํ์ฉํ๋ค๋ ๋ป์ด๋ค.
Attributes
๋ฌธ์ ์คํค๋ง๋ ๊ฐ ๋
ธ๋๋ ๋งํฌ๊ฐ ์ด๋ค ์์ฑ(attributes)์ ๊ฐ์ง ์ ์๋์ง๋ ํจ๊ป ์ ์ํ๋ค. ์๋ฅผ ๋ค์ด, heading ๋
ธ๋์ฒ๋ผ ๋ ๋ฒจ(level) ๊ฐ์ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์ ์ฅํด์ผ ํ๋ ๊ฒฝ์ฐ, ์ด๋ฌํ ๋ฐ์ดํฐ๋ attribute๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๊ฐ์ฅ ์ ์ ํ๋ค. ๊ฐ ๋
ธ๋๋ ๋งํฌ์ attribute๋ JSON์ผ๋ก ์ง๋ ฌํ ๊ฐ๋ฅํ, ํ๋ฒํ ๊ฐ์ฒด ํํ๋ก ํํ๋๋ค.
์ด๋ค ์์ฑ๋ค์ ํ์ฉํ ์ง ์ ์ํ๋ ค๋ฉด, ํด๋น ๋
ธ๋๋ ๋งํฌ์ spec์์ attrs ํ๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค. ์ด ํ๋๋ ์ ํ์ฌํญ์ด์ง๋ง, attribute๊ฐ ํ์ํ ๊ฒฝ์ฐ ๋ฐ๋์ ๋ช
์์ ์ผ๋ก ์ ์ํด์ผ ํ๋ค.
heading: {
content: "text*",
attrs: {level: {default: 1}}
}
JavaScript
๋ณต์ฌ
์ด ์คํค๋ง์์ heading ๋
ธ๋๋ฅผ ์์ฑํ๋ฉด, ๋ชจ๋ ์ธ์คํด์ค๋ .attrs.level ์์ฑ์ level์ด๋ผ๋ attribute๋ฅผ ๊ฐ์ง๊ฒ ๋๋ค. ์ด ์์ฑ์ด ๋ช
์๋์ง ์์ ์ฑ๋ก ๋
ธ๋๊ฐ ์์ฑ๋๋ฉด, ๊ธฐ๋ณธ๊ฐ์ผ๋ก 1์ด ์๋ ํ ๋น๋๋ค. ๋ง์ฝ attribute์ ๋ํด ๊ธฐ๋ณธ๊ฐ์ ์ง์ ํ์ง ์์๋๋ฐ, ๋
ธ๋๋ฅผ ์์ฑํ ๋ ํด๋น attribute๋ฅผ ์๋ตํ๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
๋ํ ๊ธฐ๋ณธ๊ฐ์ด ์๋ attribute๋ ProseMirror ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํด๋น ๋
ธ๋๋ฅผ ์๋ ์์ฑํ๋ ๊ฒ์ ๋ฐฉํดํ๊ฒ ๋๋ค. ์๋ฅผ ๋ค์ด, ํธ๋์คํผ์ด๋ createAndFill์ ์ฌ์ฉํ ๋, ์คํค๋ง ์ ์ฝ์ ๋ง์กฑ์ํค๊ธฐ ์ํด ๋น์ด ์๋ ๋
ธ๋๋ฅผ ์๋์ผ๋ก ์ฑ์ ๋ฃ์ด์ผ ํ ๋, ๊ธฐ๋ณธ๊ฐ์ ์ ์ ์์ด์ ํด๋น ๋
ธ๋๋ฅผ ๋ง๋ค ์ ์๊ฒ ๋๋ค.
์ด๋ฐ ์ด์ ๋๋ฌธ์, ๊ธฐ๋ณธ๊ฐ ์๋ attribute๋ฅผ ์๊ตฌํ๋ ๋
ธ๋๋ ์คํค๋ง ์์์ ๋ฐ๋์ ์กด์ฌํด์ผ ํ๋ ์์น(required position)์ ๋ ์ ์๋ค. ํธ์ง๊ธฐ๊ฐ ์คํค๋ง ์ ์ฝ์ ๊ฐ์ ํ๋ ค๋ฉด, ์ธ์ ๋ ์ง ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฑ์ด ๋น ๋
ธ๋๋ฅผ ๋ง๋ค์ด ๋ฃ์ ์ ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์กฐ๊ธ ๋ ์ค๋ช
์ ๋ถ์ฐํ์๋ฉด, ์๋์ ๊ฐ์ ์คํค๋ง๊ฐ ์๋ค๊ณ ํ์.
doc: { content: "heading paragraph+" }
JavaScript
๋ณต์ฌ
์ด๋ฐ ์คํค๋ง๊ฐ ์์ ๋, ๋ฌธ์๋ฅผ ์๋ก ๋ง๋ค๋ฉด ProseMirror๋ ์๋์ผ๋ก ๋ค์๊ณผ ๊ฐ์ด ์ฑ์ฐ๋ ค๊ณ ํ๋ค.
doc(
heading(...?) // ํ์๋๊น ๋ญ๊ฐ ๋ฃ์ด์ค์ผ ํจ
paragraph() // ์ต์ ํ๋ ํ์
)
JavaScript
๋ณต์ฌ
๊ทธ๋ฐ๋ฐ ํค๋ฉ ๋
ธ๋๊ฐ ๋ค์๊ณผ ๊ฐ์ด ์ ์๋์ด์๋ค๋ฉด ProseMirror๊ฐ heading์ ์๋์ผ๋ก ์์ฑํ๋ ค๊ณ ํ ๋, level ๊ฐ์ด ํ์์ธ๋ฐ ๊ธฐ๋ณธ๊ฐ๋ ์๊ณ ProseMirror๋ ์๋์ผ๋ก level ๊ฐ์ ์ถ์ธกํ ์ ์์ผ๋ฏ๋ก ์๋ฌ๊ฐ ๋๋ค.
heading: {
attrs: {
level: {} // ๊ธฐ๋ณธ๊ฐ์ด ์์
},
content: "text*"
}
JavaScript
๋ณต์ฌ
์ด๋ฐ ๋
ธ๋๋ ์์์ ๋งํ ๊ฒ์ฒ๋ผ ๊ธฐ๋ณธ ๊ฐ์ ์ง์ ํด์ค์ผ ํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด heading ๊ฐ์ ๋
ธ๋๋ฅผ ๋ฐ๋์ ํ์ํ ์์น(์: doc์ ์ฒซ ์์ ๋
ธ๋) ์ ๋ ์ ์๋ค.
ํ์ง๋ง prosemirror-model ๋ชจ๋์์ Attrs ์ ๋ํ ํ์
์ ์๋ฅผ ๋ณด๋ฉด ์ด๋ฌํ default์ ๋ํ ๊ฐ์ ๋ ๋์ด์์ง ์๋ค.
type Attrs = {
readonly [attr: string]: any;
};
TypeScript
๋ณต์ฌ
๋๋ฌธ์ tiptap๊ณผ ๊ฐ์ด default ํค๋ฅผ ํฌํจํ ํ์
์ ๊ฐ์ ํ๊ฑฐ๋ tiptap์ Attribute ํ์
์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
export declare type Attribute = {
default: any;
rendered?: boolean;
renderHTML?: ((attributes: Record<string, any>) => Record<string, any> | null) | null;
parseHTML?: ((element: HTMLElement) => any | null) | null;
keepOnSplit: boolean;
isRequired?: boolean;
};
TypeScript
๋ณต์ฌ
Serialization and Parsing
๋ธ๋ผ์ฐ์ ์์ ๋ฌธ์๋ฅผ ํธ์งํ ์ ์์ผ๋ ค๋ฉด, ๋ฌธ์์ ๊ฐ ๋
ธ๋๊ฐ ๋ธ๋ผ์ฐ์ ์ DOM ๊ตฌ์กฐ๋ก ํํ ๊ฐ๋ฅํด์ผ ํ๋ค.
์ด๋ฅผ ๊ฐ์ฅ ๊ฐ๋จํ๊ฒ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์, ๊ฐ ๋
ธ๋์ DOM ํํ ๋ฐฉ์์ ์คํค๋ง ์์ ์๋ toDOM ํ๋์ ๋ช
์ํ๋ ๊ฒ์ด๋ค.
toDOM ํ๋๋ ํจ์ ํํ๋ก ์ ์๋๋ฉฐ, ์ด ํจ์๋ ๋
ธ๋๋ฅผ ์ธ์๋ก ๋ฐ์, ๊ทธ ๋
ธ๋๋ฅผ ์ด๋ป๊ฒ DOM์ผ๋ก ๋ํ๋ผ์ง๋ฅผ ์ค๋ช
ํ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํด์ผ ํ๋ค. ์ด ๋ฐํ๊ฐ์ ์ค์ DOM ๋
ธ๋์ผ ์๋ ์๊ณ , DOMOutputSpec ํํ์ ๋ฐฐ์ด ํ์์ผ๋ก DOM ๊ตฌ์กฐ๋ฅผ ๋ฌ์ฌํ๋ ๊ฐ์ผ ์๋ ์๋ค.
์์
const schema = new Schema({
nodes: {
doc: {content: "paragraph+"},
paragraph: {
content: "text*",
toDOM(node) { return ["p", 0] }
},
text: {}
}
})
JavaScript
๋ณต์ฌ
["p", 0]๋ผ๋ ํํ์ ํด๋น ๋
ธ๋๋ฅผ HTML์ <p> ํ๊ทธ๋ก ๋ ๋๋งํ๊ฒ ๋ค๋ ์๋ฏธ๋ค. ์ฌ๊ธฐ์ 0์ ๋
ธ๋์ ์ฝํ
์ธ ๊ฐ ์ฝ์
๋ ์๋ฆฌ(hole)๋ฅผ ๋ํ๋ธ๋ค. ๋ง์ฝ HTML ์์ฑ์ ์ถ๊ฐํ๊ณ ์ถ๋ค๋ฉด, ํ๊ทธ ์ด๋ฆ ๋ค์ ๊ฐ์ฒด ํํ๋ก ๋ฃ์ ์ ์๋ค. ์๋ฅผ ๋ค์ด ["div", { class: "c" }, 0]์ <div class="c">๋ด์ฉ</div>์ ๋ํ๋ธ๋ค. Leaf ๋
ธ๋(์์์ด ์๋ ๋
ธ๋)๋ ์ฝํ
์ธ ๊ฐ ์๊ธฐ ๋๋ฌธ์, DOM ๊ตฌ์กฐ ์์ ๋ณ๋์ โholeโ์ ์ง์ ํ ํ์๊ฐ ์๋ค.
๋งํฌ(Mark) ์คํ์๋ ๋น์ทํ๊ฒ toDOM ๋ฉ์๋๋ฅผ ์ ์ํ ์ ์๋๋ฐ, ๋งํฌ๋ ํญ์ ์ฝํ
์ธ ๋ฅผ ํ๋์ ํ๊ทธ๋ก ๊ฐ์ธ๋ ํํ๋ก ๋ ๋๋งํด์ผ ํ๋ฏ๋ก, ๋ฐํ๋๋ ๋
ธ๋ ์์ ์ฝํ
์ธ ๊ฐ ๋ฐ๋ก ๋ค์ด๊ฐ๋ฉฐ, ๋ณ๋๋ก 0์ผ๋ก hole์ ์ง์ ํ ํ์๋ ์๋ค.
๋ํ ์ฌ์ฉ์๊ฐ ๋ฌด์ธ๊ฐ๋ฅผ ๋ถ์ฌ๋ฃ๊ฑฐ๋ ๋๋๊ทธํด์ ์๋ํฐ์ ๋ฃ์ ๊ฒฝ์ฐ, DOM์ผ๋ก๋ถํฐ ๋ฌธ์๋ฅผ ํ์ฑํด์ผ ํ๋ ์ํฉ์ด ์์ฃผ ๋ฐ์ํ๋ค. ์ด๋ฅผ ์ํด ProseMirror์ model ๋ชจ๋์๋ DOM ํ์ฑ ๊ธฐ๋ฅ๋ ํจ๊ป ํฌํจ๋์ด ์์ผ๋ฉฐ, ์คํค๋ง ์์ parseDOM ์์ฑ์ ์ฌ์ฉํด DOM์์ ๋ฌธ์๋ฅผ ์ด๋ป๊ฒ ์ฝ์ด์ฌ์ง ์ ์ํ๋ ๊ฒ์ด ๊ถ์ฅ๋๋ค.
parseDOM ํ๋๋ ํด๋น ๋
ธ๋๋ ๋งํฌ๊ฐ ์ด๋ค DOM ๊ตฌ์กฐ๋ก๋ถํฐ ํ์ฑ๋ ์ ์๋์ง๋ฅผ ์ค๋ช
ํ๋ ํ์ฑ ๊ท์น(parse rules)์ ๋ฐฐ์ด์ ์ ์ํ ์ ์๋ค. ์๋ฅผ ๋ค์ด, ๊ธฐ๋ณธ ์คํค๋ง์์๋ emphasis ๋งํฌ๋ฅผ ์๋์ ๊ฐ์ด ์ ์ํ๋ค.
parseDOM: [
{tag: "em"}, // Match <em> nodes
{tag: "i"}, // and <i> nodes
{style: "font-style=italic"} // and inline 'font-style: italic'
]
JavaScript
๋ณต์ฌ
์ด์ฒ๋ผ tag ๊ฐ์๋ CSS ์ ํ์(CSS selector) ๋ฅผ ์ธ ์ ์๊ธฐ ๋๋ฌธ์, ์๋ฅผ ๋ค์ด "div.myclass" ๊ฐ์ ํน์ ํด๋์ค๋ฅผ ๊ฐ์ง ์์๋ฅผ ์ง์ ํ๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค. ๋ํ style์ ์ฌ์ฉํ๋ฉด ์ธ๋ผ์ธ CSS ์คํ์ผ ๊ฐ์ ๊ธฐ์ค์ผ๋ก๋ ๋งํฌ๋ฅผ ์ธ์์ํฌ ์ ์๋ค.
์คํค๋ง์ parseDOM ์ ๋ณด๊ฐ ํฌํจ๋์ด ์๋ค๋ฉด, DOMParser.fromSchema ๋ฉ์๋๋ฅผ ํตํด ํด๋น ์คํค๋ง์ ๋ง๋ DOMParser ๊ฐ์ฒด๋ฅผ ์์ฑํ ์ ์๋ค.
ProseMirror ์๋ํฐ๋ ์ด ๊ธฐ๋ฅ์ ํ์ฉํด ๊ธฐ๋ณธ ํด๋ฆฝ๋ณด๋ ํ์๋ฅผ ๋ง๋ ๋ค. ๋ฌผ๋ก ํ์ํ๋ค๋ฉด clipboardParser ํ๋กํผํฐ๋ฅผ ํตํด ์ด ํ์๋ฅผ ์ง์ ์ค๋ฒ๋ผ์ด๋ํ ์๋ ์๋ค.
ProseMirror ๋ฌธ์๋ ๋ํ ๋ด์ฅ๋ JSON ์ง๋ ฌํ ํฌ๋งท์ ์ง์ํ๋ค. ๋ฌธ์ ๋
ธ๋์ .toJSON()์ ํธ์ถํ๋ฉด, JSON.stringify๋ก ์์ ํ๊ฒ ๋ฌธ์์ดํํ ์ ์๋ ๊ฐ์ฒด๋ฅผ ์ป์ ์ ์๋ค. ๋ฐ๋๋ก ์คํค๋ง ๊ฐ์ฒด์ nodeFromJSON() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด, ์ด JSON ํํ์ ๋ค์ ProseMirror ๋ฌธ์ ๊ฐ์ฒด๋ก ๋๋๋ฆด ์ ์๋ค.
Extending a schema
Schema ์์ฑ์์ ์ ๋ฌ๋๋ nodes์ marks ์ต์
์ ์ผ๋ฐ์ ์ธ ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ์ฒด๋ฟ ์๋๋ผ OrderedMap ๊ฐ์ฒด๋ ์ฌ์ฉํ ์ ์๋ค. ํ ๋ฒ ์คํค๋ง๊ฐ ์์ฑ๋๋ฉด, schema.spec.nodes์ schema.spec.marks๋ ํญ์ OrderedMap ํํ๋ก ์ ๊ณต๋๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ค๋ฅธ ์คํค๋ง๋ฅผ ํ์ฅํ๊ฑฐ๋ ์์ ํ๋ ๋ฐ ํ์ฉํ ์ ์๋ค.
OrderedMap์ ๋
ธ๋ ์ธํธ๋ฅผ ์ ์ฐํ๊ฒ ์์ ํ ์ ์๋ ๋ค์ํ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค. ์๋ฅผ ๋ค์ด, schema.spec.nodes.remove("blockquote")์ฒ๋ผ ์์ฑํ๋ฉด, ๊ธฐ์กด ์คํค๋ง์์ blockquote ๋
ธ๋๋ฅผ ์ ๊ฑฐํ ์๋ก์ด ๋
ธ๋ ์ธํธ๋ฅผ ๋ง๋ค ์ ์๋ค. ์ด๋ ๊ฒ ์์ ํ ๊ฒฐ๊ณผ๋ ๋ค์ ์๋ก์ด ์คํค๋ง๋ฅผ ๋ง๋ค ๋ nodes ์ต์
์ผ๋ก ๋๊ฒจ์ค ์ ์๋ค.
์ถ๊ฐ๋ก, schema-list ๋ชจ๋์ ๋ฆฌ์คํธ ๊ด๋ จ ๋
ธ๋๋ค์ ๊ธฐ์กด ๋
ธ๋์
์ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์๋๋ก ๋์์ฃผ๋ addListNodes ๋ฉ์๋๋ฅผ ํจ๊ป ์ ๊ณตํ๋ค. ์ด๋ฐ ๋ฐฉ์์ผ๋ก ProseMirror ์คํค๋ง๋ ์ ์ง์ ํ์ฅ๊ณผ ์ปค์คํฐ๋ง์ด์ง์ด ๊ฐ๋ฅํ๋ค.
์ฆ, ProseMirror์์ ๊ธฐ๋ณธ ์คํค๋ง๊ฐ ์๊ณ , ์ฌ๊ธฐ์ "ํน์ ๋
ธ๋๋ฅผ ๋นผ๊ฑฐ๋ ์ถ๊ฐํด์" ์๋ก์ด ์คํค๋ง๋ฅผ ๋ง๋ค๊ณ ์ถ์ ๋, ์๋ฅผ ๋ค์ดโฆ
โข
blockquote ๋
ธ๋๋ ๋นผ๊ณ ์ถ๊ณ
โข
bullet_list, ordered_list ๊ฐ์ ๋ฆฌ์คํธ ๊ด๋ จ ๋
ธ๋๋ ์๋ก ์ถ๊ฐํ๊ณ ์ถ๋ค
์ด๋ด ๋ ๊ธฐ์กด ์คํค๋ง์ nodes๋ฅผ ๊ทธ๋ฅ ๊ฐ์ฒด์ฒ๋ผ ๋ค๋ฃจ๋ ๊ฒ ์๋๋ผ, OrderedMap์ด๋ผ๋ ํน์ํ ์๋ฃ๊ตฌ์กฐ๋ฅผ ์ด์ฉํด์ ์ ์ฐํ๊ฒ ์์ ํ ์ ์๋ค.
์์
import { Schema } from 'prosemirror-model'
import { schema as basicSchema } from 'prosemirror-schema-basic'
import { addListNodes } from 'prosemirror-schema-list'
// 1. ๊ธฐ๋ณธ ์คํค๋ง์ nodes๋ OrderedMap ํํ๋ก ๋์ด ์์
const originalNodes = basicSchema.spec.nodes
// 2. blockquote ๋
ธ๋๋ฅผ ์ ๊ฑฐ
const nodesWithoutBlockquote = originalNodes.remove('blockquote')
// 3. ๋ฆฌ์คํธ ๋
ธ๋๋ฅผ ์ถ๊ฐ (bullet_list, ordered_list ๋ฑ)
const extendedNodes = addListNodes(
nodesWithoutBlockquote,
'paragraph block*',
'block'
)
// 4. ์๋ก์ด ์คํค๋ง ์์ฑ
const mySchema = new Schema({
nodes: extendedNodes,
marks: basicSchema.spec.marks, // ๋งํฌ๋ ๊ทธ๋๋ก ์ฌ์ฉ
})
TypeScript
๋ณต์ฌ
2. Editor State
์๋ํฐ์ ์ํ(state)๋ ์ด๋ค ์์๋ค๋ก ๊ตฌ์ฑ๋ ๊น? ๊ฐ์ฅ ๊ธฐ๋ณธ์ด ๋๋ ๊ฒ์ ๋น์ฐํ ๋ฌธ์(document) ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ํ์ฌ ์ฌ์ฉ์๊ฐ ์ปค์๋ฅผ ์ด๋์ ๋๊ณ ์๋์ง๋ฅผ ๋ํ๋ด๋ ์ ํ(selection) ์ ๋ณด๋ ํ์ํ๋ค.
๋ ํ๋ ์ค์ํ ๊ฒ์, ์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ ์์ง ์
๋ ฅ์ ์์ํ์ง ์์์ง๋ง bold ๊ฐ์ ๋งํฌ๋ฅผ ํ์ฑํํ๊ฑฐ๋ ๋นํ์ฑํํ ๊ฒฝ์ฐ์ฒ๋ผ, ํ์ฌ ์ ์ฉํ ์์ ์ธ ๋งํฌ ์ํ(storedMarks) ๋ ์ ์ฅํด์ผ ํ๋ค.
ProseMirror์ ์๋ํฐ ์ํ๋ ์ด ์ธ ๊ฐ์ง๋ฅผ ๊ธฐ๋ณธ ๊ตฌ์ฑ ์์๋ก ๊ฐ๋๋ค. ์ด๋ค์ ๊ฐ๊ฐ ๋ค์๊ณผ ๊ฐ์ ํ๋กํผํฐ๋ก ์ ๊ทผํ ์ ์๋ค.
โข
doc: ํ์ฌ ๋ฌธ์
โข
selection: ํ์ฌ ์ปค์ ๋๋ ์ ํ ์์ญ
โข
storedMarks: ์์ง ์
๋ ฅ์ ์ ํ์ง๋ง ์ ์ฉํ ๋งํฌ ์ํ
์๋ฅผ ๋ค์ด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ก ์ํ๋ฅผ ์์ฑํ ์ ์๋ค.
import {schema} from "prosemirror-schema-basic"
import {EditorState} from "prosemirror-state"
let state = EditorState.create({schema})
console.log(state.doc.toString()) // An empty paragraph
console.log(state.selection.from) // 1, the start of the paragraph
JavaScript
๋ณต์ฌ
์ด ์์์์ ์๋ํฐ์ ํ์ฌ ์ํ๋ ์์ง ๋น์ด์๋ paragraph์ ์ํ์ด๊ณ , selection์ ์กฐํํ๋ฉด ํ์ฌ ์ปค์์ ์์น๋ ๋ฌธ๋จ์ ๋งจ ์ฒซ๋ฒ์งธ์ด๋ฏ๋ก 1์ด ์ถ๋ ฅ๋๋ค.
ํ์ง๋ง ์๋ํฐ์ ์ํ๋ ์ด ๊ธฐ๋ณธ ์์๋ค๋ง์ผ๋ก ๋๋์ง ์๋๋ค.์๋ฅผ ๋ค์ด undo/redo ๊ธฐ๋ฅ์ ๋ณ๊ฒฝ ์ด๋ ฅ์ ์ ์ฅํด์ผ ํ๋๋ฐ, ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ํ๋ฌ๊ทธ์ธ ํํ๋ก ์ถ๊ฐ์ ์ธ ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์ ์์ด์ผ ํ๋ค. ๊ทธ๋์ ProseMirror๋ ํ์ฌ ํ์ฑํ๋ ํ๋ฌ๊ทธ์ธ ์งํฉ๋ state์ ํฌํจ๋๋ฉฐ, ๊ฐ ํ๋ฌ๊ทธ์ธ์ ์์ ๋ง์ ์ํ ์ฌ๋กฏ์ ์ ์ํ์ฌ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ์งํ ์ ์๋ค.
Selection
ProseMirror๋ ์ฌ๋ฌ ์ข
๋ฅ์ ์ ํ(selection) ์ ์ง์ํ๋ฉฐ, ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ฌ์ฉ์ ์ฝ๋์์ ์ปค์คํ
selection ํ์
์ ์ ์ํ๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค. ์ ํ์ Selection ํด๋์ค(๋๋ ๊ทธ ์๋ธํด๋์ค)์ ์ธ์คํด์ค๋ก ํํ๋๋ค. ๋ฌธ์๋ ๊ธฐํ ์ํ ๊ด๋ จ ๊ฐ๋ค๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก, selection ๊ฐ์ฒด๋ ๋ถ๋ณ(immutable) ์ด๋ค.
์ฆ, ์ ํ์ ๋ณ๊ฒฝํ๋ ค๋ฉด ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ์์ ํ๋ ๊ฒ์ด ์๋๋ผ, ์๋ก์ด selection ๊ฐ์ฒด์ ์ด๋ฅผ ๋ด์ ์๋ก์ด state๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค. ๋ชจ๋ selection์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์ ์ ๋ณด๋ฅผ ๊ฐ์ง๋ค.
โข
from: ์ ํ์ ์์ ์์น
โข
to: ์ ํ์ ๋ ์์น
์ด ๋ ๊ฐ์ ํ์ฌ ๋ฌธ์ ์์ ๊ตฌ์ฒด์ ์ธ ์์น(position) ๋ฅผ ๋ํ๋ธ๋ค. ๋ํ ๋ง์ selection ํ์
์์๋ anchor์ head๋ผ๋ ๊ฐ๋
๋ ์กด์ฌํ๋ค.
โข
anchor: ์ปค์๊ฐ ์์๋ ๊ณณ
โข
head: ํ์ฌ ์ปค์๊ฐ ๋์ฐฉํ ์ง์
์๋ฅผ ๋ค์ด, Hello, world! ๋ฌธ์์์ "Hello"์ H๋ 1๋ฒ ์์น, !์ 13๋ฒ ์์น๋ผ๊ณ ๊ฐ์ ํ๋ค. ์ฌ์ฉ์๊ฐ ๋ง์ฐ์ค๋ก "Hello"์ H์์ ์์ํด์ o๊น์ง ๋๋๊ทธํ๋ฉด anchor๋ H ์์น (1๋ฒ)๊ฐ ๋๊ณ , head: o ์์น (6๋ฒ)๊ฐ ๋๋ค.
TextSelection์ from๊ณผ to๋ ํญ์ ์ธ๋ผ์ธ ์ฝํ
์ธ ๋ฅผ ํ์ฉํ๋ ๋
ธ๋ ๋ด๋ถ๋ฅผ ๊ฐ๋ฆฌ์ผ์ผ ํ๋ค. ProseMirror ์ฝ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ NodeSelection๋ ์ง์ํ๋ค. ์ด ํ์
์ ๋ฌธ์์์ ํ๋์ ๋
ธ๋ ์ ์ฒด๊ฐ ์ ํ๋ ๋ ์ฌ์ฉ๋๋ค.
์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ ctrl ๋๋ cmd ํค๋ฅผ ๋๋ฅธ ์ฑ ๋
ธ๋๋ฅผ ํด๋ฆญํ์ ๋ ๋ฐ์ํ๋ ๊ฒฝ์ฐ๊ฐ ์ด์ ํด๋นํ๋ค(Mac์์๋ ctrl). NodeSelection์ ์ ํ๋ ๋
ธ๋์ ์ง์ ์์น๋ถํฐ ์งํ ์์น๊น์ง๋ฅผ ๋ฒ์๋ก ์ผ๋๋ค. ์ฆ, ํน์ ๋
ธ๋ ์์ฒด๊ฐ ํต์งธ๋ก ์ ํ๋ ์ํ๊ฐ ๋๋ค.
๋ฐ๋๋ก, ์ด๋ฒ์ ์ฌ์ฉ์๊ฐ "Hello"์ o์์ ์์ํด์ H๊น์ง ๋๋๊ทธํ๋ฉด ์
๋ ์
๊ฐ์ ์๋์ ๊ฐ๋ค.
selection = {
anchor: 6,
head: 1,
from: 1,
to: 6
}
TypeScript
๋ณต์ฌ
from, to๋ ํญ์ ์์ ๊ฐ๋ถํฐ ํฐ ๊ฐ๊น์ง ์ ๋ ฌ๋์ง๋ง, anchor, head๋ ์ฌ์ฉ์์ ์ค์ ๋๋๊ทธ ๋ฐฉํฅ์ ๋ณด์กดํ๋ค.
๋ฐ๋ผ์ ๋ชจ๋ selection ๊ฐ์ฒด์๋ from, to๋ฟ๋ง ์๋๋ผ anchor, head๋ ๋ฐ๋์ ์กด์ฌํด์ผ ํ๋ค. ๊ฐ์ฅ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋๋ selection ํ์
์ TextSelection์ด๋ค. ์ด ํ์
์ ๋ค์ ๋ ๊ฐ์ง ๊ฒฝ์ฐ์ ์ฌ์ฉ๋๋ค.
โข
์ปค์๊ฐ ๊น๋นก์ด๋ ์์น (์ฆ, anchor์ head๊ฐ ๊ฐ์ ๋)
โข
ํ
์คํธ ์ผ๋ถ๊ฐ ๋๋๊ทธ๋ก ์ ํ๋์์ ๋
Transactions
์ผ๋ฐ์ ์ธ ํธ์ง ์ํฉ์์๋ ProseMirror์ ์ํ(state)๋ ์ด์ ์ํ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ์ง์ ์ผ๋ก ๊ฐฑ์ ๋๋ค. ํน๋ณํ ๊ฒฝ์ฐ(์: ์๋ก์ด ๋ฌธ์๋ฅผ ๋ถ๋ฌ์ฌ ๋)๋ฅผ ์ ์ธํ๋ฉด, ๊ธฐ์กด ์ํ์์ ์๋ก์ด ์ํ๋ฅผ ํ์์ํค๋ ๋ฐฉ์์ด ๊ธฐ๋ณธ ํ๋ฆ์ด๋ค.
์ํ ์
๋ฐ์ดํธ๋ transaction์ ๊ธฐ์กด ์ํ์ applyํ์ฌ ์ด๋ฃจ์ด์ง๋ค. ์ด ๊ณผ์ ์ ํตํด ์๋ก์ด ์ํ๊ฐ ์์ฑ๋๋ฉฐ, ๊ฐ๋
์ ์ผ๋ก๋ ํ ๋ฒ์ "์ฑ" ์ผ์ด๋๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผ๋๋ค.
์ฆ, ์ด์ ์ํ์ ํธ๋์ญ์
์ด ์ฃผ์ด์ง๋ฉด, ์ํ๋ฅผ ๊ตฌ์ฑํ๋ ๊ฐ ์์(doc, selection, storedMarks ๋ฑ)๊ฐ ์๋กญ๊ฒ ๊ณ์ฐ๋๊ณ , ์ด ๊ฐ๋ค์ ๋ชจ์ ์๋ก์ด ์ํ ๊ฐ์ฒด๊ฐ ๋ง๋ค์ด์ง๋ค.
let tr = state.tr
console.log(tr.doc.content.size) // 25
tr.insertText("hello") // Replaces selection with 'hello'
let newState = state.apply(tr)
console.log(tr.doc.content.size) // 30
JavaScript
๋ณต์ฌ
Transaction์ Transform์ ์๋ธํด๋์ค๋ก, ์ฌ๋ฌ ๊ฐ์ step์ ๋ฌธ์์ ์ฐจ๋ก๋ก ์ ์ฉํด ์ ๋ฌธ์๋ฅผ ๊ตฌ์ฑํ๋ ๊ธฐ๋ฅ์ ์์๋ฐ๋๋ค. ์ฌ๊ธฐ์ ๋ํด, ํธ๋์ญ์
์ ํ์ฌ ์ ํ(selection) ์ํ ๋ฑ ์ํ ๊ด๋ จ ์ ๋ณด๋ฅผ ํจ๊ป ์ถ์ ํ๋ฉฐ, replaceSelection ๊ฐ์ selection ์กฐ์์ ํนํ๋ ํธ์ ๋ฉ์๋๋ ํจ๊ป ์ ๊ณตํ๋ค.
ํธ๋์ญ์
์ ๋ง๋๋ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ์ EditorState ๊ฐ์ฒด์์ ์ ๊ณตํ๋ tr getter๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ์ด๋ ๊ฒ ํ๋ฉด ํ์ฌ ์ํ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ๋น์ด ์๋ ์ ํธ๋์ญ์
์ด ๋ง๋ค์ด์ง๊ณ , ์ฌ๊ธฐ์ step์ด๋ ๊ธฐํ ์
๋ฐ์ดํธ๋ฅผ ์ถ๊ฐํด๋๊ฐ ์ ์๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ํธ๋์ญ์
์ด ์ํ๋๋ฉด, ๊ธฐ์กด ์ ํ ์์ญ์ ๊ฐ step์ ๊ฑฐ์น๋ฉฐ ์๋์ผ๋ก map๋์ด ์๋ก์ด ๋ฌธ์ ๊ตฌ์กฐ์ ๋ง๊ฒ ์์น๊ฐ ์กฐ์ ๋๋ค.
ํ์ง๋ง ํ์ํ๋ค๋ฉด setSelection์ ํตํด ์ ํ ์์ญ์ ๋ช
์์ ์ผ๋ก ์ค์ ํ ์๋ ์๋ค.
let tr = state.tr
console.log(tr.selection.from) // โ 10
tr.delete(6, 8)
console.log(tr.selection.from) // โ 8 (moved back)
tr.setSelection(TextSelection.create(tr.doc, 3))
console.log(tr.selection.from) // โ 3
JavaScript
๋ณต์ฌ
๋น์ทํ๊ฒ, ํ์ฌ ํ์ฑํ๋ ๋งํฌ ์งํฉ(storedMarks)๋ ๋ฌธ์๋ ์ ํ ์์ญ์ด ๋ฐ๋๋ฉด ์๋์ผ๋ก ์ด๊ธฐํ๋๋ค. ํ์ํ ๊ฒฝ์ฐ setStoredMarks๋ ensureMarks๋ฅผ ์ฌ์ฉํด ์ ์ฉํ ๋งํฌ๋ฅผ ๋ช
์์ ์ผ๋ก ์ค์ ํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์ ์ธํฐ๋์
์ดํ์๋ ๋๋ถ๋ถ์ ๊ฒฝ์ฐscrollIntoView๋ฅผ ํธ์ถํด ์ ํ ์์ญ์ด ์คํฌ๋กค๋ทฐ์ ๋ํ๋๋๋ก ๊ฐ์ ํ๋ ๊ฒ์ด ์ข๋ค.
๋ง์ง๋ง์ผ๋ก, Transform ๊ณ์ด์ ๋ฉ์๋๋ค๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก, ๋๋ถ๋ถ์ Transaction ๋ฉ์๋๋ ์๊ธฐ ์์ (this) ์ ๋ฐํํ๋ฏ๋ก, .insertText(...).setSelection(...).scrollIntoView() ์ ๊ฐ์ด ๋ฉ์๋ ์ฒด์ด๋์ด ๊ฐ๋ฅํ๋ค.
Plugins
์๋ก์ด EditorState๋ฅผ ์์ฑํ ๋, ์ฌ์ฉํ ํ๋ฌ๊ทธ์ธ ๋ฐฐ์ด์ ํจ๊ป ์ง์ ํ ์ ์๋ค. ์ด๋ ๊ฒ ์ง์ ๋ ํ๋ฌ๊ทธ์ธ๋ค์ ํด๋น ์ํ ๊ฐ์ฒด์, ๊ทธ๋ก๋ถํฐ ํ์๋๋ ๋ชจ๋ ์ํ์์ ๊ณต์ ๋๋ฉฐ, ํธ๋์ญ์
์ฒ๋ฆฌ ๋ฐฉ์์ด๋ ์๋ํฐ ๋์ ์ ๋ฐ์ ์ํฅ์ ์ค ์ ์๋ค.
ํ๋ฌ๊ทธ์ธ์ Plugin ํด๋์ค์ ์ธ์คํด์ค์ด๋ฉฐ, ๊ฐ๋จํ ์ด๋ฒคํธ ์๋ต๋ถํฐ ๋ณต์กํ ์ํ ๊ด๋ฆฌ๊น์ง ๋ค์ํ ๊ธฐ๋ฅ์ ๋ชจ๋ธ๋งํ ์ ์๋ค.
๊ฐ์ฅ ๋จ์ํ ํ๋ฌ๊ทธ์ธ์ EditorView์ ํน์ props๋ฅผ ์ถ๊ฐํ๋ ์ฉ๋๋ก ์ฌ์ฉ๋๋๋ฐ, ์๋ฅผ ๋ค์ด ํค๋ณด๋ ์
๋ ฅ ์ด๋ฒคํธ์ ๋ฐ์ํ๋ ํ๋ฌ๊ทธ์ธ์ ๋ค์์ฒ๋ผ ๋ง๋ค ์ ์๋ค.
let myPlugin = new Plugin({
props: {
handleKeyDown(view, event) {
console.log("A key was pressed!")
return false // We did not handle this
}
}
})
let state = EditorState.create({schema, plugins: [myPlugin]})
JavaScript
๋ณต์ฌ
ํ๋ฌ๊ทธ์ธ์ด ์์ ๋ง์ ์ํ ๊ฐ์ ์ ์ฅํ๊ณ ์ถ๋ค๋ฉด, state ์์ฑ์ ์ฌ์ฉํด ์ด๊ธฐ๊ฐ๊ณผ ํธ๋์ญ์
์ฒ๋ฆฌ ๋ฐฉ์์ ์ ์ํ ์ ์๋ค. ์ด ํ๋ฌ๊ทธ์ธ์ ๋จ์ํ ์ง๊ธ๊น์ง ์ ์ฉ๋ ํธ๋์ญ์
์ ์๋ฅผ ์นด์ดํ
ํ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง๋ค. ํ์ฌ ์นด์ดํธ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์ถ์ ๋๋ getState() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
let transactionCounter = new Plugin({
state: {
init() { return 0 }, // ์ด๊ธฐ๊ฐ 0
apply(tr, value) { return value + 1 } // ํธ๋์ญ์
๋ฐ์ ์ ์นด์ดํฐ ์ฆ๊ฐ
}
})
function getTransactionCount(state) {
return transactionCounter.getState(state)
}
JavaScript
๋ณต์ฌ
ProseMirror์ ์ํ ๊ฐ์ฒด๋ ๋ถ๋ณ(immutable) ์ด๊ธฐ ๋๋ฌธ์, ํ๋ฌ๊ทธ์ธ ์ํ๋ ์ญ์ ๋ถ๋ณ ๊ฐ์ฒด์ฌ์ผ ํ๋ค. ์ฆ, ์ํ๋ฅผ ๋ณ๊ฒฝํ ํ์๊ฐ ์๋ค๋ฉด ๊ธฐ์กด ๊ฐ์ ์์ ํ๋ ๊ฒ์ด ์๋๋ผ ์ ๊ฐ์ ๋ฐํํด์ผ ํ๋ฉฐ, ๋ค๋ฅธ ์ฝ๋๊ฐ ์ด ๊ฐ์ ์ง์ ๋ณ๊ฒฝํด์๋ ์ ๋๋ค.
๋ฉํ๋ฐ์ดํฐ์ ํธ๋์ญ์ ๋งํน
ํ๋ฌ๊ทธ์ธ์ ํตํด ํธ๋์ญ์
์ ์ถ๊ฐ ์ ๋ณด(metadata)๋ฅผ ๋ถ์ด๋ ๊ฒ๋ ๋งค์ฐ ์ ์ฉํ๋ค. ์๋ฅผ ๋ค์ด undo ๊ธฐ๋ฅ์ ๊ตฌํํ ํ๋ฌ๊ทธ์ธ์ ๊ฒฝ์ฐ, ์ผ๋ฐ์ ์ธ ๋ณ๊ฒฝ ๋ด์ฉ์ undo ์คํ์ ์ถ๊ฐํ์ง๋ง, undo๊ฐ ์ค์ ์คํ๋๋ ์๊ฐ์๋ ํธ๋์ญ์
์ ํ์๋ฅผ ๋จ๊ฒจ์ undo ์คํ์์ ์ ๊ฑฐํ๊ณ , redo ์คํ์ ๋ฃ๋๋ก ํน๋ณ ์ฒ๋ฆฌํด์ผ ํ๋ค.
let transactionCounter = new Plugin({
state: {
init() { return 0 },
apply(tr, value) {
if (tr.getMeta(transactionCounter)) return value
else return value + 1
}
}
})
function markAsUncounted(tr) {
tr.setMeta(transactionCounter, true)
}
JavaScript
๋ณต์ฌ
์ฌ๊ธฐ์ getMeta()์ setMeta()์ ๋๊ฒจ์ง๋ ํค๋ ๋ฌธ์์ด๋ ๊ฐ๋ฅํ์ง๋ง, ์ด๋ฆ ์ถฉ๋์ ํผํ๊ธฐ ์ํด ํ๋ฌ๊ทธ์ธ ๊ฐ์ฒด ์์ฒด๋ฅผ ํค๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ๊ถ์ฅ๋๋ค. ProseMirror ๋ด๋ถ์์๋ ์๋ฏธ ์๋ ๋ฌธ์์ด ํค๊ฐ ๋ช ๊ฐ์ง ์กด์ฌํ๋๋ฐ, ์๋ฅผ ๋ค๋ฉด,
โข
"addToHistory": false undo ํ์คํ ๋ฆฌ์ ํฌํจ๋์ง ์๋๋ก ์ง์
โข
"paste": true ๋ถ์ฌ๋ฃ๊ธฐ ์ด๋ฒคํธ๋ก ์์ฑ๋ ํธ๋์ญ์
์์ ๋ช
์
tr.setMeta('addToHistory', false);
tr.setMeta('paste', true);
TypeScript
๋ณต์ฌ
์ด์ฒ๋ผ ํธ๋์ญ์
์ ๋ถ๊ฐ ์ ๋ณด๋ฅผ ๋ถ์ฐฉํ๋ฉด, ํ๋ฌ๊ทธ์ธ์ด ๋ณด๋ค ์ ๊ตํ๊ฒ ํธ๋์ญ์
์ ์ ์ดํ ์ ์๊ฒ ๋๋ค.
3. View Component
ProseMirror์ EditorView๋ ์๋ํฐ ์ํ(editor state)๋ฅผ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ฃผ๊ณ , ํด๋น ์ํ์ ๋ํด ์ฌ์ฉ์๊ฐ ํธ์ง ์์
์ ์ํํ ์ ์๋๋ก ํ๋ UI ์ปดํฌ๋ํธ์ด๋ค. ๋ค๋ง ProseMirror ์ฝ์ด ๋ทฐ ์ปดํฌ๋ํธ๊ฐ ๋ค๋ฃจ๋ โํธ์ง ์์
(editing actions)โ์ ๋ฒ์๋ ๊ฝค ์ ํ์ ์ด๋ค. ์ฌ๊ธฐ์ ๋งํ๋ ํธ์ง ์์
์ด๋ ๋ค์๊ณผ ๊ฐ์ ์ง์ ์ ์ธ ์ํธ์์ฉ๋ง์ ์๋ฏธํ๋ค.
โข
ํค๋ณด๋ ์
๋ ฅ
โข
๋ง์ฐ์ค ํด๋ฆญ
โข
๋ณต์ฌ/๋ถ์ฌ๋ฃ๊ธฐ
โข
๋๋๊ทธ
์ฆ, ๋ฉ๋ด๋ฅผ ๋์ฐ๋ UI๋, ๋ชจ๋ ํค๋ณด๋ ๋จ์ถํค๋ฅผ ์ ๊ณตํ๋ ๊ธฐ๋ฅ ๋ฑ์ ์ฝ์ด ๋ทฐ ์ปดํฌ๋ํธ์ ์ฑ
์์ ํฌํจ๋์ง ์์ผ๋ฉฐ, ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ํ๋ฌ๊ทธ์ธ(plugin) ์ ํตํด ๋ฐ๋ก ๊ตฌํํด์ผ ํ๋ค.
Editable DOM
๋ธ๋ผ์ฐ์ ๋ HTML ์์์ contentEditable ์์ฑ์ ์ง์ ํจ์ผ๋ก์จ ํด๋น ์์๋ฅผ ํธ์ง ๊ฐ๋ฅํ๊ฒ ๋ง๋ค ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์ด ์์ฑ์ด ํ์ฑํ๋๋ฉด, ํด๋น ์์๋ฅผ ํฌ์ปค์คํ ์ ์๊ณ , ์ ํ(selection)ํ ์ ์์ผ๋ฉฐ, ์ฌ์ฉ์๊ฐ ํค๋ณด๋๋ก ํ
์คํธ๋ฅผ ์
๋ ฅํ ์ ์๋ค.
ProseMirror์ EditorView๋ ์คํค๋ง์ ์ ์๋ toDOM ๋ฉ์๋๋ฅผ ํ์ฉํด ๋ฌธ์(document)์ DOM ํํ์ ์์ฑํ๊ณ , ํด๋น DOM ์์๋ฅผ ํธ์ง ๊ฐ๋ฅ ์ํ๋ก ๋ง๋ ๋ค. ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์๊ฐ ํด๋น ์์์ ํฌ์ปค์ค๋ฅผ ์ฃผ๋ฉด, ๋ธ๋ผ์ฐ์ ์ DOM selection ์ํ์ ์๋ํฐ ๋ด๋ถ selection ์ํ๊ฐ ์ผ์นํ๋๋ก ๋๊ธฐํํด์ค๋ค.
๋ํ ProseMirror๋ ์ฌ๋ฌ ์ข
๋ฅ์ DOM ์ด๋ฒคํธ์ ๋ํ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํ์ฌ, ๋ฐ์ํ ์ด๋ฒคํธ๋ฅผ ์ ์ ํ transaction์ผ๋ก ๋ณํํ๋ค. ์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ ๋ถ์ฌ๋ฃ๊ธฐ๋ฅผ ์ํํ๋ฉด, ๋ถ์ฌ๋ฃ์ ์ฝํ
์ธ ๋ clipboardParser๋ฅผ ํตํด ProseMirror ๋ฌธ์ ์กฐ๊ฐ(slice)์ผ๋ก ํ์ฑ๋๊ณ , ํด๋น ์์น์ ์ฝ์
๋๋ค.
ํํธ ๋ง์ ์ด๋ฒคํธ๋ ์ฒ์๋ถํฐ ProseMirror๊ฐ ์ง์ ์ฒ๋ฆฌํ์ง ์๊ณ , ์ผ๋จ ๋ธ๋ผ์ฐ์ ๊ฐ ๊ธฐ๋ณธ ๋์์ ์ํํ ๋ค, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ProseMirror๊ฐ ๋ค์ ํ์ธํ์ฌ ๋ด๋ถ ๋ชจ๋ธ๋ก ํด์ํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๋ค. ์๋ฅผ ๋ค์ด, ์ปค์ ์ด๋์ด๋ ์ ํ(selection) ์ฒ๋ฆฌ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์ ์ฒ๋ฆฌํ๋ค. ํนํ ์๋ฐฉํฅ ํ
์คํธ(bidirectional text, ์ผ์ชฝ์์ ์ค๋ฅธ์ชฝ์ผ๋ก ์ฝ๋ ์ธ์ด(LTR) ์ ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ์ฝ๋ ์ธ์ด(RTL) ๊ฐ ๊ฐ์ ๋ฌธ์ฅ์ด๋ ๋ฌธ์ ์์ ์์ฌ ์๋ ๊ฒฝ์ฐ) ๊ฐ์ ๋ณต์กํ ์ํฉ๊น์ง ๊ณ ๋ คํด์ผ ํ๋ฏ๋ก, ์ด๋ฐ ์ปค์ ๊ด๋ จ ํค ์
๋ ฅ์ด๋ ๋ง์ฐ์ค ๋์์ ๋ธ๋ผ์ฐ์ ์๊ฒ ๋งก๊ธฐ๊ณ , ๋ธ๋ผ์ฐ์ ๊ฐ ๋ง๋ DOM selection์ด ์๋ํฐ ์ํ์ ๋ค๋ฅด๋ค๊ณ ํ๋จ๋ ๊ฒฝ์ฐ์๋ง ์ ํ(selection)์ ๊ฐฑ์ ํ๋ ํธ๋์ญ์
์ ๋์คํจ์น(dispatch)ํ๋ค.
์ฌ์ง์ด ํ
์คํธ ์
๋ ฅ์กฐ์ฐจ๋ ๋ธ๋ผ์ฐ์ ์๊ฒ ์์ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. ์๋ํ๋ฉด ํ
์คํธ ์
๋ ฅ ๊ณผ์ ์ ์ง์ ๊ฐ๋ก์ฑ๋ฉด
โข
์๋ ์์ฑ
โข
์๋ ๋๋ฌธ์ ๋ณํ
โข
๋ชจ๋ฐ์ผ ์ธํฐํ์ด์ค์ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ๋ค(์: iOS์ ์๋ ์์ )
๊ฐ์ ๋ธ๋ผ์ฐ์ ๊ณ ์ ์ ํธ์ ๊ธฐ๋ฅ๋ค์ด ์ ๋๋ก ์๋ํ์ง ์์ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋์ ๋ธ๋ผ์ฐ์ ๊ฐ DOM์ ์
๋ฐ์ดํธํ๋ฉด, ProseMirror๋ ๊ทธ ๋ณ๊ฒฝ ์ฌํญ์ ๊ฐ์งํ๊ณ , ๋ณ๊ฒฝ๋ ๋ถ๋ถ์ ๋ค์ ํ์ฑํ ๋ค, ๋ฌธ์์ ์ฐจ์ด์ ์ ๊ธฐ๋ฐ์ผ๋ก ์๋ก์ด ํธ๋์ญ์
์ ์์ฑํด ๋ฌธ์๋ฅผ ๊ฐฑ์ ํ๋ค.
Data flow
ProseMirror์์ EditorView๋ ํน์ ํ EditorState๋ฅผ ํ๋ฉด์ ๋ ๋๋งํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ค ์ผ์ด ๋ฐ์ํ์ ๋(์: ์
๋ ฅ, ํด๋ฆญ ๋ฑ), ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํธ๋์ญ์
(transaction) ์ ์์ฑํ๊ณ ๋ธ๋ก๋์บ์คํธ(์ ํ) ํ๋ค.
์ด ํธ๋์ญ์
์ ์ผ๋ฐ์ ์ผ๋ก ์๋ก์ด ์ํ(state) ๋ฅผ ๋ง๋๋ ๋ฐ ์ฌ์ฉ๋๊ณ , ๊ทธ ์๋ก์ด ์ํ๋ EditorView์ updateState ๋ฉ์๋๋ฅผ ํตํด ๋ทฐ์ ๋ค์ ์ ๋ฌ๋๋ค.
์ด ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ ์ง๊ด์ ์ด๊ณ ์ํ์ ์ธ ๋ฐ์ดํฐ ํ๋ฆ(cyclic data flow)์ ๊ตฌ์ฑํ๋ค.
์ถ์ฒ: ํ๋ก์ค๋ฏธ๋ฌ ๊ฐ์ด๋ ๊ณต์๋ฌธ์
์ด ๋ฐฉ์์ JavaScript์์ ํํ ์ฐ์ด๋ ๋ช
๋ นํ ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ฒด๊ณ๋ณด๋ค ํจ์ฌ ๋จ์ํ๊ณ ์์ธก ๊ฐ๋ฅํ๋ค. ์ ํต์ ์ธ ๋ฐฉ์์ ๋ค์ํ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๊ณณ๊ณณ์์ ์ํ๋ฅผ ๋ฐ๊พธ๊ธฐ ๋๋ฌธ์, ๋ฐ์ดํฐ ํ๋ฆ์ด ๋งค์ฐ ๋ณต์กํ ๊ฑฐ๋ฏธ์ค์ฒ๋ผ ์ฝํ๊ฒ ๋๊ธฐ ์ฝ๋ค. ๋ฐ๋ฉด ProseMirror๋ ์ด ์ํ ๊ตฌ์กฐ ๋๋ถ์ ์ํ ํ๋ฆ์ ๊น๋ํ๊ฒ ์ ์งํ ์ ์๋ค.
์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์ ์ฐ๊ฒฐํ๊ณ ์ถ์ ๋?
ProseMirror์ ํธ๋์ญ์
์ ๋ ํฐ ์ฑ ๊ตฌ์กฐ์ ํตํฉํ๊ณ ์ถ๋ค๋ฉด, ํธ๋์ญ์
์ด dispatch ๋ ๋dispatchTransaction์ด๋ผ๋ prop์ ์ด์ฉํด ๊ฐ๋ก์ฑ๋(intercept) ๊ฒ์ด ๊ฐ๋ฅํ๋ค. ์ด ๋ฐฉ๋ฒ์ ์ฐ๋ฉด, ProseMirror์ ๋ด๋ถ ์ํ ๊ฐฑ์ ํ๋ฆ์ Redux์ ๊ฐ์ ์ํคํ
์ฒ์ ์ ์ฒด ์ํ ํ๋ฆ์ ํตํฉํ ์ ์๋ค.
์ฆ, ProseMirror์ ์ํ๋ฅผ ์ ํ๋ฆฌ์ผ์ด์
์ ์ ์ญ ์คํ ์ด(store) ์์ ๊ด๋ฆฌํ ์ ์๊ฒ ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ProseMirror๋ ๋ค๋ฅธ UI ์ํ์ ๋์ผํ ๋ฐฉ์์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด, ๋ณต์กํ ์๋ํฐ ๋ก์ง๋ ์ผ๊ด์ฑ ์๊ฒ ํต์ ํ ์ ์๋ค.
// The app's state
let appState = {
editor: EditorState.create({schema}),
score: 0
}
let view = new EditorView(document.body, {
state: appState.editor,
dispatchTransaction(transaction) {
update({type: "EDITOR_TRANSACTION", transaction})
}
})
// A crude app state update function, which takes an update object,
// updates the `appState`, and then refreshes the UI.
function update(event) {
if (event.type == "EDITOR_TRANSACTION")
appState.editor = appState.editor.apply(event.transaction)
else if (event.type == "SCORE_POINT")
appState.score++
draw()
}
// An even cruder drawing function
function draw() {
document.querySelector("#score").textContent = appState.score
view.updateState(appState.editor)
}
JavaScript
๋ณต์ฌ
appState๋ ์๋ํฐ ์ํ(editor)์ ์ ์(score)๋ผ๋ ๋ ๊ฐ์ง ์ํ๋ฅผ ๊ฐ์ง ์ ์ญ ์ํ ๊ฐ์ฒด์ด๋ค. ์ฆ, ProseMirror์ ์ํ๋ ์ด ๊ฐ์ฒด ์์์ ๊ฐ์ด ๊ด๋ฆฌ๋๊ณ ์๋ค.
์๋ํฐ๋ appState.editor๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋๋๋ฐ, ProseMirror ๋ด๋ถ์์ ์ด๋ค ํธ๋์ญ์
(transaction) ์ด ๋ฐ์ํ๋ฉด dispatchTransaction ํจ์๊ฐ ํธ์ถ๋๊ณ ์ด ํธ๋์ญ์
์ update()๋ผ๋ ์ฑ ๋ ๋ฒจ ์ํ ๊ฐฑ์ ํจ์๋ก ์ ๋ฌํ๋ค. ์ฆ, ProseMirror์ ์ํ ๋ณ๊ฒฝ ํ๋ฆ์ด appState๋ฅผ ํตํด ์ค์ ์ง์ค์์ผ๋ก ์ฒ๋ฆฌ๋๋ค.
update ํจ์๋ ๋ง์น Redux์ reducer์ฒ๋ผ event.type์ ๋ฐ๋ผ ๋ค๋ฅธ ๋์ํ๋๋ฐ, "EDITOR_TRANSACTION"์ด๋ฉด โ ํธ๋์ญ์
์ ์ ์ฉํด ์๋ก์ด ์๋ํฐ ์ํ๋ฅผ ๋ง๋ค๊ณ "SCORE_POINT"์ด๋ฉด ์ ์๋ฅผ 1 ์ฌํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋งค๋ฒ ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค draw()๋ฅผ ํธ์ถํด UI๋ฅผ ๊ฐฑ์ ํ๋ค.
๋ง์ง๋ง์ผ๋ก draw ํจ์์์๋ #score์ ํ์ฌ ์ ์๋ฅผ ๋ฐ์ํ๊ณ , view.updateState()๋ฅผ ํตํด ์๋ํฐ์ ์๋ก์ด ์ํ๋ฅผ ์ ์ฉํ๋ค.
ํต์ฌ ํ๋ฆ์ ์์ฝํ๋ฉด ์ด๋ ๊ฒ ๋๋ค.
์ฌ์ฉ์ ํธ์ง
โ transaction ๋ฐ์
โ dispatchTransaction์ผ๋ก app-level update() ํธ์ถ
โ appState.editor์ ํธ๋์ญ์
์ ์ฉ
โ draw() ํตํด UI ์
๋ฐ์ดํธ (DOM + EditorView)
View โ Transaction โ AppState โ View ๋ก ์ด์ด์ง๋ ์ํ์ ๋ฐ์ดํฐ ํ๋ฆ(cyclic data flow) ์ ๊ตฌ์ฑํ๋ค.
Efficient updating
updateState๋ฅผ ๊ตฌํํ๋ ๊ฐ์ฅ ๋จ์ํ ๋ฐฉ๋ฒ์, ์๋ํฐ ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ ์ฒด ๋ฌธ์๋ฅผ ๋ค์ ๊ทธ๋ฆฌ๋ ๊ฒ์ด๋ค. ํ์ง๋ง ๋ฌธ์๊ฐ ํด ๊ฒฝ์ฐ, ์ด๋ฐ ๋ฐฉ์์ ์ฑ๋ฅ์ ํฐ ๋ถ๋ด์ด ๋๊ธฐ ๋๋ฌธ์ ํ์ค์ ์ด์ง ์๋ค. ๋คํํ EditorView๋ ์ํ๋ฅผ ์
๋ฐ์ดํธํ ๋, ์ด์ ๋ฌธ์์ ์ ๋ฌธ์๋ฅผ ๋ชจ๋ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์, ๋ ๋ฌธ์๋ฅผ ๋น๊ตํด์ ๋ณ๊ฒฝ๋์ง ์์ ๋ถ๋ถ์ DOM์ ๊ทธ๋๋ก ๋๊ณ , ๋ฐ๋ ๋ถ๋ถ๋ง ๊ฐฑ์ ํ๋ ๋ฐฉ์์ผ๋ก ์ต์ ํํ ์ ์๋ค. ProseMirror๋ ์ด๋ฐ ๋ฐฉ์์ผ๋ก ์๋ํ๋ฉฐ, ๋๋ถ์ ์ผ๋ฐ์ ์ธ ํธ์ง ์ํฉ์์๋ DOM์ ๋ํด ์ต์ํ์ ์์
๋ง ์ํํด๋ ๋๋ค.
์์: ํ์ดํ์ฒ๋ผ ๋ธ๋ผ์ฐ์ ๊ฐ ๋จผ์ DOM์ ์์ ํ ๊ฒฝ์ฐ
์ฌ์ฉ์๊ฐ ํ
์คํธ๋ฅผ ์
๋ ฅํ๋ฉด, ๋ธ๋ผ์ฐ์ ๋ ์ด๋ฏธ DOM์ ํด๋น ํ
์คํธ๋ฅผ ์ถ๊ฐํ ์ํ๋ค. ์ด๋ด ๊ฒฝ์ฐ ProseMirror๋ DOM๊ณผ ๋ด๋ถ ์ํ๋ฅผ ๋ง์ถ๊ธฐ๋ง ํ๋ฉด ๋๋ฏ๋ก, ์ถ๊ฐ์ ์ธ DOM ์์ ์์ด ์ํ๋ง ๋๊ธฐํํ๋ฉด ๋๋ค. ํ์ง๋ง ๋ง์ฝ ํด๋น ํธ๋์ญ์
์ด ๋์ค์ ์ทจ์๋๊ฑฐ๋ ๋ณ๊ฒฝ๋๋ค๋ฉด, ProseMirror๋ DOM์ ๋๋๋ ค์ ๋ค์ ์ํ์ ์ผ์นํ๋๋ก ๋ง๋ ๋ค. ์ํ(state) ์ ํ๋ฉด(DOM) ์ ๋ถ์ผ์น๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด์๋ค.
ํํธ, EditorView๋ ์ฌ์ฉ์์ ์ ํ(selection)์ด ์ค์ ๋ก ์ํ์ ์ด๊ธ๋ฌ์ ๋๋ง DOM selection์ ๊ฐฑ์ ํ๋ค. ๊ทธ ์ด์ ๋, ๋ธ๋ผ์ฐ์ ๊ฐ selection๊ณผ ๊ด๋ จ๋ ์จ๊ฒจ์ง ์ํ๋ค์ ํจ๊ป ๊ด๋ฆฌํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ์๋ ๋ฐฉํฅํค๋ก ์งง์ ์ค์ ํต๊ณผํด ๋ค์ ๊ธด ์ค๋ก ์ด๋ํ ๋, ์ด์ ์ค์์์ ์ํ ์์น ์ ๋ณด๋ฅผ ๊ธฐ์ตํด๋๊ณ , ๋ค์ ์ค์์๋ ๊ฐ์ ๊ฐ๋ก ์์น๋ก ์ปค์๋ฅผ ์๋ ์ด๋์ํค๋ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฅ์ด ์๋ค. ๋ง์ฝ ProseMirror๊ฐ ๋งค๋ฒ DOM selection์ ๋ฎ์ด์จ๋ฒ๋ฆฌ๋ฉด, ์ด๋ฐ ๋ฏธ๋ฌํ ๋ธ๋ผ์ฐ์ ์ ๋์์ด ๊นจ์ง ์ ์๊ธฐ ๋๋ฌธ์, ์ ๋ง ์ด๊ธ๋ฌ์ ๋๋ง ์กฐ์ฌ์ค๋ฝ๊ฒ ๊ฐฑ์ ํ๋ค.
Props
ํ๋ก์ค๋ฏธ๋ฌ์ Props๋ผ๋ ์ฉ์ด๋ React์ ๊ทธ Props์์ ๊ฐ์ ธ์จ ๊ฐ๋
์ด ๋ง๋ค. Props๋ UI ์ปดํฌ๋ํธ์ ์ ๋ฌ๋๋ ํ๋ผ๋ฏธํฐ(์์ฑ)์ ๊ฐ์ ์ญํ ์ ํ๋ฉฐ, ์ด์์ ์ผ๋ก๋ ์ปดํฌ๋ํธ๊ฐ ์ด๋ค props๋ฅผ ๋ฐ๋๋์ ๋ฐ๋ฅธ ๋์์ด ์์ ํ ์ ์๋์ด์ผ ํ๋ค.
let view = new EditorView({
state: myState,
editable() { return false }, // Enables read-only behavior
handleDoubleClick() { console.log("Double click!") }
})
JavaScript
๋ณต์ฌ
์ด์ฒ๋ผ state ์ญ์ ํ๋์ prop์ด๋ฉฐ, ์ด ์ธ์ ๋ค๋ฅธ prop ๊ฐ๋ค๋ ์๊ฐ์ด ์ง๋๋ฉด์ ๋ณ๊ฒฝ๋ ์ ์์ง๋ง, ์ด๋ค์ ์ปดํฌ๋ํธ ์ธ๋ถ์์๋ง ๋ณ๊ฒฝ๋๋ฉฐ, ์ปดํฌ๋ํธ ์์ ์ด ์ง์ ๋ฐ๊พธ์ง ์๊ธฐ ๋๋ฌธ์ state๋ก ๊ฐ์ฃผ๋์ง ์๋๋ค. updateState() ๋ฉ์๋๋ ๊ฒฐ๊ตญ state prop์ ์
๋ฐ์ดํธํ๋ ๋จ์ถ ๋ฉ์๋์ผ ๋ฟ์ด๋ค.
ํ๋ฌ๊ทธ์ธ๋ props๋ฅผ ์ ์ธํ ์ ์๋ค
ํ๋ฌ๊ทธ์ธ ์ญ์ props ์์ฑ์ ํตํด ์์ ๋ง์ props๋ฅผ ๋ทฐ์ ์ ๋ฌํ ์ ์๋ค. ๋จ, state๋ dispatchTransaction์ฒ๋ผ ๋ทฐ์ ์ง์ ์ง์ ํด์ผ ํ๋ ํ์ props๋ ์ ์ธ๋ค. ์๋ฅผ ๋ค์ด, ๋ฌธ์ ํฌ๊ธฐ์ ๋ฐ๋ผ ์๋ํฐ๋ฅผ ์๋์ผ๋ก ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ๋ง๋๋ ํ๋ฌ๊ทธ์ธ์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ํ ์ ์๋ค
function maxSizePlugin(max) {
return new Plugin({
props: {
editable(state) { return state.doc.content.size < max }
}
})
}
JavaScript
๋ณต์ฌ
์ฌ๋ฌ ๊ณณ์์ ๋์ผํ prop์ ์ ์ธํ๋ฉด?
ํน์ prop์ด ์ง์ ์ง์ ๋ ๊ฒฝ์ฐ์ ํ๋ฌ๊ทธ์ธ ์์ชฝ์์ ์ค๋ณต ์ ์ธ๋ ์๋ ์๋๋ฐ, ์ด๋ฐ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ๋์ํ ์ง๋ prop์ ์ข
๋ฅ์ ๋ฐ๋ผ ๋ค๋ฅด๋ค. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ, ์ง์ ์ ๊ณต๋ prop์ด ์ฐ์ ๊ถ์ ๊ฐ์ง๋ค. ๊ทธ๋ค์์ผ๋ก๋ ๋ฑ๋ก๋ ํ๋ฌ๊ทธ์ธ๋ค์ด ์ ์ธํ props๊ฐ ์์๋๋ก ์ ์ฉ๋๋ค. props ๋ณ๋ก ์ค๋ณต ์ ์ธ๋ prop์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ๋ค๋ฅธ๋ฐ, ์๋ฅผ ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
prop ๋ณ ์ฒ๋ฆฌ ๋ฐฉ์ ์์
prop ์ข
๋ฅ | ๋ณํฉ ๋ฐฉ์ |
domParser | ๊ฐ์ฅ ๋จผ์ ๋ฐ๊ฒฌ๋ ๊ฐ ํ๋๋ง ์ฌ์ฉํ๊ณ , ๋๋จธ์ง๋ ๋ฌด์ |
handleKeyDown, handleClick ๋ฑ ํธ๋ค๋ฌ ํจ์ | ์ฒ์์ผ๋ก true๋ฅผ ๋ฐํํ ํธ๋ค๋ฌ๋ง ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌ |
attributes, decorations | ๋ชจ๋ ๊ฐ๋ค์ ํฉ์ณ์(union) ์ ์ฉ |
Decorations
Decorations(๋ฐ์ฝ๋ ์ด์
) ์ ProseMirror ์๋ํฐ ๋ทฐ๊ฐ ๋ฌธ์๋ฅผ ๊ทธ๋ฆด ๋ ์คํ์ผ์ด๋ ์๊ฐ์ ์์๋ฅผ ๋ง๋ถ์ผ ์ ์๋ ๋ฉ์ปค๋์ฆ์ด๋ค. ๋ฐ์ฝ๋ ์ด์
์ decorations prop์ ํตํด ์ ๊ณต๋๋ฉฐ, ์ด ์ธ ๊ฐ์ง ์ข
๋ฅ๊ฐ ์๋ค.
โข
โข
Widget (Decoration.widget): ๋ฌธ์์ ์ค์ ์กด์ฌํ์ง ์๋ DOM ๋
ธ๋๋ฅผ ํน์ ์์น์ ์ฝ์
ํจ (์: ์ปค์ ์์ ๋ฒํผ ํ์)
โข
Inline (Decoration.inline): ํน์ ๋ฒ์ ์์ ์ธ๋ผ์ธ ์ฝํ
์ธ ์ ์คํ์ผ์ด๋ ์์ฑ์ ์ ์ฉํจ
๋ฐ์ฝ๋ ์ด์
์ ํจ์จ์ ์ผ๋ก ๋ ๋๋งํ๊ณ ๋น๊ตํ๊ธฐ ์ํด, ๋จ์ผ ๊ฐ์ฒด๊ฐ ์๋ DecorationSet ํํ๋ก ์ ๊ณต๋์ด์ผ ํ๋ค. DecorationSet์ ์ค์ ๋ฌธ์ ํธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ๋ด ๋ธ ์๋ฃ๊ตฌ์กฐ๋ก, create ์ ์ ๋ฉ์๋๋ฅผ ํตํด ์์ฑํ ์ ์๋ค.
let purplePlugin = new Plugin({
props: {
decorations(state) {
return DecorationSet.create(state.doc, [
Decoration.inline(0, state.doc.content.size, {style: "color: purple"})
])
}
}
})
JavaScript
๋ณต์ฌ
์ ํ๋ฌ๊ทธ์ธ์ ์ ์ฒด ๋ฌธ์ ํ
์คํธ์ ๋ณด๋ผ์ ์ธ๋ผ์ธ ์คํ์ผ์ ์ ์ฉํ๋ ๋ฐ์ฝ๋ ์ด์
์ ์์ฑํ๋ค.
๋นํจ์จ์ ์ธ ๋ฐฉ์: ๋งค๋ฒ ๋ฐ์ฝ๋ ์ด์ ์๋ก ์์ฑ
๋ฌธ์๊ฐ ํฌ๊ฑฐ๋ ๋ฐ์ฝ๋ ์ด์
์ด ๋ง์ ๋๋, ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค DecorationSet.create๋ฅผ ๋ค์ ํธ์ถํ๋ ๊ฑด ๋นํจ์จ์ ์ด๋ค. ์ด๋ด ๋ ํ๋ฌ๊ทธ์ธ ๋ด๋ถ ์ํ์ DecorationSet์ ์ ์ฅํด๋๊ณ , ๋ฌธ์๊ฐ ๋ณ๊ฒฝ๋ ๋๋ ๊ธฐ์กด ๋ฐ์ฝ๋ ์ด์
์ map()์ผ๋ก ๋งคํํด์ ๋ณํ๋ ๋ฌธ์์ ๋ง๊ฒ ์กฐ์ ํ๋ ๋ฐฉ์์ด ์ข๋ค.
let specklePlugin = new Plugin({
state: {
init(_, {doc}) {
let speckles = []
for (let pos = 1; pos < doc.content.size; pos += 4)
speckles.push(Decoration.inline(pos - 1, pos, {style: "background: yellow"}))
return DecorationSet.create(doc, speckles)
},
apply(tr, set) { return set.map(tr.mapping, tr.doc) }
},
props: {
decorations(state) { return specklePlugin.getState(state) }
}
})
JavaScript
๋ณต์ฌ
์ด ํ๋ฌ๊ทธ์ธ์ ๋ฌธ์์ 4๋ฒ์งธ ๊ธ์๋ง๋ค ๋
ธ๋์ ๋ฐฐ๊ฒฝ์ ์ฃผ๋ ๋ฐ์ฝ๋ ์ด์
์ ์์ฑํ๋ค. ์ค์ฉ์ ์ด์ง ์์ง๋ง, ์๋ฅผ ๋ค์ด ๊ฒ์์ด ํ์ด๋ผ์ดํ
์ด๋ ์ฃผ์ ํ์ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌ์กฐ์ ์ ์ฌํ๋ค.
.map()์ ์ญํ
ํธ๋์ญ์
์ด ๋ฐ์ํ ๋๋ง๋ค apply() ๋ฉ์๋์์ DecorationSet.map()์ ํธ์ถํด ๊ธฐ์กด ๋ฐ์ฝ๋ ์ด์
์ ์ ๋ฌธ์ ๊ตฌ์กฐ์ ๋ง๊ฒ ์๋ ์ฌ๋ฐฐ์นํ๋ค. map()์ ๋ด๋ถ์ ์ผ๋ก ๋ฌธ์ ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ผ๊ฐ๋ ํธ๋ฆฌ ๊ตฌ์กฐ ๋๋ถ์, ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ๋ค์ ๊ณ์ฐํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ด ๋งค์ฐ ๋ฐ์ด๋๋ค.
์ค์ ์ฌ์ฉ์์๋?
์ค์ ํ๋ฌ๊ทธ์ธ์์๋ ๋จ์ํ map()๋ง ํ์ง ์๊ณ , apply() ์์์ ํธ๋์ญ์
์ ๋ณด๋ฅผ ๋ณด๊ณ
โข
โข
๋ฐ์ฝ๋ ์ด์
์ํ๋ฅผ ๋์ ์ผ๋ก ๊ฐฑ์ ํ๋ค. ์ด ๋ ํธ๋์ญ์
์ ๋ฉํ๋ฐ์ดํฐ๋ ๋ณ๊ฒฝ ๋ฒ์๋ฅผ ํ์ฉํ๋ฉด ๋์ฑ ์ ๊ตํ๊ฒ ์ ์ดํ ์ ์๋ค.
Node views
ProseMirror์์ ์๋ํฐ ๋ทฐ๊ฐ ๋ฌธ์๋ฅผ ๊ทธ๋ฆด ๋์ ์ํฅ์ ์ค ์ ์๋ ๋ ํ๋์ ๋ฐฉ๋ฒ์ด ์๋ค. ๋ฐ๋ก NodeView๋ฅผ ํ์ฉํ๋ ๊ฒ์ด๋ค. NodeView๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฌธ์์ ํน์ ๋
ธ๋์ ๋ํด ์์ UI ์ปดํฌ๋ํธ์ฒ๋ผ ๋์ํ๋ ๋ ๋๋ง ๋ก์ง์ ์ ์ํ ์ ์๋ค. ์ด๋ฅผ ํตํด ๊ฐ ๋
ธ๋์ DOM์ ์ง์ ๋ ๋๋งํ๊ณ , ์
๋ฐ์ดํธ ๋ฐฉ์์ด๋ ์ด๋ฒคํธ ์๋ต ๋ฑ์ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ค.
let view = new EditorView({
state,
nodeViews: {
image(node) { return new ImageView(node) }
}
})
class ImageView {
constructor(node) {
// The editor will use this as the node's DOM representation
this.dom = document.createElement("img")
this.dom.src = node.attrs.src
this.dom.addEventListener("click", e => {
console.log("You clicked me!")
e.preventDefault()
})
}
stopEvent() { return true }
}
JavaScript
๋ณต์ฌ
์ ์์์์ image ๋
ธ๋์ ๋ํด ImageView๋ผ๋ NodeView๋ฅผ ์ง์ ํ์ผ๋ฉฐ, ํด๋น ํด๋์ค์์๋ img ์๋ฆฌ๋จผํธ๋ฅผ ์ง์ ๋ง๋ค์ด ์ฌ์ฉ์์ ํด๋ฆญ ์ด๋ฒคํธ์ ๋ฐ์ํ๋๋ก ์ค์ ํ๋ค. ๋ํ stopEvent()๋ฅผ ์ ์ํจ์ผ๋ก์จ, ์ด DOM์์ ๋ฐ์ํ ์ด๋ฒคํธ๋ ProseMirror ๊ธฐ๋ณธ ์ฒ๋ฆฌ์์ ์ ์ธ๋๋ค.
๋ฌธ์ ์ํ๋ฅผ ๋ฐ๊พธ๋ ค๋ฉด ๋ ธ๋์ ์์น(getPos)๊ฐ ํ์ํ๋ค
NodeView๋ฅผ ํตํด ์ฌ์ฉ์์ ์ํธ์์ฉํ ์๋ ์์ง๋ง, ์ค์ ๋ก ๋ฌธ์ ๋ด๋ถ์ ๋
ธ๋๋ฅผ ๋ฐ๊พธ๋ ค๋ฉด ํด๋น ๋
ธ๋๊ฐ ๋ฌธ์ ๋ด์์ ์ด๋ ์์นํ๋์ง๋ฅผ ์์์ผ ํ๋ค. ์ด๋ฅผ ์ํด ProseMirror๋ NodeView ์์ฑ์์ getPos() ํจ์๋ฅผ ํจ๊ป ์ ๋ฌํด์ค๋ค. ์๋๋ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ํด๋ฆญํ์ ๋ alt ์์ฑ์ ๋ฐ๊พธ๋๋ก ์์ ํ ์์๋ค.
let view = new EditorView({
state,
nodeViews: {
image(node, view, getPos) { return new ImageView(node, view, getPos) }
}
})
class ImageView {
constructor(node, view, getPos) {
this.dom = document.createElement("img")
this.dom.src = node.attrs.src
this.dom.alt = node.attrs.alt
this.dom.addEventListener("click", e => {
e.preventDefault()
let alt = prompt("New alt text:", "")
if (alt) view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, {
src: node.attrs.src,
alt
}))
})
}
stopEvent() { return true }
}
JavaScript
๋ณต์ฌ
์ฌ๊ธฐ์ ์ฌ์ฉํ๋ setNodeMarkup ๋ฉ์๋๋ ํน์ ์์น์ ๋
ธ๋์ ๋ํด ๋
ธ๋ ํ์
์ด๋ ์์ฑ(attribute)์ ๋ณ๊ฒฝํ ์ ์๋ ํธ๋์ญ์
์ ๋ง๋ ๋ค.
์ฝํ ์ธ ๊ฐ ์๋ ๋ ธ๋๋ผ๋ฉด? (contentDOM ์ฌ์ฉ)
์์ ๋ณธ ์ด๋ฏธ์ง ๋
ธ๋๋ ์์ ์ฝํ
์ธ ๊ฐ ์๊ธฐ ๋๋ฌธ์ contentDOM ๊ฐ์ ๊ฑธ ์ ๊ฒฝ ์ฐ์ง ์์๋ ๋์ง๋ง,
๋จ๋ฝ(paragraph)์ฒ๋ผ ์ฝํ
์ธ ๋ฅผ ํฌํจํ๋ ๋
ธ๋๋ ์ํฉ์ด ๋ฌ๋ผ์ง๋ค.
let view = new EditorView({
state,
nodeViews: {
paragraph(node) { return new ParagraphView(node) }
}
})
class ParagraphView {
constructor(node) {
this.dom = this.contentDOM = document.createElement("p")
if (node.content.size == 0) this.dom.classList.add("empty")
}
update(node) {
if (node.content.size > 0) this.dom.classList.remove("empty")
else this.dom.classList.add("empty")
return true
}
}
JavaScript
๋ณต์ฌ
์ด ์์์์ ParagraphView๋ ๋จ๋ฝ์ ํ
์คํธ๊ฐ ์์ ๊ฒฝ์ฐ "empty" ํด๋์ค๋ฅผ ์ถ๊ฐํ๊ณ , ๋ด์ฉ์ด ์๊ธฐ๋ฉด ํด๋น ํด๋์ค๋ฅผ ์ ๊ฑฐํ๋ค. ์ฌ๊ธฐ์ ํต์ฌ์ contentDOM์ด๋ผ๋ ์์ฑ์ด๋ค. ProseMirror๋ ์ด ์์ฑ์ด ์ ์๋์ด ์์ผ๋ฉด, ๋
ธ๋์ ์ฝํ
์ธ ๋ฅผ ์๋์ผ๋ก contentDOM ์์ ๋ ๋๋งํ๊ณ , ๋ด์ฉ ๋ณ๊ฒฝ๋ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค๋ค. ๋ง์ฝ contentDOM์ ์ง์ ํ์ง ์์ผ๋ฉด, ํด๋น ๋
ธ๋์ ์์ ์ฝํ
์ธ ๋ ProseMirror๊ฐ ์ ํ ๊ฑด๋๋ฆฌ์ง ์์ผ๋ฉฐ, ์์ ํ ๊ฐ๋ฐ์๊ฐ ์ง์ ๋ ๋๋ง ๋ฐ ์
๋ฐ์ดํธ๋ฅผ ์ฑ
์์ ธ์ผ ํ๋ค.
update(node) ๋ฉ์๋๋?
NodeView์ update() ๋ฉ์๋๋ ์๋ํฐ ๋ด๋ถ์์ ํด๋น ๋
ธ๋์ ๋ณ๊ฒฝ์ด ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ฉฐ, ๊ธฐ์กด NodeView๊ฐ ์ ๋
ธ๋๋ฅผ ๊ณ์ ์ฌ์ฉํด๋ ๊ด์ฐฎ์์ง ํ๋จํ๋ ์ญํ ์ ํ๋ค. true๋ฅผ ๋ฐํํ๋ฉด ๊ธฐ์กด NodeView ์ธ์คํด์ค์ ์ธ๋ถ DOM์ ๊ทธ๋๋ก ์ ์งํ๊ณ ๊ทธ ๋ด๋ถ์ ์๋ ์ฝํ
์ธ (contentDOM)๋ง ๊ฐฑ์ ํ๋ค.
๋ฐ๋๋ก false๋ฅผ ๋ฐํํ๋ฉด ProseMirror๋ ์ด NodeView๋ฅผ ์๋ก ๋ง๋ค์ด์ผ ํ๋ค๊ณ ํ๋จํ๊ณ , ๊ธฐ์กด DOM๊ณผ ์ธ์คํด์ค๋ฅผ ์์ ํ ํ๊ธฐํ๊ณ , ์๋ก์ด NodeView๋ฅผ ์์ฑํด์ ์๋ก์ด DOM์ ๋ ๋๋งํ๋ค.
๋๋ฌธ์, ์
๋ฐ์ดํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ด๋ฃจ์ด์ก๋ค๋ฉด true๋ฅผ ๋ฐํํ๊ณ , ๊ทธ๋ ์ง ์๋ค๋ฉด false๋ฅผ ๋ฐํํด ์๋ก์ด DOM์ ๋ ๋๋งํด์ผํ๋ค.
โข
๋๋ ๋
ธ๋์ ํ์
์ด ๋ฐ๋์๊ฑฐ๋, ์ด NodeView๊ฐ ๋ค๋ฃฐ ์ ์๋ ๊ตฌ์กฐ๋ผ๋ฉด false๋ฅผ ๋ฐํํด์ผ ํ๊ณ
โข
๊ทธ๋ ์ง ์๊ณ DOM์ ๊ทธ๋๋ก ๋๊ณ ์คํ์ผ๋ง ๋ฐ๊พผ๋ค๋ ์ง, ๋ด๋ถ ์ฝํ
์ธ ๋ง ๋ฐ๊พธ๋ฉด ๋๋ ๊ฒฝ์ฐ true๋ฅผ ๋ฐํํด์ผํ๋ค.
์ ์์์์๋ update()๋ฅผ ํ์ฉํด ๋จ๋ฝ์ด ๋น์๋์ง ์ฌ๋ถ์ ๋ฐ๋ผ "empty" ํด๋์ค๋ฅผ ๋์ ์ผ๋ก ๊ด๋ฆฌํ๋ค. ๊ธฐ์กด DOM ์๋ฆฌ๋จผํธ๋ฅผ ๊ทธ๋๋ก ์ ์งํ ์ฑ, class๋ง ๋ฐ๊พธ๋ฉด ๋๋ฏ๋ก true๋ฅผ ๋ฐํํ ๊ฒ์ด๋ค.



