@moda-editor/core 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/dist/collab.d.ts +61 -0
  2. package/dist/collab.d.ts.map +1 -0
  3. package/dist/collab.js +67 -0
  4. package/dist/collab.js.map +1 -0
  5. package/dist/commands.d.ts +11 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +48 -4
  8. package/dist/commands.js.map +1 -1
  9. package/dist/conformance/editor-base.d.ts +7 -0
  10. package/dist/conformance/editor-base.d.ts.map +1 -0
  11. package/dist/conformance/editor-base.js +280 -0
  12. package/dist/conformance/editor-base.js.map +1 -0
  13. package/dist/conformance/editor-codeblock.d.ts +8 -0
  14. package/dist/conformance/editor-codeblock.d.ts.map +1 -0
  15. package/dist/conformance/editor-codeblock.js +402 -0
  16. package/dist/conformance/editor-codeblock.js.map +1 -0
  17. package/dist/conformance/editor-ime.d.ts +7 -0
  18. package/dist/conformance/editor-ime.d.ts.map +1 -0
  19. package/dist/conformance/editor-ime.js +166 -0
  20. package/dist/conformance/editor-ime.js.map +1 -0
  21. package/dist/conformance/editor-input.d.ts +8 -0
  22. package/dist/conformance/editor-input.d.ts.map +1 -0
  23. package/dist/conformance/editor-input.js +676 -0
  24. package/dist/conformance/editor-input.js.map +1 -0
  25. package/dist/conformance/editor-nodeview.d.ts +7 -0
  26. package/dist/conformance/editor-nodeview.d.ts.map +1 -0
  27. package/dist/conformance/editor-nodeview.js +120 -0
  28. package/dist/conformance/editor-nodeview.js.map +1 -0
  29. package/dist/conformance/editor-overlay.d.ts +8 -0
  30. package/dist/conformance/editor-overlay.d.ts.map +1 -0
  31. package/dist/conformance/editor-overlay.js +323 -0
  32. package/dist/conformance/editor-overlay.js.map +1 -0
  33. package/dist/conformance/fixtures.d.ts +4 -0
  34. package/dist/conformance/fixtures.d.ts.map +1 -0
  35. package/dist/conformance/fixtures.js +15 -0
  36. package/dist/conformance/fixtures.js.map +1 -0
  37. package/dist/conformance/index.d.ts +14 -0
  38. package/dist/conformance/index.d.ts.map +1 -0
  39. package/dist/conformance/index.js +28 -0
  40. package/dist/conformance/index.js.map +1 -0
  41. package/dist/conformance/types.d.ts +51 -0
  42. package/dist/conformance/types.d.ts.map +1 -0
  43. package/dist/conformance/types.js +2 -0
  44. package/dist/conformance/types.js.map +1 -0
  45. package/dist/conformance/widget.d.ts +3 -0
  46. package/dist/conformance/widget.d.ts.map +1 -0
  47. package/dist/conformance/widget.js +49 -0
  48. package/dist/conformance/widget.js.map +1 -0
  49. package/dist/conformance.d.ts +1 -58
  50. package/dist/conformance.d.ts.map +1 -1
  51. package/dist/conformance.js +3 -1608
  52. package/dist/conformance.js.map +1 -1
  53. package/dist/editor/extension.d.ts +51 -5
  54. package/dist/editor/extension.d.ts.map +1 -1
  55. package/dist/features/ace.d.ts +4 -0
  56. package/dist/features/ace.d.ts.map +1 -1
  57. package/dist/features/ace.js +57 -25
  58. package/dist/features/ace.js.map +1 -1
  59. package/dist/features/block-dnd.d.ts +7 -0
  60. package/dist/features/block-dnd.d.ts.map +1 -0
  61. package/dist/features/block-dnd.js +207 -0
  62. package/dist/features/block-dnd.js.map +1 -0
  63. package/dist/features/code-block.d.ts +12 -58
  64. package/dist/features/code-block.d.ts.map +1 -1
  65. package/dist/features/code-block.js +155 -630
  66. package/dist/features/code-block.js.map +1 -1
  67. package/dist/features/code-context.d.ts +81 -0
  68. package/dist/features/code-context.d.ts.map +1 -0
  69. package/dist/features/code-context.js +322 -0
  70. package/dist/features/code-context.js.map +1 -0
  71. package/dist/features/code-edit.d.ts +50 -0
  72. package/dist/features/code-edit.d.ts.map +1 -0
  73. package/dist/features/code-edit.js +412 -0
  74. package/dist/features/code-edit.js.map +1 -0
  75. package/dist/features/code-format.d.ts +21 -0
  76. package/dist/features/code-format.d.ts.map +1 -0
  77. package/dist/features/code-format.js +410 -0
  78. package/dist/features/code-format.js.map +1 -0
  79. package/dist/features/doc.d.ts.map +1 -1
  80. package/dist/features/doc.js +1 -1
  81. package/dist/features/doc.js.map +1 -1
  82. package/dist/features/heading.d.ts.map +1 -1
  83. package/dist/features/heading.js.map +1 -1
  84. package/dist/features/image.d.ts +14 -1
  85. package/dist/features/image.d.ts.map +1 -1
  86. package/dist/features/image.js +288 -11
  87. package/dist/features/image.js.map +1 -1
  88. package/dist/features/index.d.ts +7 -0
  89. package/dist/features/index.d.ts.map +1 -1
  90. package/dist/features/index.js +16 -0
  91. package/dist/features/index.js.map +1 -1
  92. package/dist/features/inline-style.d.ts.map +1 -1
  93. package/dist/features/inline-style.js +4 -1
  94. package/dist/features/inline-style.js.map +1 -1
  95. package/dist/features/link.d.ts.map +1 -1
  96. package/dist/features/link.js +1 -1
  97. package/dist/features/link.js.map +1 -1
  98. package/dist/features/markdown.d.ts +15 -0
  99. package/dist/features/markdown.d.ts.map +1 -0
  100. package/dist/features/markdown.js +80 -0
  101. package/dist/features/markdown.js.map +1 -0
  102. package/dist/features/marks.d.ts.map +1 -1
  103. package/dist/features/marks.js +76 -4
  104. package/dist/features/marks.js.map +1 -1
  105. package/dist/features/mention.d.ts +30 -0
  106. package/dist/features/mention.d.ts.map +1 -0
  107. package/dist/features/mention.js +89 -0
  108. package/dist/features/mention.js.map +1 -0
  109. package/dist/features/script.d.ts.map +1 -1
  110. package/dist/features/script.js.map +1 -1
  111. package/dist/features/search.d.ts +73 -0
  112. package/dist/features/search.d.ts.map +1 -0
  113. package/dist/features/search.js +252 -0
  114. package/dist/features/search.js.map +1 -0
  115. package/dist/features/shared.d.ts +69 -0
  116. package/dist/features/shared.d.ts.map +1 -0
  117. package/dist/features/shared.js +272 -0
  118. package/dist/features/shared.js.map +1 -0
  119. package/dist/features/table.d.ts +16 -3
  120. package/dist/features/table.d.ts.map +1 -1
  121. package/dist/features/table.js +309 -8
  122. package/dist/features/table.js.map +1 -1
  123. package/dist/features/testkit.d.ts.map +1 -1
  124. package/dist/features/testkit.js +1 -1
  125. package/dist/features/testkit.js.map +1 -1
  126. package/dist/history.d.ts.map +1 -1
  127. package/dist/history.js +65 -20
  128. package/dist/history.js.map +1 -1
  129. package/dist/index.d.ts +20 -7
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +17 -6
  132. package/dist/index.js.map +1 -1
  133. package/dist/model/schema.d.ts.map +1 -1
  134. package/dist/model/schema.js +2 -1
  135. package/dist/model/schema.js.map +1 -1
  136. package/dist/parse/html.d.ts.map +1 -1
  137. package/dist/parse/html.js +38 -6
  138. package/dist/parse/html.js.map +1 -1
  139. package/dist/parse/sanitize.d.ts.map +1 -1
  140. package/dist/parse/sanitize.js +14 -2
  141. package/dist/parse/sanitize.js.map +1 -1
  142. package/dist/presets/highlight.d.ts +17 -0
  143. package/dist/presets/highlight.d.ts.map +1 -1
  144. package/dist/presets/highlight.js +52 -4
  145. package/dist/presets/highlight.js.map +1 -1
  146. package/dist/presets/markdown-parse.d.ts +13 -0
  147. package/dist/presets/markdown-parse.d.ts.map +1 -0
  148. package/dist/presets/markdown-parse.js +398 -0
  149. package/dist/presets/markdown-parse.js.map +1 -0
  150. package/dist/presets/markdown.d.ts +45 -0
  151. package/dist/presets/markdown.d.ts.map +1 -0
  152. package/dist/presets/markdown.js +195 -0
  153. package/dist/presets/markdown.js.map +1 -0
  154. package/dist/state/apply.d.ts.map +1 -1
  155. package/dist/state/apply.js +19 -11
  156. package/dist/state/apply.js.map +1 -1
  157. package/dist/state/delete-ops.d.ts +34 -0
  158. package/dist/state/delete-ops.d.ts.map +1 -0
  159. package/dist/state/delete-ops.js +204 -0
  160. package/dist/state/delete-ops.js.map +1 -0
  161. package/dist/state/input-rules.d.ts +30 -0
  162. package/dist/state/input-rules.d.ts.map +1 -0
  163. package/dist/state/input-rules.js +70 -0
  164. package/dist/state/input-rules.js.map +1 -0
  165. package/dist/state/insert-ops.d.ts +54 -0
  166. package/dist/state/insert-ops.d.ts.map +1 -0
  167. package/dist/state/insert-ops.js +186 -0
  168. package/dist/state/insert-ops.js.map +1 -0
  169. package/dist/state/list-ops.d.ts +46 -0
  170. package/dist/state/list-ops.d.ts.map +1 -0
  171. package/dist/state/list-ops.js +231 -0
  172. package/dist/state/list-ops.js.map +1 -0
  173. package/dist/state/navigate.d.ts.map +1 -1
  174. package/dist/state/navigate.js +3 -1
  175. package/dist/state/navigate.js.map +1 -1
  176. package/dist/state/resolve-insert.d.ts +55 -0
  177. package/dist/state/resolve-insert.d.ts.map +1 -0
  178. package/dist/state/resolve-insert.js +118 -0
  179. package/dist/state/resolve-insert.js.map +1 -0
  180. package/dist/state/slice-ops.d.ts +46 -0
  181. package/dist/state/slice-ops.d.ts.map +1 -0
  182. package/dist/state/slice-ops.js +150 -0
  183. package/dist/state/slice-ops.js.map +1 -0
  184. package/dist/state/state.d.ts +6 -1
  185. package/dist/state/state.d.ts.map +1 -1
  186. package/dist/state/state.js +10 -0
  187. package/dist/state/state.js.map +1 -1
  188. package/dist/state/transaction.d.ts +40 -53
  189. package/dist/state/transaction.d.ts.map +1 -1
  190. package/dist/state/transaction.js +62 -829
  191. package/dist/state/transaction.js.map +1 -1
  192. package/dist/styles.css +472 -46
  193. package/dist/view/adapter.d.ts +84 -0
  194. package/dist/view/adapter.d.ts.map +1 -0
  195. package/dist/view/adapter.js +76 -0
  196. package/dist/view/adapter.js.map +1 -0
  197. package/dist/view/caret.d.ts +25 -0
  198. package/dist/view/caret.d.ts.map +1 -0
  199. package/dist/view/caret.js +43 -0
  200. package/dist/view/caret.js.map +1 -0
  201. package/dist/view/decoration.d.ts +9 -0
  202. package/dist/view/decoration.d.ts.map +1 -0
  203. package/dist/view/decoration.js +214 -0
  204. package/dist/view/decoration.js.map +1 -0
  205. package/dist/view/dirty-keys.d.ts +18 -0
  206. package/dist/view/dirty-keys.d.ts.map +1 -0
  207. package/dist/view/dirty-keys.js +31 -0
  208. package/dist/view/dirty-keys.js.map +1 -0
  209. package/dist/view/dom.d.ts.map +1 -1
  210. package/dist/view/dom.js +69 -27
  211. package/dist/view/dom.js.map +1 -1
  212. package/dist/view/hook-ctx.d.ts +32 -0
  213. package/dist/view/hook-ctx.d.ts.map +1 -0
  214. package/dist/view/hook-ctx.js +64 -0
  215. package/dist/view/hook-ctx.js.map +1 -0
  216. package/dist/view/ime.d.ts +72 -0
  217. package/dist/view/ime.d.ts.map +1 -0
  218. package/dist/view/ime.js +185 -0
  219. package/dist/view/ime.js.map +1 -0
  220. package/dist/view/input.d.ts +12 -12
  221. package/dist/view/input.d.ts.map +1 -1
  222. package/dist/view/input.js +122 -435
  223. package/dist/view/input.js.map +1 -1
  224. package/dist/view/keys.d.ts +25 -0
  225. package/dist/view/keys.d.ts.map +1 -0
  226. package/dist/view/keys.js +106 -0
  227. package/dist/view/keys.js.map +1 -0
  228. package/dist/view/line-nav.d.ts +13 -0
  229. package/dist/view/line-nav.d.ts.map +1 -0
  230. package/dist/view/line-nav.js +114 -0
  231. package/dist/view/line-nav.js.map +1 -0
  232. package/dist/view/mount.d.ts.map +1 -1
  233. package/dist/view/mount.js +48 -32
  234. package/dist/view/mount.js.map +1 -1
  235. package/dist/view/node-selection.d.ts +33 -0
  236. package/dist/view/node-selection.d.ts.map +1 -0
  237. package/dist/view/node-selection.js +107 -0
  238. package/dist/view/node-selection.js.map +1 -0
  239. package/dist/view/render.d.ts +11 -2
  240. package/dist/view/render.d.ts.map +1 -1
  241. package/dist/view/render.js +17 -3
  242. package/dist/view/render.js.map +1 -1
  243. package/dist/view/serialize.d.ts +8 -0
  244. package/dist/view/serialize.d.ts.map +1 -1
  245. package/dist/view/serialize.js +33 -5
  246. package/dist/view/serialize.js.map +1 -1
  247. package/package.json +1 -1
@@ -1,115 +1,21 @@
1
1
  import { nodeAt } from "../model/traverse.js";
2
2
  import { parseHTML } from "../parse/html.js";
3
3
  import { pointBefore, pointAfter } from "../state/navigate.js";
4
- import { normalizeRange, point, pointEq, textSelection } from "../state/selection.js";
5
- import { applyDomSelection, domSelection, domToPoint, PATH_ATTR, pointToDOM, } from "./dom.js";
6
- import { blockText, blockTextOffset, blockTextPoint, toggleMark, } from "../commands.js";
7
- /** format* inputType → 마크 타입 (스펙의 inputType 이름 ↔ 스키마 마크) */
8
- const FORMAT_MARKS = {
9
- formatBold: "bold",
10
- formatItalic: "italic",
11
- formatUnderline: "underline",
12
- formatStrikeThrough: "strike",
13
- formatSuperscript: "superscript",
14
- formatSubscript: "subscript",
15
- };
16
- /** drop 좌표 → doc Point — caretPositionFromPoint / Range 폴백 */
17
- function caretPointFromXY(doc, root, x, y) {
18
- const d = doc;
19
- const pos = doc.caretPositionFromPoint?.(x, y);
20
- if (pos)
21
- return domToPoint(root, pos.offsetNode, pos.offset);
22
- const r = d.caretRangeFromPoint?.(x, y);
23
- return r ? domToPoint(root, r.startContainer, r.startOffset) : null;
24
- }
25
- /**
26
- * 텍스트 범위가 한 블록 안에 있으면 입력 훅용 컨텍스트 — 블록을
27
- * 걸치는 범위는 훅이 받지 않는다 (BlockTextCtx 계약).
28
- */
29
- function blockCtx(editor, r, tr) {
30
- const blockPath = r.anchor.path.slice(0, -1);
31
- if (r.focus.path.length !== blockPath.length + 1 ||
32
- !r.focus.path.slice(0, -1).every((v, i) => v === blockPath[i])) {
33
- return null;
34
- }
35
- const block = nodeAt(editor.state.doc, blockPath);
36
- if (!block)
37
- return null;
38
- return {
39
- state: editor.state,
40
- tr,
41
- dispatch: () => editor.dispatch(tr),
42
- block,
43
- blockPath,
44
- text: blockText(block),
45
- anchor: r.anchor,
46
- focus: r.focus,
47
- charPoint: (off) => blockTextPoint(block, blockPath, off),
48
- offsetOf: (p) => blockTextOffset(block, p),
49
- };
50
- }
51
- /**
52
- * 어댑터 공용 dirty-key 헬퍼 — binding.dirtyPaths를 소비해 블록
53
- * 렌더 키를 안정적으로 버전업한다. 일반 블록은 path 그대로,
54
- * 외부 DOM 변경이 있었던 블록은 path@N으로 키가 바뀌어
55
- * 프레임워크가 내부 diff 대신 리마운트를 택한다.
56
- */
57
- export class DirtyKeyMap {
58
- constructor() {
59
- Object.defineProperty(this, "versions", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: new Map()
64
- });
65
- }
66
- /** rn.path.join(",")와 binding.dirtyPaths를 넣으면 렌더 키를 반환 */
67
- key(pathKey, dirty) {
68
- if (dirty?.delete(pathKey)) {
69
- this.versions.set(pathKey, (this.versions.get(pathKey) ?? 0) + 1);
70
- }
71
- const v = this.versions.get(pathKey);
72
- return v ? `${pathKey}@${v}` : pathKey;
73
- }
74
- }
75
- /** KeyboardEvent → "Mod-b"/"Shift-Enter" 형태 키 문자열 (PM 관례) */
76
- const LINE_NAV_KEYS = new Set([
77
- "Home",
78
- "End",
79
- "Shift-Home",
80
- "Shift-End",
81
- "Mod-ArrowLeft",
82
- "Mod-ArrowRight",
83
- "Mod-Shift-ArrowLeft",
84
- "Mod-Shift-ArrowRight",
85
- "Mod-Home",
86
- "Mod-End",
87
- "Mod-Shift-Home",
88
- "Mod-Shift-End",
89
- ]);
90
- function normalizeKey(e) {
91
- const mods = [];
92
- if (e.ctrlKey || e.metaKey)
93
- mods.push("Mod");
94
- if (e.altKey)
95
- mods.push("Alt");
96
- if (e.shiftKey)
97
- mods.push("Shift");
98
- const name = e.key.length === 1 ? e.key.toLowerCase() : e.key;
99
- return [...mods, name].join("-");
100
- }
4
+ import { normalizeRange, pointEq, textSelection } from "../state/selection.js";
5
+ import { caretPointFromXY, isLeafPoint } from "./caret.js";
6
+ import { applyDomSelection, domSelection, domToPoint, PATH_ATTR, } from "./dom.js";
7
+ import { blockCtx, dispatchFiles, rangeCtx } from "./hook-ctx.js";
8
+ import { createImeSession } from "./ime.js";
9
+ import { lineNav } from "./line-nav.js";
10
+ import { nodeSelectionBeforeInput, nodeSelectionKeydown, trySelectAtom, } from "./node-selection.js";
11
+ import { dispatchFormatInput, LINE_NAV_KEYS, normalizeKey } from "./keys.js";
101
12
  export function bindEditorInput(editor, el) {
102
- let composing = false;
103
- let compStart = null;
104
- let compTimer = null;
105
- // compositionend가 왔고 흡수만 남은 상태 — 이 창에 도착하는 구조
106
- // 입력(Enter/Backspace)은 커밋이 끝난 뒤의 것이라 브라우저에 맡기면
107
- // 네이티브 split/delete가 doc 밖 DOM을 만든다
108
- let pendingAbsorb = false;
109
13
  const dirtyPaths = new Set();
110
- /** p가 속한 최상위 블록 경로 — 프레임워크 리마운트 대상 */
111
- const topBlockKey = (p) => p?.path[0] == null ? null : String(p.path[0]);
112
14
  const readDomSel = () => domSelection(el);
15
+ // IME 조합 세션 — composing/compStart/타이머/에코 판별은 전부 세션
16
+ // 내부다. 이 파일은 DOM 이벤트를 세션 메서드로 변환하고 판정
17
+ // 결과(ImeGate)에 따라 preventDefault/무시만 수행한다
18
+ const ime = createImeSession({ editor, el, dirtyPaths, readDomSel });
113
19
  /**
114
20
  * 기능 DOM 오버레이 갱신 — 활성 줄 표시·괄호 하이라이트처럼
115
21
  * 렌더 트리가 아니라 DOM을 직접 마킹하는 것들. 각 기능이
@@ -118,7 +24,14 @@ export function bindEditorInput(editor, el) {
118
24
  */
119
25
  const paintOverlays = () => {
120
26
  for (const h of editor.inputHooks) {
121
- h.paintOverlay?.(el, editor.state);
27
+ h.paintOverlay?.(el, editor.state, editor);
28
+ }
29
+ // atom 노드 셀렉션 시각화 — 선택 경로의 최외곽 요소에 is-selected를
30
+ // 단다. 각 기능의 오버레이(이미지 편집 바 등)와 별개로 공용으로 칠한다
31
+ el.querySelectorAll(".is-selected").forEach((n) => n.classList.remove("is-selected"));
32
+ const sel = editor.state.selection;
33
+ if (sel.type === "node") {
34
+ el.querySelector(`[${PATH_ATTR}="${sel.path.join(",")}"]`)?.classList.add("is-selected");
122
35
  }
123
36
  };
124
37
  /**
@@ -134,8 +47,11 @@ export function bindEditorInput(editor, el) {
134
47
  };
135
48
  const restoreSelection = () => {
136
49
  const sel = editor.state.selection;
137
- if (sel.type !== "text")
50
+ // 노드 셀렉션은 DOM 캐럿이 없다 — 오버레이만 갱신하고 끝낸다
51
+ if (sel.type !== "text") {
52
+ paintOverlays();
138
53
  return;
54
+ }
139
55
  // 이미 DOM 셀렉션이 doc과 같으면 건드리지 않는다 (깜빡임/역순 방지)
140
56
  const cur = readDomSel();
141
57
  if (cur &&
@@ -152,20 +68,16 @@ export function bindEditorInput(editor, el) {
152
68
  // setSel이 먹지 않는 역효과 방지)
153
69
  // - 리프 포인트 — 렌더가 노드를 교체하면 Chrome이 캐럿을 요소
154
70
  // 경계로 재앵커하는데 그건 사용자 이동이 아니라 폐기 대상이다
155
- const isLeaf = (p) => nodeAt(editor.state.doc, p.path)?.type === "text";
156
71
  if (cur &&
157
72
  lastApplied &&
158
- isLeaf(cur.anchor) &&
159
- isLeaf(cur.focus) &&
73
+ isLeafPoint(editor.state.doc, cur.anchor) &&
74
+ isLeafPoint(editor.state.doc, cur.focus) &&
160
75
  pointEq(sel.anchor, lastApplied.anchor) &&
161
76
  pointEq(sel.focus, lastApplied.focus) &&
162
77
  (!pointEq(cur.anchor, lastApplied.anchor) ||
163
78
  !pointEq(cur.focus, lastApplied.focus))) {
164
79
  lastApplied = cur;
165
- editor.dispatch(editor.state
166
- .tr()
167
- .setSelection({ type: "text", ...cur })
168
- .setStoredMarks([]));
80
+ editor.dispatch(editor.state.tr().setSelection({ type: "text", ...cur }));
169
81
  paintOverlays();
170
82
  return;
171
83
  }
@@ -182,11 +94,9 @@ export function bindEditorInput(editor, el) {
182
94
  pointEq(cur.focus, s.focus)) {
183
95
  return;
184
96
  }
185
- // 커서 이동은 대기 중인 storedMarks를 지운다 (PM 관례 — []가 클리어)
186
- editor.dispatch(editor.state
187
- .tr()
188
- .setSelection({ type: "text", ...s })
189
- .setStoredMarks([]));
97
+ // 대기 서식(storedMarks)은 커서 이동에도 유지한다 — GDocs/Word
98
+ // 관례: 토글로 끌 때까지 다음 타이핑에 계속 적용된다
99
+ editor.dispatch(editor.state.tr().setSelection({ type: "text", ...s }));
190
100
  };
191
101
  const onBeforeInput = (e) => {
192
102
  if (!editor.editable || e.isComposing)
@@ -195,30 +105,27 @@ export function bindEditorInput(editor, el) {
195
105
  // 흡수된 텍스트를 포함해 정확하고, 플러시의 리렌더가 DOM
196
106
  // 셀렉션을 doc(조합 시작점)으로 되돌리기 전의 값이다
197
107
  const domSel = readDomSel();
198
- if (composing) {
199
- // 조합 종료 직후(흡수 대기) 도착한 구조 입력만 통과 — 커밋-Enter의
200
- // insertParagraph가 대표적이다. 텍스트 계열은 계속 브라우저에
201
- // 맡겨 DOM이 바뀌게 두고 타이머의 흡수가 잡는다
202
- const structural = e.inputType === "insertParagraph" ||
203
- e.inputType === "insertLineBreak" ||
204
- e.inputType === "insertFromPaste" ||
205
- e.inputType === "insertFromDrop" ||
206
- e.inputType.startsWith("delete");
207
- if (!pendingAbsorb || !structural)
208
- return;
209
- flushComposition();
108
+ // 조합 세션 게이트 — swallow(조합 중 무시)/prevent(커밋 에코 차단)/
109
+ // proceed(정상 처리 — 흡수 대기 중의 구조 입력은 플러시 후 통과)
110
+ const gated = ime.gate(e, domSel);
111
+ if (gated === "swallow")
112
+ return;
113
+ if (gated === "prevent") {
114
+ e.preventDefault();
115
+ return;
210
116
  }
211
- // DOM 셀렉션이 무효하면(리렌더로 분리된 노드 위 등) doc 셀렉션으로
212
- // 폴백 — sel 없이 리턴하면 preventDefault도 안 걸려 브라우저가
213
- // 직접 DOM을 바꾸고 doc과 영구히 어긋난다. doc 셀렉션이 텍스트가
214
- // 아닌 경우(노드 선택 등)도 마찬가지라 막아둔다
215
117
  const ds = editor.state.selection;
216
- const sel = domSel ??
217
- (ds.type === "text" ? { anchor: ds.anchor, focus: ds.focus } : null);
218
- if (!sel) {
118
+ // 노드 셀렉션 — DOM 캐럿은 클릭 전 위치에 멈춰 있어 신뢰 불가.
119
+ // 삭제는 노드를 통째로 지우고, 텍스트/개행 입력은 노드 뒤 블록
120
+ // 경계에서 일어나는 것으로 처리한다 (노드는 유지)
121
+ if (ds.type === "node") {
219
122
  e.preventDefault();
123
+ nodeSelectionBeforeInput(editor, e);
220
124
  return;
221
125
  }
126
+ // DOM 셀렉션이 무효하면(리렌더로 분리된 노드 위 등) doc 셀렉션으로
127
+ // 폴백 — 여기까지 왔으면 doc 셀렉션은 항상 텍스트다
128
+ const sel = domSel ?? { anchor: ds.anchor, focus: ds.focus };
222
129
  const tr = editor.state.tr();
223
130
  switch (e.inputType) {
224
131
  case "insertText":
@@ -227,6 +134,10 @@ export function bindEditorInput(editor, el) {
227
134
  const data = e.data ?? "";
228
135
  if (!data)
229
136
  return;
137
+ // 커밋 에코가 흡수 타이머보다 늦게 도착한 경우 — DOM과 doc에
138
+ // 이미 같은 글자가 있으므로 다시 넣으면 "세세"가 된다
139
+ if (ime.isLateEcho(data, domSel))
140
+ return;
230
141
  // 범위 삭제 뒤엔 삭제 전 좌표가 무효 — 정규화한 앞점 기준으로 삽입
231
142
  const r = normalizeRange(sel);
232
143
  // 입력 훅 — 기능이 글자 가로채기(코드블록 자동 쌍·타입오버 등)를
@@ -348,8 +259,12 @@ export function bindEditorInput(editor, el) {
348
259
  // 끝점이 요소 레벨 포인트(컨테이너 자식 경계, 블록 머지 시 Chrome이
349
260
  // (P,0) 같은 위치를 줌)면 tr.delete가 해석하지 못한다 — 리프
350
261
  // 포인트를 주는 pointBefore/After 폴백으로 돌린다
351
- const leaf = (p) => !!p && nodeAt(editor.state.doc, p.path)?.type === "text";
352
- if (ta && tf && !pointEq(ta, tf) && onCaret && leaf(ta) && leaf(tf)) {
262
+ if (ta &&
263
+ tf &&
264
+ !pointEq(ta, tf) &&
265
+ onCaret &&
266
+ isLeafPoint(editor.state.doc, ta) &&
267
+ isLeafPoint(editor.state.doc, tf)) {
353
268
  tr.delete(normalizeRange({ anchor: ta, focus: tf }));
354
269
  tr.setMeta("history.group", "delete");
355
270
  editor.dispatch(tr);
@@ -375,13 +290,7 @@ export function bindEditorInput(editor, el) {
375
290
  const r = normalizeRange(sel);
376
291
  // 기능 훅 — 링크 URL 감싸기·코드블록 순수 텍스트 등.
377
292
  // 하나라도 소비하면 기본 경로를 건너뛴다
378
- const ctx = {
379
- state: editor.state,
380
- tr,
381
- dispatch: () => editor.dispatch(tr),
382
- anchor: r.anchor,
383
- focus: r.focus,
384
- };
293
+ const ctx = rangeCtx(editor, r, tr);
385
294
  const data = { html, text };
386
295
  let consumed = false;
387
296
  for (const h of editor.inputHooks) {
@@ -418,8 +327,7 @@ export function bindEditorInput(editor, el) {
418
327
  let r = normalizeRange(sel);
419
328
  if (e.inputType === "insertReplacementText") {
420
329
  const tr0 = e.getTargetRanges?.()[0];
421
- const a = tr0 &&
422
- domToPoint(el, tr0.startContainer, tr0.startOffset);
330
+ const a = tr0 && domToPoint(el, tr0.startContainer, tr0.startOffset);
423
331
  const f = tr0 && domToPoint(el, tr0.endContainer, tr0.endOffset);
424
332
  if (a && f)
425
333
  r = normalizeRange({ anchor: a, focus: f });
@@ -432,14 +340,6 @@ export function bindEditorInput(editor, el) {
432
340
  editor.dispatch(tr);
433
341
  break;
434
342
  }
435
- case "historyUndo":
436
- case "historyRedo": {
437
- // iOS/Android의 OS 실행취소·재실행 제스처 — 네이티브가 DOM을
438
- // 되돌리면 doc과 어긋나니 우리 히스토리로 돌린다
439
- e.preventDefault();
440
- editor.commands[e.inputType === "historyUndo" ? "undo" : "redo"]?.();
441
- break;
442
- }
443
343
  // 단어/줄 단위·드래그·잘라내기 삭제 — macOS Option-Delete는
444
344
  // deleteWord*, Ctrl-K는 deleteSoftLine*, Cmd-X는 deleteByCut으로
445
345
  // 온다. getTargetRanges가 브라우저가 계산한 정확한 범위를 주므로
@@ -464,12 +364,11 @@ export function bindEditorInput(editor, el) {
464
364
  // 레벨 포인트면 tr.delete가 해석 못하므로 리프만 받는다
465
365
  const delBack = e.inputType.endsWith("Backward");
466
366
  const delFwd = e.inputType.endsWith("Forward");
467
- const leafP = (p) => !!p && nodeAt(editor.state.doc, p.path)?.type === "text";
468
367
  const trusted = a &&
469
368
  f &&
470
369
  !pointEq(a, f) &&
471
- leafP(a) &&
472
- leafP(f) &&
370
+ isLeafPoint(editor.state.doc, a) &&
371
+ isLeafPoint(editor.state.doc, f) &&
473
372
  (!delBack || pointEq(f, sel.focus)) &&
474
373
  (!delFwd || pointEq(a, sel.focus));
475
374
  const r = trusted
@@ -481,59 +380,7 @@ export function bindEditorInput(editor, el) {
481
380
  }
482
381
  // 모바일·컨텍스트 메뉴·리더의 서식 명령 — 네이티브 execCommand가
483
382
  // DOM을 직접 바꾸면 어긋나므로 우리 커맨드로 매핑한다
484
- case "formatBold":
485
- case "formatItalic":
486
- case "formatUnderline":
487
- case "formatStrikeThrough":
488
- case "formatSuperscript":
489
- case "formatSubscript": {
490
- e.preventDefault();
491
- const mark = FORMAT_MARKS[e.inputType];
492
- if (editor.state.schema.markSpec(mark)) {
493
- toggleMark(mark)({
494
- state: editor.state,
495
- tr,
496
- dispatch: () => editor.dispatch(tr),
497
- });
498
- }
499
- break;
500
- }
501
- case "formatRemove": {
502
- e.preventDefault();
503
- editor.commands.clearFormat?.();
504
- break;
505
- }
506
- case "formatJustifyLeft":
507
- case "formatJustifyCenter":
508
- case "formatJustifyRight": {
509
- e.preventDefault();
510
- const align = {
511
- formatJustifyLeft: "left",
512
- formatJustifyCenter: "center",
513
- formatJustifyRight: "right",
514
- }[e.inputType];
515
- editor.commands.setAlign?.(align);
516
- break;
517
- }
518
- case "formatIndent":
519
- case "formatOutdent": {
520
- e.preventDefault();
521
- editor.commands[e.inputType === "formatIndent" ? "sinkListItem" : "liftListItem"]?.();
522
- break;
523
- }
524
- case "insertUnorderedList":
525
- case "insertOrderedList": {
526
- e.preventDefault();
527
- editor.commands[e.inputType === "insertOrderedList"
528
- ? "toggleOrderedList"
529
- : "toggleBulletList"]?.();
530
- break;
531
- }
532
- case "insertHorizontalRule": {
533
- e.preventDefault();
534
- editor.commands.insertHorizontalRule?.();
535
- break;
536
- }
383
+ // (매핑 테이블은 keys.ts의 dispatchFormatInput)
537
384
  case "insertFromYank":
538
385
  case "insertFromPasteAsQuotation": {
539
386
  // macOS 킬링 붙여넣기(Ctrl-Y)·인용 붙여넣기 — 있으면 순수
@@ -549,10 +396,15 @@ export function bindEditorInput(editor, el) {
549
396
  editor.dispatch(tr);
550
397
  break;
551
398
  }
552
- // 나머지 타입(insertTranspose, formatFontColor 등)은 우리가
553
- // 처리하지 않는다 — 그대로 두면 브라우저가 DOM만 바꿔 doc과
554
- // 영구히 어긋나므로 기본 동작을 막아 no-op으로 둔다
555
399
  default:
400
+ // format*/history*/insert*List 등 — 커맨드 디스패치 테이블에 위임
401
+ if (dispatchFormatInput(editor, e.inputType)) {
402
+ e.preventDefault();
403
+ break;
404
+ }
405
+ // 나머지 타입(insertTranspose, formatFontColor 등)은 우리가
406
+ // 처리하지 않는다 — 그대로 두면 브라우저가 DOM만 바꿔 doc과
407
+ // 영구히 어긋나므로 기본 동작을 막아 no-op으로 둔다
556
408
  e.preventDefault();
557
409
  break;
558
410
  }
@@ -562,7 +414,7 @@ export function bindEditorInput(editor, el) {
562
414
  * false 반환은 다음 바인딩으로 폴스루 (PM keymap 관례).
563
415
  */
564
416
  const onKeydown = (e) => {
565
- if (composing || e.isComposing || !editor.editable)
417
+ if (ime.active || e.isComposing || !editor.editable)
566
418
  return;
567
419
  const key = normalizeKey(e);
568
420
  // Mod-A 전체 선택은 직접 처리 — 중첩 편집 호스트(.me-node-view 안의
@@ -570,7 +422,19 @@ export function bindEditorInput(editor, el) {
570
422
  // 셀렉션을 펼치지 못하고 문서 시작으로 collapse한다
571
423
  if (key === "Mod-a") {
572
424
  e.preventDefault();
573
- editor.dispatch(editor.state.tr().setSelection(textSelection({ path: [], offset: 0 }, { path: [], offset: editor.state.doc.content?.length ?? 0 })));
425
+ editor.dispatch(editor.state
426
+ .tr()
427
+ .setSelection(textSelection({ path: [], offset: 0 }, { path: [], offset: editor.state.doc.content?.length ?? 0 })));
428
+ return;
429
+ }
430
+ // 노드 셀렉션(이미지 클릭 등) — DOM 캐럿이 없으므로 네이티브
431
+ // 키 동작은 모두 무시하고 모델에서 처리한다. Backspace/Delete는
432
+ // 노드 삭제, Enter는 노드 뒤에 새 블록, 화살표/Escape는 텍스트
433
+ // 셀렉션으로 붕괴, 인쇄 문자는 노드 뒤에 삽입(노드는 유지).
434
+ // Mod/Alt 조합(undo·키맵 등)은 아래 키맵 루프에 넘긴다
435
+ if (editor.state.selection.type === "node" &&
436
+ nodeSelectionKeydown(editor, e, key)) {
437
+ e.preventDefault();
574
438
  return;
575
439
  }
576
440
  for (const km of editor.keymaps) {
@@ -595,192 +459,10 @@ export function bindEditorInput(editor, el) {
595
459
  // Ctrl+←/→는 비-Mac에서 단어 이동이라 metaKey(Cmd)일 때만 가로챈다
596
460
  if (LINE_NAV_KEYS.has(key) &&
597
461
  (!key.includes("Arrow") || e.metaKey) &&
598
- lineNav(key)) {
462
+ lineNav(editor, el, key)) {
599
463
  e.preventDefault();
600
464
  }
601
465
  };
602
- /**
603
- * caretRangeFromPoint로 시각적 줄 끝/시작의 doc 위치를 계산해 캐럿을 옮긴다.
604
- * 클릭 배치와 같은 Blink 엔진이라 네이티브 줄 경계 계산보다 신뢰할 수 있다.
605
- * Mod-Home/End는 doc 모델 기준 문서 가장자리로 간다. 실패 시 false → 네이티브 폴백.
606
- */
607
- const lineNav = (key) => {
608
- const doc = el.ownerDocument;
609
- // DOM 셀렉션 우선 — 클릭 직후엔 doc 셀렉션이 아직 동기화 전이라
610
- // stale할 수 있다. DOM이 분리 노드 위거나(리렌더 직후) 렌더
611
- // 재앵커로 요소 레벨 포인트에 있으면 doc 포커스로 폴백한다
612
- const domNow = readDomSel();
613
- const ds = editor.state.selection;
614
- const docFocus = ds.type === "text" ? ds.focus : null;
615
- const leaf = (p) => !!p && nodeAt(editor.state.doc, p.path)?.type === "text";
616
- const focus = leaf(domNow?.focus) ? domNow.focus : docFocus;
617
- if (!focus)
618
- return false;
619
- const extend = key.startsWith("Shift-") || key.startsWith("Mod-Shift-");
620
- const anchor = () => extend
621
- ? leaf(domNow?.anchor)
622
- ? domNow.anchor
623
- : ds.type === "text"
624
- ? ds.anchor
625
- : null
626
- : null;
627
- // 문서 가장자리 계열(Mod-Home/End) — doc 모델로 바로 적용.
628
- // applySel이 아니라 dispatch로 간다 — doc이 동기 갱신돼야 뒤따르는
629
- // 비동기 렌더 커밋이 DOM을 파괴해도 restore가 같은 위치를 다시
630
- // 적용한다(DOM 직접 쓰기는 pending selchg 사이 커밋에 되돌아간다)
631
- if (key.startsWith("Mod") &&
632
- !key.endsWith("ArrowLeft") &&
633
- !key.endsWith("ArrowRight")) {
634
- const t = key.endsWith("Home")
635
- ? point([], 0)
636
- : point([], editor.state.doc.content?.length ?? 0);
637
- editor.dispatch(editor.state
638
- .tr()
639
- .setSelection({ type: "text", anchor: anchor() ?? t, focus: t }));
640
- return true;
641
- }
642
- // 캐럿의 화면 y — 포커스의 DOM 위치에서 collapsed range rect을 잰다.
643
- // pointToDOM이 현재 DOM으로 재해석하므로 리렌더로 분리된
644
- // 노드 위의 브라우저 셀렉션에 흔들리지 않는다
645
- const domPos = pointToDOM(el, focus);
646
- if (!domPos || !el.contains(domPos.node))
647
- return false;
648
- const range = doc.createRange();
649
- try {
650
- range.setStart(domPos.node, domPos.offset);
651
- range.collapse(true);
652
- }
653
- catch {
654
- return false;
655
- }
656
- let rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
657
- // 줄 시작의 빈/경계 위치 캐럿이나 빈 텍스트 노드(Vue 앵커)에선
658
- // collapsed range가 빈 rect을 준다 — 인접 한 글자 → 부모 요소 순으로
659
- // 근사한다
660
- if ((!rect || rect.height === 0) && domPos.node.nodeType === 3) {
661
- const len = domPos.node.nodeValue?.length ?? 0;
662
- if (domPos.offset < len) {
663
- range.setEnd(domPos.node, domPos.offset + 1);
664
- }
665
- else if (domPos.offset > 0) {
666
- range.setStart(domPos.node, domPos.offset - 1);
667
- }
668
- if (domPos.offset < len || domPos.offset > 0) {
669
- rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
670
- }
671
- }
672
- if (!rect || rect.height === 0) {
673
- const parent = domPos.node.nodeType === 3
674
- ? domPos.node.parentElement
675
- : domPos.node;
676
- rect = parent?.getBoundingClientRect();
677
- }
678
- if (!rect || rect.height === 0)
679
- return false;
680
- // 캐럿이 속한 블록 요소 — 시각적 줄이 갇힌 박스 (인라인 맥락 루트)
681
- let block = domPos.node.nodeType === 3
682
- ? domPos.node.parentElement
683
- : domPos.node;
684
- while (block && block !== el) {
685
- if (getComputedStyle(block).display !== "inline")
686
- break;
687
- block = block.parentElement;
688
- }
689
- const br = (block && block !== el ? block : el).getBoundingClientRect();
690
- const y = rect.top + rect.height / 2;
691
- const t = caretPointFromXY(doc, el, key.endsWith("End") || key.endsWith("ArrowRight")
692
- ? br.right - 1
693
- : br.left + 1, y);
694
- if (!t)
695
- return false;
696
- // 다른 블록의 위치가 잡히면 신뢰 불가 — 네이티브에 맡긴다
697
- const sameBlock = t.path.length === focus.path.length &&
698
- t.path.slice(0, -1).every((v, i) => v === focus.path[i]);
699
- if (!sameBlock)
700
- return false;
701
- editor.dispatch(editor.state
702
- .tr()
703
- .setSelection({ type: "text", anchor: anchor() ?? t, focus: t }));
704
- return true;
705
- };
706
- /**
707
- * composition 중 브라우저가 직접 바꾼 DOM을 doc에 흡수한다.
708
- * 캐럿이 위치한 텍스트 홀더의 DOM 텍스트와 doc 텍스트를 공통
709
- * 접두/접미로 diff해 차이만큼만 트랜잭션으로 반영한다.
710
- * (제약: 조합이 하나의 텍스트 홀더 안에 머문다고 가정 —
711
- * 크로스-블록 조합 삭제는 미지원)
712
- */
713
- const absorbComposition = () => {
714
- const focus = readDomSel()?.focus ?? compStart;
715
- if (!focus || focus.path.length === 0)
716
- return;
717
- const holder = el.querySelector(`[${PATH_ATTR}="${focus.path.join(",")}"]`);
718
- // 텍스트 홀더가 아니면(안에 me-path 자식이 있으면) 흡수 불가
719
- if (!holder || holder.querySelector(`[${PATH_ATTR}]`))
720
- return;
721
- const domText = holder.textContent ?? "";
722
- const target = nodeAt(editor.state.doc, focus.path);
723
- const docText = target?.type === "text" ? target.text ?? "" : "";
724
- if (domText === docText)
725
- return;
726
- let lo = 0;
727
- const max = Math.min(domText.length, docText.length);
728
- while (lo < max && domText[lo] === docText[lo])
729
- lo++;
730
- let hiDom = domText.length;
731
- let hiDoc = docText.length;
732
- while (hiDom > lo && hiDoc > lo && domText[hiDom - 1] === docText[hiDoc - 1]) {
733
- hiDom--;
734
- hiDoc--;
735
- }
736
- const tr = editor.state.tr();
737
- if (hiDoc > lo) {
738
- tr.delete({
739
- anchor: point(focus.path, lo),
740
- focus: point(focus.path, hiDoc),
741
- });
742
- }
743
- const inserted = domText.slice(lo, hiDom);
744
- if (inserted)
745
- tr.insertText(inserted, point(focus.path, lo));
746
- if (!tr.docChanged)
747
- return;
748
- tr.setMeta("history.group", "type").setMeta("ime", true);
749
- // 브라우저가 이 블록의 DOM을 직접 바꿨다 — 리렌더 시 프레임워크
750
- // 리컨실이 깨지지 않게 블록 리마운트 대상으로 표시한다
751
- const dk = topBlockKey(focus) ?? topBlockKey(compStart);
752
- if (dk != null)
753
- dirtyPaths.add(dk);
754
- editor.dispatch(tr);
755
- };
756
- const onCompositionStart = () => {
757
- composing = true;
758
- pendingAbsorb = false;
759
- compStart = readDomSel()?.focus ?? null;
760
- };
761
- /**
762
- * 대기 중인 흡수를 즉시 실행 — 커밋 직후의 구조 입력(Enter 등)이
763
- * 최신 doc 기준으로 처리되게 하기 위해 beforeinput에서 호출한다.
764
- */
765
- const flushComposition = () => {
766
- if (compTimer !== null) {
767
- clearTimeout(compTimer);
768
- compTimer = null;
769
- }
770
- composing = false;
771
- pendingAbsorb = false;
772
- if (editor.editable)
773
- absorbComposition();
774
- compStart = null;
775
- };
776
- const onCompositionEnd = () => {
777
- // composing 해제와 흡수를 다음 태스크로 미룬다 — 같은 태스크의
778
- // 후속 beforeinput/keydown(Safari 최종 커밋, IME Enter 오커밋)을
779
- // composing 가드가 계속 걸러내게 하기 위해서다. 단 그 창에 도착한
780
- // 구조 입력은 flushComposition이 흡수를 앞당겨 정상 처리한다.
781
- pendingAbsorb = true;
782
- compTimer = setTimeout(flushComposition, 0);
783
- };
784
466
  /**
785
467
  * 체크박스 아이템(taskItem)의 마커 영역 클릭 — ::before 마커는
786
468
  * 의사요소라 이벤트 타겟이 li 자신이 되고, 클릭이 li의 패딩
@@ -797,27 +479,23 @@ export function bindEditorInput(editor, el) {
797
479
  const path = t.getAttribute(PATH_ATTR).split(",").map(Number);
798
480
  const node = nodeAt(editor.state.doc, path);
799
481
  const spec = node && editor.state.schema.nodeSpec(node.type);
800
- if (!spec?.checkbox)
801
- return;
802
- // offsetX는 패딩 가장자리 기준 — 마커 존 밖이면 무시
803
- if (e.offsetX > 28)
482
+ if (spec?.checkbox) {
483
+ // offsetX는 패딩 가장자리 기준 — 마커 존 밖이면 무시
484
+ if (e.offsetX > 28)
485
+ return;
486
+ e.preventDefault();
487
+ editor.dispatch(editor.state
488
+ .tr()
489
+ .setNodeAttr(path, "checked", !node?.attrs?.["checked"]));
804
490
  return;
805
- e.preventDefault();
806
- editor.dispatch(editor.state
807
- .tr()
808
- .setNodeAttr(path, "checked", !node?.attrs?.["checked"]));
809
- };
810
- /**
811
- * 파일 수신 — paste/drop의 File 목록을 기능 훅(files)에 넘긴다.
812
- * 어느 기능도 소비하지 않으면 폴스루: paste는 beforeinput이
813
- * 텍스트/HTML을 처리하고, drop은 브라우저 기본 동작을 따른다.
814
- */
815
- const dispatchFiles = (dt, source, at) => {
816
- const files = Array.from(dt?.files ?? []);
817
- if (!files.length)
818
- return false;
819
- const ctx = { editor, files, at, source };
820
- return editor.inputHooks.some((h) => h.files?.(ctx));
491
+ }
492
+ // atom 노드(img/hr 등) 클릭 → NodeSelection — DOM 캐럿 이동을
493
+ // 막고(preventDefault) 모델 셀렉션만 둔다. 오버레이 표시는
494
+ // paintOverlays의 is-selected 마킹 + 각 기능의 paintOverlay가 담당.
495
+ // 같은 노드를 다시 누르면 트랜잭션을 생략한다
496
+ if (trySelectAtom(editor, t)) {
497
+ e.preventDefault();
498
+ }
821
499
  };
822
500
  /**
823
501
  * 붙여넣기 — 클립보드의 파일을 기능 훅으로 위임한다 (이미지 업로드는
@@ -826,11 +504,12 @@ export function bindEditorInput(editor, el) {
826
504
  * 처리한다.
827
505
  */
828
506
  const onPaste = (e) => {
829
- if (composing || !editor.editable)
507
+ if (ime.active || !editor.editable)
830
508
  return;
831
509
  // preventDefault하면 beforeinput이 오지 않는다
832
- if (dispatchFiles(e.clipboardData, "paste", null))
510
+ if (dispatchFiles(editor, e.clipboardData, "paste", null)) {
833
511
  e.preventDefault();
512
+ }
834
513
  };
835
514
  /** 드롭을 받으려면 dragover를 먼저 막아야 한다 — 파일(업로드 훅)
836
515
  * 외에 텍스트 드래그도 beforeinput insertFromDrop으로 트랜잭션 처리 */
@@ -843,15 +522,15 @@ export function bindEditorInput(editor, el) {
843
522
  }
844
523
  };
845
524
  const onDrop = (e) => {
846
- if (composing || !editor.editable)
525
+ if (ime.active || !editor.editable)
847
526
  return;
848
527
  const at = caretPointFromXY(el.ownerDocument, el, e.clientX, e.clientY);
849
528
  // 소비되지 않으면 기본 동작(파일을 새 탭으로 여는 것)이 살아있다
850
- if (dispatchFiles(e.dataTransfer, "drop", at))
529
+ if (dispatchFiles(editor, e.dataTransfer, "drop", at))
851
530
  e.preventDefault();
852
531
  };
853
532
  const onSelectionChange = () => {
854
- if (composing)
533
+ if (ime.active)
855
534
  return;
856
535
  // 에디터 안의 셀렉션만 동기화 — 바깥 클릭은 무시
857
536
  const sel = readDomSel();
@@ -867,8 +546,8 @@ export function bindEditorInput(editor, el) {
867
546
  pointEq(cur.focus, next.focus)) {
868
547
  return;
869
548
  }
870
- // 커서 이동은 대기 중인 storedMarks를 지운다 (PM 관례 — []가 클리어)
871
- editor.dispatch(editor.state.tr().setSelection(next).setStoredMarks([]));
549
+ // 대기 서식(storedMarks)은 커서 이동에도 유지한다 — GDocs/Word 관례
550
+ editor.dispatch(editor.state.tr().setSelection(next));
872
551
  };
873
552
  el.addEventListener("beforeinput", onBeforeInput);
874
553
  el.addEventListener("keydown", onKeydown);
@@ -876,26 +555,34 @@ export function bindEditorInput(editor, el) {
876
555
  el.addEventListener("paste", onPaste);
877
556
  el.addEventListener("dragover", onDragOver);
878
557
  el.addEventListener("drop", onDrop);
879
- el.addEventListener("compositionstart", onCompositionStart);
880
- el.addEventListener("compositionend", onCompositionEnd);
558
+ el.addEventListener("compositionstart", ime.start);
559
+ el.addEventListener("compositionend", ime.end);
881
560
  el.ownerDocument.addEventListener("selectionchange", onSelectionChange);
561
+ // 오버레이 초기 페인트 — React/Vue2처럼 마운트 layoutEffect가
562
+ // 바인딩보다 먼저 도는 렌더러에서도 첫 커밋 전에 오버레이가
563
+ // 존재하도록 바인딩 시점에 한 번 그린다
564
+ paintOverlays();
882
565
  return {
883
566
  dirtyPaths,
884
567
  unbind() {
885
- if (compTimer !== null)
886
- clearTimeout(compTimer);
568
+ ime.destroy();
569
+ // 기능 오버레이 정리 — 거터/크롬 바 DOM + ResizeObserver 해제
570
+ for (const h of editor.inputHooks)
571
+ h.destroyOverlay?.(el);
887
572
  el.removeEventListener("beforeinput", onBeforeInput);
888
573
  el.removeEventListener("keydown", onKeydown);
889
574
  el.removeEventListener("mousedown", onMouseDown);
890
575
  el.removeEventListener("paste", onPaste);
891
576
  el.removeEventListener("dragover", onDragOver);
892
577
  el.removeEventListener("drop", onDrop);
893
- el.removeEventListener("compositionstart", onCompositionStart);
894
- el.removeEventListener("compositionend", onCompositionEnd);
578
+ el.removeEventListener("compositionstart", ime.start);
579
+ el.removeEventListener("compositionend", ime.end);
895
580
  el.ownerDocument.removeEventListener("selectionchange", onSelectionChange);
896
581
  },
897
582
  restoreSelection,
898
583
  syncDocSelection,
899
584
  };
900
585
  }
586
+ // DirtyKeyMap은 dirty-keys.ts로 이동했다 — 공개 API 경로 유지를 위해 재수출
587
+ export { DirtyKeyMap } from "./dirty-keys.js";
901
588
  //# sourceMappingURL=input.js.map