@teriusu/rich-editor 0.10.8 → 0.11.0

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 (147) hide show
  1. package/dist/components/BubbleToolbar.svelte +40 -36
  2. package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
  3. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  4. package/dist/components/EmojiSuggestionMenu.svelte +291 -0
  5. package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
  6. package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
  7. package/dist/components/FixedToolbar.svelte +53 -47
  8. package/dist/components/FixedToolbar.svelte.d.ts +3 -0
  9. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  10. package/dist/components/FloatingHelper.svelte +219 -0
  11. package/dist/components/FloatingHelper.svelte.d.ts +14 -0
  12. package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
  13. package/dist/components/InputModal.svelte +7 -3
  14. package/dist/components/InputModal.svelte.d.ts +3 -0
  15. package/dist/components/InputModal.svelte.d.ts.map +1 -1
  16. package/dist/components/MathModal.svelte +33 -29
  17. package/dist/components/MathModal.svelte.d.ts +3 -0
  18. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  19. package/dist/components/MediaPickerModal.svelte +7 -3
  20. package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
  21. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
  22. package/dist/components/SlashCommandMenu.svelte +88 -72
  23. package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
  24. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  25. package/dist/components/TableBubbleMenu.svelte +41 -33
  26. package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
  27. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  28. package/dist/components/TipTapEditor.svelte +239 -127
  29. package/dist/components/TipTapEditor.svelte.d.ts +7 -0
  30. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  31. package/dist/extensions/CardBlock.d.ts.map +1 -1
  32. package/dist/extensions/CardBlock.js +6 -4
  33. package/dist/extensions/FileAttachment.d.ts.map +1 -1
  34. package/dist/extensions/FileAttachment.js +2 -1
  35. package/dist/extensions/FixedDetails.d.ts.map +1 -1
  36. package/dist/extensions/FixedDetails.js +5 -3
  37. package/dist/extensions/LegacyBlock.d.ts.map +1 -1
  38. package/dist/extensions/LegacyBlock.js +8 -6
  39. package/dist/extensions/Math.d.ts.map +1 -1
  40. package/dist/extensions/Math.js +5 -2
  41. package/dist/extensions/MbusVideo.d.ts.map +1 -1
  42. package/dist/extensions/MbusVideo.js +73 -9
  43. package/dist/extensions/MediaToolbar.d.ts +41 -0
  44. package/dist/extensions/MediaToolbar.d.ts.map +1 -0
  45. package/dist/extensions/MediaToolbar.js +196 -0
  46. package/dist/extensions/MediaToolbar.test.d.ts +2 -0
  47. package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
  48. package/dist/extensions/MediaToolbar.test.js +231 -0
  49. package/dist/extensions/OrderedListMarker.d.ts +2 -0
  50. package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
  51. package/dist/extensions/OrderedListMarker.js +114 -0
  52. package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
  53. package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
  54. package/dist/extensions/OrderedListMarker.test.js +176 -0
  55. package/dist/extensions/PdfBlock.d.ts.map +1 -1
  56. package/dist/extensions/PdfBlock.js +25 -23
  57. package/dist/extensions/ResizableImage.d.ts.map +1 -1
  58. package/dist/extensions/ResizableImage.js +20 -1
  59. package/dist/extensions/TableKeymap.d.ts +16 -0
  60. package/dist/extensions/TableKeymap.d.ts.map +1 -0
  61. package/dist/extensions/TableKeymap.js +97 -0
  62. package/dist/extensions/TableKeymap.test.d.ts +2 -0
  63. package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
  64. package/dist/extensions/TableKeymap.test.js +147 -0
  65. package/dist/extensions/TabsBlock.d.ts.map +1 -1
  66. package/dist/extensions/TabsBlock.js +12 -9
  67. package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
  68. package/dist/extensions/TiptapMidibus.js +60 -6
  69. package/dist/extensions/UploadSkeleton.d.ts +61 -0
  70. package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
  71. package/dist/extensions/UploadSkeleton.js +233 -0
  72. package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
  73. package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
  74. package/dist/extensions/UploadSkeleton.test.js +128 -0
  75. package/dist/extensions/VideoEmbed.d.ts +8 -0
  76. package/dist/extensions/VideoEmbed.d.ts.map +1 -1
  77. package/dist/extensions/VideoEmbed.js +226 -45
  78. package/dist/extensions/VideoEmbed.test.js +131 -2
  79. package/dist/i18n/en.d.ts +3 -0
  80. package/dist/i18n/en.d.ts.map +1 -0
  81. package/dist/i18n/en.js +191 -0
  82. package/dist/i18n/es.d.ts +3 -0
  83. package/dist/i18n/es.d.ts.map +1 -0
  84. package/dist/i18n/es.js +191 -0
  85. package/dist/i18n/index.d.ts +43 -0
  86. package/dist/i18n/index.d.ts.map +1 -0
  87. package/dist/i18n/index.js +109 -0
  88. package/dist/i18n/index.test.d.ts +2 -0
  89. package/dist/i18n/index.test.d.ts.map +1 -0
  90. package/dist/i18n/index.test.js +173 -0
  91. package/dist/i18n/ja.d.ts +3 -0
  92. package/dist/i18n/ja.d.ts.map +1 -0
  93. package/dist/i18n/ja.js +191 -0
  94. package/dist/i18n/ko.d.ts +198 -0
  95. package/dist/i18n/ko.d.ts.map +1 -0
  96. package/dist/i18n/ko.js +215 -0
  97. package/dist/i18n/zh-hans.d.ts +3 -0
  98. package/dist/i18n/zh-hans.d.ts.map +1 -0
  99. package/dist/i18n/zh-hans.js +191 -0
  100. package/dist/i18n/zh-hant.d.ts +3 -0
  101. package/dist/i18n/zh-hant.d.ts.map +1 -0
  102. package/dist/i18n/zh-hant.js +192 -0
  103. package/dist/index.d.ts +7 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -1
  106. package/dist/static/highlight.d.ts +13 -0
  107. package/dist/static/highlight.d.ts.map +1 -0
  108. package/dist/static/highlight.js +99 -0
  109. package/dist/static/index.d.ts +33 -0
  110. package/dist/static/index.d.ts.map +1 -0
  111. package/dist/static/index.js +367 -0
  112. package/dist/static/index.test.d.ts +2 -0
  113. package/dist/static/index.test.d.ts.map +1 -0
  114. package/dist/static/index.test.js +194 -0
  115. package/dist/static/tokenizer.d.ts +52 -0
  116. package/dist/static/tokenizer.d.ts.map +1 -0
  117. package/dist/static/tokenizer.js +289 -0
  118. package/dist/styles/editor.css +1 -1
  119. package/dist/utils/emoji-data.d.ts +15 -0
  120. package/dist/utils/emoji-suggestion.d.ts +48 -0
  121. package/dist/utils/emoji-suggestion.d.ts.map +1 -0
  122. package/dist/utils/emoji-suggestion.js +51 -0
  123. package/dist/utils/emoji-suggestion.test.d.ts +2 -0
  124. package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
  125. package/dist/utils/emoji-suggestion.test.js +117 -0
  126. package/dist/utils/floating-helper.d.ts +60 -0
  127. package/dist/utils/floating-helper.d.ts.map +1 -0
  128. package/dist/utils/floating-helper.js +63 -0
  129. package/dist/utils/floating-helper.test.d.ts +2 -0
  130. package/dist/utils/floating-helper.test.d.ts.map +1 -0
  131. package/dist/utils/floating-helper.test.js +141 -0
  132. package/dist/utils/guard-vscode-paste.d.ts +19 -0
  133. package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
  134. package/dist/utils/guard-vscode-paste.js +46 -0
  135. package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
  136. package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
  137. package/dist/utils/guard-vscode-paste.test.js +86 -0
  138. package/dist/utils/media-size.d.ts +29 -0
  139. package/dist/utils/media-size.d.ts.map +1 -0
  140. package/dist/utils/media-size.js +90 -0
  141. package/dist/utils/resize.d.ts +12 -0
  142. package/dist/utils/resize.d.ts.map +1 -1
  143. package/dist/utils/resize.js +5 -5
  144. package/dist/utils/sanitize.d.ts +2 -0
  145. package/dist/utils/sanitize.d.ts.map +1 -1
  146. package/dist/utils/sanitize.js +28 -3
  147. package/package.json +8 -1
@@ -1,5 +1,6 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
2
  import { attachResize } from "../utils/resize";
3
+ import { getEditorTranslator } from "../i18n";
3
4
  /**
4
5
  * 카드 블록 — 본문 중간에 넣는 **배경 있는 상자**.
5
6
  *
@@ -96,6 +97,7 @@ export const CardBlock = Node.create({
96
97
  },
97
98
  addNodeView() {
98
99
  return ({ node, editor, getPos }) => {
100
+ const t = getEditorTranslator(editor);
99
101
  let currentNode = node;
100
102
  let detachResize = null;
101
103
  const dom = document.createElement("div");
@@ -145,7 +147,7 @@ export const CardBlock = Node.create({
145
147
  titleInput = document.createElement("input");
146
148
  titleInput.type = "text";
147
149
  titleInput.className = "hce-card-title-input";
148
- titleInput.placeholder = "카드 제목";
150
+ titleInput.placeholder = t("cardTitlePlaceholder");
149
151
  titleInput.contentEditable = "false";
150
152
  // ProseMirror 가 입력칸의 키/포인터를 가져가지 않도록 막는다.
151
153
  titleInput.setAttribute("data-card-control", "");
@@ -158,7 +160,7 @@ export const CardBlock = Node.create({
158
160
  pick.className = "hce-card-bg-button";
159
161
  pick.setAttribute("data-card-control", "");
160
162
  pick.contentEditable = "false";
161
- pick.textContent = "배경 변경";
163
+ pick.textContent = t("cardChangeBackground");
162
164
  pick.addEventListener("click", async (e) => {
163
165
  e.preventDefault();
164
166
  e.stopPropagation();
@@ -171,7 +173,7 @@ export const CardBlock = Node.create({
171
173
  const current = String(currentNode.attrs.background ?? "");
172
174
  const next = prompt
173
175
  ? await prompt(current)
174
- : window.prompt("카드 배경 (CSS background 값)", current);
176
+ : window.prompt(t("cardBackgroundPrompt"), current);
175
177
  if (next == null)
176
178
  return;
177
179
  setAttr({ background: next });
@@ -186,7 +188,7 @@ export const CardBlock = Node.create({
186
188
  attr: "height",
187
189
  min: MIN_HEIGHT,
188
190
  max: MAX_HEIGHT,
189
- label: "카드 높이 조절",
191
+ label: t("cardResizeHeight"),
190
192
  // 원본과 같은 저장 형식 — 단위 없는 숫자 문자열.
191
193
  format: (v) => String(Math.round(v))
192
194
  });
@@ -1 +1 @@
1
- {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkFD,eAAO,MAAM,cAAc,kCAyNzB,CAAC"}
1
+ {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAGrD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkFD,eAAO,MAAM,cAAc,kCAyNzB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { getEditorTranslator } from "../i18n";
2
3
  function formatFileSize(bytes) {
3
4
  if (bytes < 1024)
4
5
  return `${bytes} B`;
@@ -222,7 +223,7 @@ export const FileAttachment = Node.create({
222
223
  // resolver가 있으면 이름/크기 메타데이터를 채운다.
223
224
  const resolver = editor.storage.fileAttachment?.resolver;
224
225
  if (resolver) {
225
- sizeEl.textContent = "loading...";
226
+ sizeEl.textContent = getEditorTranslator(editor)("fileSizeLoading");
226
227
  resolver(node.attrs.fileId)
227
228
  .then((result) => {
228
229
  resolvedSrc = result.src;
@@ -1 +1 @@
1
- {"version":3,"file":"FixedDetails.d.ts","sourceRoot":"","sources":["../../src/extensions/FixedDetails.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,YAAY,sFAyGvB,CAAC"}
1
+ {"version":3,"file":"FixedDetails.d.ts","sourceRoot":"","sources":["../../src/extensions/FixedDetails.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,sFA0GvB,CAAC"}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { Details } from "@tiptap/extension-details";
11
11
  import { mergeAttributes } from "@tiptap/core";
12
+ import { getEditorTranslator } from "../i18n";
12
13
  export const FixedDetails = Details.extend({
13
14
  addOptions() {
14
15
  return {
@@ -19,6 +20,7 @@ export const FixedDetails = Details.extend({
19
20
  },
20
21
  addNodeView() {
21
22
  return ({ editor, getPos, node: initialNode, HTMLAttributes }) => {
23
+ const t = getEditorTranslator(editor);
22
24
  let currentNode = initialNode;
23
25
  const dom = document.createElement("div");
24
26
  const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
@@ -28,7 +30,7 @@ export const FixedDetails = Details.extend({
28
30
  // 토글 버튼
29
31
  const toggle = document.createElement("button");
30
32
  toggle.type = "button";
31
- toggle.setAttribute("aria-label", "Expand details content");
33
+ toggle.setAttribute("aria-label", t("detailsExpand"));
32
34
  dom.append(toggle);
33
35
  // 콘텐츠 래퍼
34
36
  const content = document.createElement("div");
@@ -37,11 +39,11 @@ export const FixedDetails = Details.extend({
37
39
  const applyState = () => {
38
40
  if (isOpen) {
39
41
  dom.classList.add(this.options.openClassName);
40
- toggle.setAttribute("aria-label", "Collapse details content");
42
+ toggle.setAttribute("aria-label", t("detailsCollapse"));
41
43
  }
42
44
  else {
43
45
  dom.classList.remove(this.options.openClassName);
44
- toggle.setAttribute("aria-label", "Expand details content");
46
+ toggle.setAttribute("aria-label", t("detailsExpand"));
45
47
  }
46
48
  const detailsContent = content.querySelector(':scope > div[data-type="detailsContent"]');
47
49
  if (detailsContent) {
@@ -1 +1 @@
1
- {"version":3,"file":"LegacyBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/LegacyBlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAiB,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE/E,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;AAElF,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,mDAAmD;IACnD,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,+CAA+C;IAC/C,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,iDAAiD;AACjD,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAClE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,wBAAwB,KAAK,uBAAuB,CAAC;AAEjG,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACrC;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,iBAAiB,CAqB5D;AA6DD,eAAO,MAAM,WAAW,+BA6GtB,CAAC"}
1
+ {"version":3,"file":"LegacyBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/LegacyBlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAiB,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE/E,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;AAElF,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,mDAAmD;IACnD,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,+CAA+C;IAC/C,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,iDAAiD;AACjD,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAClE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,wBAAwB,KAAK,uBAAuB,CAAC;AAEjG,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACrC;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,iBAAiB,CAqB5D;AA6DD,eAAO,MAAM,WAAW,+BA8GtB,CAAC"}
@@ -21,6 +21,7 @@
21
21
  * 같은 경로(`getHTML()`)로 만들어져 이미 `=""` 꼴이라 실사용에서는 어긋나지 않는다.
22
22
  */
23
23
  import { Node } from "@tiptap/core";
24
+ import { getEditorTranslator } from "../i18n";
24
25
  /** 되쓸 이유가 없는(그리고 되쓰면 위험한) 것들. prod 저장본에는 원래 들어 있지 않다. */
25
26
  const SKIP_TAGS = new Set(["script", "style", "noscript", "template"]);
26
27
  /** `Columns`/`Column` 의 같은 셀렉터보다 먼저 잡혀야 한다(기본 50). */
@@ -96,10 +97,10 @@ function detectKind(el) {
96
97
  return "columns";
97
98
  return "iframe";
98
99
  }
99
- const KIND_LABEL = {
100
- youtube: "유튜브 영상",
101
- iframe: "임베드",
102
- columns: "단 나누기"
100
+ const KIND_LABEL_KEY = {
101
+ youtube: "legacyYoutube",
102
+ iframe: "legacyIframe",
103
+ columns: "legacyColumns"
103
104
  };
104
105
  /** 자리표시자의 "새 탭에서 보기" 주소. 없으면 링크를 안 만든다. */
105
106
  function outboundUrl(kind, spec) {
@@ -156,6 +157,7 @@ export const LegacyBlock = Node.create({
156
157
  },
157
158
  addNodeView() {
158
159
  return ({ node, editor }) => {
160
+ const t = getEditorTranslator(editor);
159
161
  const kind = (node.attrs.kind ?? "iframe");
160
162
  const spec = node.attrs.spec;
161
163
  const dom = document.createElement("div");
@@ -189,7 +191,7 @@ export const LegacyBlock = Node.create({
189
191
  dom.style.cssText =
190
192
  "margin:8px 0;padding:16px;border:1px dashed rgba(120,130,150,0.5);border-radius:8px;background:rgba(120,130,150,0.06);box-sizing:border-box;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;";
191
193
  const label = document.createElement("span");
192
- label.textContent = `${KIND_LABEL[kind]} (옛 형식 · 편집 불가)`;
194
+ label.textContent = t("legacyNotice", { kind: t(KIND_LABEL_KEY[kind]) });
193
195
  label.style.cssText = "font-size:13px;font-weight:600;opacity:0.8;";
194
196
  dom.appendChild(label);
195
197
  /*
@@ -209,7 +211,7 @@ export const LegacyBlock = Node.create({
209
211
  open.href = url;
210
212
  open.target = "_blank";
211
213
  open.rel = "noopener noreferrer";
212
- open.textContent = "새 탭에서 보기";
214
+ open.textContent = t("openInNewTab");
213
215
  open.style.cssText = "font-size:13px;font-weight:600;text-decoration:underline;";
214
216
  dom.appendChild(open);
215
217
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Math.d.ts","sourceRoot":"","sources":["../../src/extensions/Math.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAa,IAAI,EAAgC,MAAM,cAAc,CAAC;AAK7E,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/B;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,UAAU,EAAE;YACV,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YAChD,iDAAiD;YACjD,gBAAgB,EAAE,MAAM,UAAU,CAAC;YACnC,+BAA+B;YAC/B,gBAAgB,EAAE,MAAM,UAAU,CAAC;SACpC,CAAC;QACF,WAAW,EAAE;YACX,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YACjD,8BAA8B;YAC9B,iBAAiB,EAAE,MAAM,UAAU,CAAC;SACrC,CAAC;KACH;CACF;AAkZD,eAAO,MAAM,UAAU,wBA4GrB,CAAC;AAEH,eAAO,MAAM,WAAW,wBA8EtB,CAAC"}
1
+ {"version":3,"file":"Math.d.ts","sourceRoot":"","sources":["../../src/extensions/Math.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAa,IAAI,EAAgC,MAAM,cAAc,CAAC;AAM7E,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/B;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,UAAU,EAAE;YACV,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YAChD,iDAAiD;YACjD,gBAAgB,EAAE,MAAM,UAAU,CAAC;YACnC,+BAA+B;YAC/B,gBAAgB,EAAE,MAAM,UAAU,CAAC;SACpC,CAAC;QACF,WAAW,EAAE;YACX,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YACjD,8BAA8B;YAC9B,iBAAiB,EAAE,MAAM,UAAU,CAAC;SACrC,CAAC;KACH;CACF;AAoZD,eAAO,MAAM,UAAU,wBA4GrB,CAAC;AAEH,eAAO,MAAM,WAAW,wBA8EtB,CAAC"}
@@ -34,6 +34,7 @@ import { InputRule, Node, mergeAttributes } from "@tiptap/core";
34
34
  import { Fragment, Slice } from "@tiptap/pm/model";
35
35
  import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
36
36
  import katex from "katex";
37
+ import { getEditorTranslator } from "../i18n";
37
38
  const INLINE_NAME = "math_inline";
38
39
  const DISPLAY_NAME = "math_display";
39
40
  /**
@@ -97,7 +98,8 @@ async function askMath(editor, current, displayMode) {
97
98
  const prompt = getPrompt(editor);
98
99
  if (prompt)
99
100
  return prompt(current, displayMode);
100
- return window.prompt(displayMode ? "수식 (LaTeX)" : "인라인 수식 (LaTeX)", current);
101
+ const t = getEditorTranslator(editor);
102
+ return window.prompt(t(displayMode ? "mathPromptDisplay" : "mathPromptInline"), current);
101
103
  }
102
104
  /** 프롬프트를 띄워 새 수식을 넣는다. 취소·빈 입력이면 아무것도 하지 않는다. */
103
105
  async function promptInsert(editor, displayMode) {
@@ -159,7 +161,8 @@ function buildNodeView(displayMode) {
159
161
  const latex = current.textContent.trim();
160
162
  if (!latex) {
161
163
  dom.classList.add("empty-math");
162
- dom.innerHTML = `<span class="math-placeholder">${displayMode ? "수식 입력" : "수식"}</span>`;
164
+ const t = getEditorTranslator(editor);
165
+ dom.innerHTML = `<span class="math-placeholder">${t(displayMode ? "mathEmpty" : "math")}</span>`;
163
166
  return;
164
167
  }
165
168
  dom.classList.remove("empty-math");
@@ -1 +1 @@
1
- {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAGrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BAwHpB,CAAC"}
1
+ {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAUrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BAmLpB,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
2
  import { attachResize } from "../utils/resize";
3
+ import { getEditorTranslator } from "../i18n";
4
+ import { mediaRatioCss, normalizeMediaAlign, normalizeMediaHeight, normalizeMediaRatio } from "../utils/media-size";
3
5
  export const MbusVideo = Node.create({
4
6
  name: "mbusVideo",
5
7
  group: "block",
@@ -8,10 +10,17 @@ export const MbusVideo = Node.create({
8
10
  addOptions() {
9
11
  return { HTMLAttributes: {} };
10
12
  },
13
+ /*
14
+ * 크기·정렬 저장 규칙은 `VideoEmbed` 와 같다 — 정본은 `data-*` 속성, `style` 은
15
+ * 폭·정렬의 거울. 이름만 `data-mbus-*` 로 다르다(저장 형식이 곧 계약이라 섞지 않는다).
16
+ */
11
17
  addAttributes() {
12
18
  return {
13
19
  src: { default: null },
14
- width: { default: null }
20
+ width: { default: null },
21
+ height: { default: null },
22
+ ratio: { default: null },
23
+ align: { default: null }
15
24
  };
16
25
  },
17
26
  parseHTML() {
@@ -22,7 +31,10 @@ export const MbusVideo = Node.create({
22
31
  const el = dom;
23
32
  return {
24
33
  src: el.getAttribute("data-mbus-src"),
25
- width: el.getAttribute("data-mbus-width") || el.style?.width || null
34
+ width: el.getAttribute("data-mbus-width") || el.style?.width || null,
35
+ height: normalizeMediaHeight(el.getAttribute("data-mbus-height")),
36
+ ratio: normalizeMediaRatio(el.getAttribute("data-mbus-ratio")),
37
+ align: normalizeMediaAlign(el.getAttribute("data-mbus-align"))
26
38
  };
27
39
  }
28
40
  }
@@ -32,27 +44,61 @@ export const MbusVideo = Node.create({
32
44
  const attrs = {
33
45
  "data-mbus-src": HTMLAttributes.src
34
46
  };
47
+ const styles = [];
35
48
  if (HTMLAttributes.width) {
36
49
  attrs["data-mbus-width"] = HTMLAttributes.width;
37
- attrs["style"] = `width: ${HTMLAttributes.width}`;
50
+ styles.push(`width: ${HTMLAttributes.width}`);
38
51
  }
52
+ const height = normalizeMediaHeight(HTMLAttributes.height);
53
+ if (height != null)
54
+ attrs["data-mbus-height"] = String(height);
55
+ const ratio = normalizeMediaRatio(HTMLAttributes.ratio);
56
+ if (height == null && ratio)
57
+ attrs["data-mbus-ratio"] = ratio;
58
+ const align = normalizeMediaAlign(HTMLAttributes.align);
59
+ if (align) {
60
+ attrs["data-mbus-align"] = align;
61
+ if (align === "center")
62
+ styles.push("margin-left: auto", "margin-right: auto");
63
+ else if (align === "right")
64
+ styles.push("margin-left: auto");
65
+ }
66
+ if (styles.length)
67
+ attrs["style"] = styles.join("; ");
39
68
  return ["div", mergeAttributes(this.options.HTMLAttributes, attrs)];
40
69
  },
41
70
  addNodeView() {
42
71
  return ({ node, editor, getPos }) => {
72
+ const t = getEditorTranslator(editor);
43
73
  // 리사이즈가 attrs 를 되쓰므로 **항상 최신 노드**여야 한다(복붙 시절의 stale 버그).
44
74
  let currentNode = node;
45
75
  let detachResize = null;
76
+ let detachHeightResize = null;
46
77
  const dom = document.createElement("div");
47
78
  dom.setAttribute("data-type", "mbusVideo");
48
79
  dom.setAttribute("data-node-view-wrapper", "");
49
80
  dom.style.cssText = "margin:8px 0;position:relative;box-sizing:border-box;max-width:100%;";
50
- if (node.attrs.width)
51
- dom.style.width = node.attrs.width;
81
+ /* `padding-top:56.25%` → `aspect-ratio` 로 바꾼 이유는 `VideoEmbed` 주석 참조. */
52
82
  const aspect = document.createElement("div");
53
83
  aspect.style.cssText =
54
- "position:relative;width:100%;padding-top:56.25%;background:#0b1020;border-radius:8px;overflow:hidden;";
84
+ "position:relative;width:100%;aspect-ratio:16 / 9;background:#0b1020;border-radius:8px;overflow:hidden;";
55
85
  dom.appendChild(aspect);
86
+ const applyLayout = (attrs) => {
87
+ dom.style.width = typeof attrs.width === "string" && attrs.width ? attrs.width : "";
88
+ const height = normalizeMediaHeight(attrs.height);
89
+ if (height != null) {
90
+ aspect.style.height = `${height}px`;
91
+ aspect.style.removeProperty("aspect-ratio");
92
+ }
93
+ else {
94
+ aspect.style.removeProperty("height");
95
+ aspect.style.aspectRatio = mediaRatioCss(attrs.ratio) ?? "16 / 9";
96
+ }
97
+ const align = normalizeMediaAlign(attrs.align);
98
+ dom.style.marginLeft = align === "center" || align === "right" ? "auto" : "";
99
+ dom.style.marginRight = align === "center" ? "auto" : "";
100
+ };
101
+ applyLayout(node.attrs);
56
102
  if (node.attrs.src) {
57
103
  const iframe = document.createElement("iframe");
58
104
  iframe.src = node.attrs.src;
@@ -82,7 +128,25 @@ export const MbusVideo = Node.create({
82
128
  getPos: () => (typeof getPos === "function" ? getPos() : undefined),
83
129
  getNode: () => currentNode,
84
130
  axis: "x",
85
- label: "영상 너비 조절"
131
+ label: t("videoResizeWidth")
132
+ });
133
+ /* 높이 드래그 — 크기는 비율 박스, 손잡이는 래퍼(`VideoEmbed` 주석 참조). */
134
+ detachHeightResize = attachResize({
135
+ dom: aspect,
136
+ handleParent: dom,
137
+ editor,
138
+ getPos: () => (typeof getPos === "function" ? getPos() : undefined),
139
+ getNode: () => currentNode,
140
+ axis: "y",
141
+ attr: "height",
142
+ min: 120,
143
+ max: 1600,
144
+ label: t("videoResizeHeight"),
145
+ buildAttrs: (current, value) => ({
146
+ ...current.attrs,
147
+ height: String(Math.round(value)),
148
+ ratio: null
149
+ })
86
150
  });
87
151
  }
88
152
  return {
@@ -90,13 +154,13 @@ export const MbusVideo = Node.create({
90
154
  update: (updated) => {
91
155
  if (updated.type !== currentNode.type)
92
156
  return false;
93
- const w = updated.attrs.width;
94
- dom.style.width = w || "";
157
+ applyLayout(updated.attrs);
95
158
  currentNode = updated;
96
159
  return true;
97
160
  },
98
161
  destroy: () => {
99
162
  detachResize?.();
163
+ detachHeightResize?.();
100
164
  }
101
165
  };
102
166
  };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 미디어 리사이즈 툴바 — 미디어 블록을 **선택하면** 그 아래에 뜨는 프리셋 칩 줄.
3
+ * (main 정올 `plugin/resize/index.ts` 1343줄 참조 — 의미 있는 로직만 재작성.)
4
+ *
5
+ * main 에서 가져온 것: 비율 프리셋(16:9·4:3·1:1), 좌/중/우 정렬, 폭 프리셋(100%·50%),
6
+ * "비율을 고르면 고정 높이를 풀고, 높이를 드래그하면 비율을 푼다" 는 배타 규칙.
7
+ * main 에서 **뺀 것**: hover 앵커 + 열고 닫는 상태 기계(여기서는 선택 = 표시라 상태가
8
+ * 필요 없다), 손잡이 겸 토글 버튼(높이 드래그는 각 노드뷰의 `attachResize` 가 맡는다),
9
+ * 드래그 프록시, `data-resize-handler` 로 아무 노드나 끼워 주는 범용 attr 경로,
10
+ * 이미지 전용 rAF 위치 보정. 전부 "코드 크기 대비 얻는 것" 이 없어서다.
11
+ *
12
+ * UI 는 ProseMirror **widget decoration** 으로 세운다 — NodeView 안에 넣으면 노드마다
13
+ * 툴바 DOM 이 상주하지만, 데코레이션은 선택된 하나에만 붙었다 떨어진다.
14
+ *
15
+ * ⚠️ **편집 모드 전용.** `decorations`/`mousedown` 양쪽에서 `editor.isEditable` 을 보고,
16
+ * `TipTapEditor` 도 편집일 때만 이 확장을 싣는다(이중 안전).
17
+ *
18
+ * ⚠️ 크기·정렬은 전부 **노드 속성**으로 저장된다(각 노드의 `renderHTML`/`parseHTML` 이
19
+ * 왕복을 책임진다). 여기서는 `setNodeMarkup` 으로 속성만 바꾼다 — DOM 을 직접 만지면
20
+ * 저장본과 화면이 갈라진다.
21
+ */
22
+ import { Extension } from "@tiptap/core";
23
+ export interface MediaToolbarTypeConfig {
24
+ /** 비율 프리셋 칩(16:9 등)을 보여줄지. 비율 박스를 그리는 노드뷰에만 켠다. */
25
+ ratio?: boolean;
26
+ /** 좌/중/우 정렬 칩. */
27
+ align?: boolean;
28
+ /** 폭 프리셋 칩(100%·50%). `width` 속성이 CSS 크기 문자열인 노드에만 켠다. */
29
+ widthPresets?: boolean;
30
+ }
31
+ export interface MediaResizeToolbarOptions {
32
+ /** 노드 타입 이름 → 보여줄 칩 구성. */
33
+ types: Record<string, MediaToolbarTypeConfig>;
34
+ }
35
+ /**
36
+ * 칩 하나가 만드는 속성 변경. **순수 함수** — 테스트가 이걸 직접 두드린다.
37
+ * 이미 켜진 칩을 다시 누르면 해제(`null`)다. main 의 `Auto` 칩을 재클릭-해제로 접었다.
38
+ */
39
+ export declare function applyMediaToolbarAction(attrs: Record<string, unknown>, action: string, value: string, config: MediaToolbarTypeConfig): Record<string, unknown> | null;
40
+ export declare const MediaResizeToolbar: Extension<MediaResizeToolbarOptions, any>;
41
+ //# sourceMappingURL=MediaToolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaToolbar.d.ts","sourceRoot":"","sources":["../../src/extensions/MediaToolbar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzC,MAAM,WAAW,sBAAsB;IACtC,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kBAAkB;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACzC,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CAC9C;AAsBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,sBAAsB,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAmBhC;AAuFD,eAAO,MAAM,kBAAkB,2CAiF7B,CAAC"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * 미디어 리사이즈 툴바 — 미디어 블록을 **선택하면** 그 아래에 뜨는 프리셋 칩 줄.
3
+ * (main 정올 `plugin/resize/index.ts` 1343줄 참조 — 의미 있는 로직만 재작성.)
4
+ *
5
+ * main 에서 가져온 것: 비율 프리셋(16:9·4:3·1:1), 좌/중/우 정렬, 폭 프리셋(100%·50%),
6
+ * "비율을 고르면 고정 높이를 풀고, 높이를 드래그하면 비율을 푼다" 는 배타 규칙.
7
+ * main 에서 **뺀 것**: hover 앵커 + 열고 닫는 상태 기계(여기서는 선택 = 표시라 상태가
8
+ * 필요 없다), 손잡이 겸 토글 버튼(높이 드래그는 각 노드뷰의 `attachResize` 가 맡는다),
9
+ * 드래그 프록시, `data-resize-handler` 로 아무 노드나 끼워 주는 범용 attr 경로,
10
+ * 이미지 전용 rAF 위치 보정. 전부 "코드 크기 대비 얻는 것" 이 없어서다.
11
+ *
12
+ * UI 는 ProseMirror **widget decoration** 으로 세운다 — NodeView 안에 넣으면 노드마다
13
+ * 툴바 DOM 이 상주하지만, 데코레이션은 선택된 하나에만 붙었다 떨어진다.
14
+ *
15
+ * ⚠️ **편집 모드 전용.** `decorations`/`mousedown` 양쪽에서 `editor.isEditable` 을 보고,
16
+ * `TipTapEditor` 도 편집일 때만 이 확장을 싣는다(이중 안전).
17
+ *
18
+ * ⚠️ 크기·정렬은 전부 **노드 속성**으로 저장된다(각 노드의 `renderHTML`/`parseHTML` 이
19
+ * 왕복을 책임진다). 여기서는 `setNodeMarkup` 으로 속성만 바꾼다 — DOM 을 직접 만지면
20
+ * 저장본과 화면이 갈라진다.
21
+ */
22
+ import { Extension } from "@tiptap/core";
23
+ import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
24
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
25
+ import { normalizeMediaAlign, normalizeMediaRatio } from "../utils/media-size";
26
+ import { getEditorTranslator } from "../i18n";
27
+ const RATIO_PRESETS = ["16:9", "4:3", "1:1"];
28
+ const ALIGN_PRESETS = [
29
+ { value: "left", labelKey: "alignLeftShort" },
30
+ { value: "center", labelKey: "alignCenterShort" },
31
+ { value: "right", labelKey: "alignRightShort" }
32
+ ];
33
+ const WIDTH_PRESETS = ["100%", "50%"];
34
+ /*
35
+ * ⚠️ 이미지의 `width` 는 단위 없는 px 정수 계약이라(`ResizableImage` 주석) `%` 프리셋을
36
+ * 주면 파싱에서 조용히 버려진다 — 그래서 이미지는 `widthPresets` 를 켜지 않는다.
37
+ * `tiptapMidibus`(정올 prod 강의영상)는 main 과 같게 높이 드래그만 갖고 툴바는 없다 —
38
+ * 저장이 `rawAttrs` 바이트 보존이라 칩이 만드는 속성 변경과 궁합이 나쁘다.
39
+ */
40
+ const DEFAULT_TYPES = {
41
+ image: { align: true },
42
+ videoEmbed: { ratio: true, align: true, widthPresets: true },
43
+ mbusVideo: { ratio: true, align: true, widthPresets: true }
44
+ };
45
+ /**
46
+ * 칩 하나가 만드는 속성 변경. **순수 함수** — 테스트가 이걸 직접 두드린다.
47
+ * 이미 켜진 칩을 다시 누르면 해제(`null`)다. main 의 `Auto` 칩을 재클릭-해제로 접었다.
48
+ */
49
+ export function applyMediaToolbarAction(attrs, action, value, config) {
50
+ if (action === "ratio" && config.ratio) {
51
+ const ratio = normalizeMediaRatio(value);
52
+ if (!ratio)
53
+ return null;
54
+ const current = normalizeMediaRatio(attrs.ratio);
55
+ /* 비율을 고르면 고정 높이를 푼다 — 높이가 남아 있으면 비율이 안 먹는다(노드뷰 규칙). */
56
+ return { ...attrs, ratio: ratio === current ? null : ratio, height: null };
57
+ }
58
+ if (action === "align" && config.align) {
59
+ const align = normalizeMediaAlign(value);
60
+ if (!align)
61
+ return null;
62
+ const current = normalizeMediaAlign(attrs.align);
63
+ return { ...attrs, align: align === current ? null : align };
64
+ }
65
+ if (action === "width" && config.widthPresets) {
66
+ if (!WIDTH_PRESETS.includes(value))
67
+ return null;
68
+ return { ...attrs, width: attrs.width === value ? null : value };
69
+ }
70
+ return null;
71
+ }
72
+ function chip(pos, action, value, label, active) {
73
+ const button = document.createElement("button");
74
+ button.type = "button";
75
+ button.className = "hce-media-toolbar-chip";
76
+ button.dataset.pos = String(pos);
77
+ button.dataset.action = action;
78
+ button.dataset.value = value;
79
+ button.textContent = label;
80
+ button.setAttribute("aria-pressed", active ? "true" : "false");
81
+ return button;
82
+ }
83
+ function separator() {
84
+ const span = document.createElement("span");
85
+ span.className = "hce-media-toolbar-separator";
86
+ span.setAttribute("aria-hidden", "true");
87
+ return span;
88
+ }
89
+ function buildToolbar(pos, node, config, t) {
90
+ /* 높이 0 앵커 — 문서 흐름을 밀지 않고 노드 바로 아래에 띄우기 위한 기준점. */
91
+ const anchor = document.createElement("div");
92
+ anchor.className = "hce-media-toolbar-anchor";
93
+ anchor.contentEditable = "false";
94
+ const toolbar = document.createElement("div");
95
+ toolbar.className = "hce-media-toolbar";
96
+ toolbar.setAttribute("role", "toolbar");
97
+ toolbar.setAttribute("aria-label", t("mediaToolbar"));
98
+ anchor.appendChild(toolbar);
99
+ const groups = [];
100
+ if (config.ratio) {
101
+ const currentRatio = normalizeMediaRatio(node.attrs.ratio);
102
+ groups.push(RATIO_PRESETS.map((preset) => chip(pos, "ratio", preset, preset, currentRatio === preset)));
103
+ }
104
+ if (config.align) {
105
+ const currentAlign = normalizeMediaAlign(node.attrs.align);
106
+ groups.push(ALIGN_PRESETS.map((preset) => chip(pos, "align", preset.value, t(preset.labelKey), currentAlign === preset.value)));
107
+ }
108
+ if (config.widthPresets) {
109
+ groups.push(WIDTH_PRESETS.map((preset) => chip(pos, "width", preset, preset, node.attrs.width === preset)));
110
+ }
111
+ groups.forEach((buttons, index) => {
112
+ if (index > 0)
113
+ toolbar.appendChild(separator());
114
+ buttons.forEach((button) => toolbar.appendChild(button));
115
+ });
116
+ return anchor;
117
+ }
118
+ /** 속성이 바뀌면 위젯을 다시 그리게 하는 키 — 같은 키면 ProseMirror 가 DOM 을 재사용한다. */
119
+ function toolbarKey(pos, node) {
120
+ return `media-toolbar-${pos}-${node.type.name}-${String(node.attrs.width ?? "")}-${String(node.attrs.height ?? "")}-${String(node.attrs.ratio ?? "")}-${String(node.attrs.align ?? "")}`;
121
+ }
122
+ export const MediaResizeToolbar = Extension.create({
123
+ name: "mediaResizeToolbar",
124
+ addOptions() {
125
+ return { types: DEFAULT_TYPES };
126
+ },
127
+ addProseMirrorPlugins() {
128
+ const getConfig = (node) => this.options.types[node.type.name] ?? null;
129
+ return [
130
+ new Plugin({
131
+ key: new PluginKey("mediaResizeToolbar"),
132
+ props: {
133
+ decorations: (state) => {
134
+ /* 읽기 전용에는 절대 안 뜬다. */
135
+ if (!this.editor.isEditable)
136
+ return DecorationSet.empty;
137
+ const { selection } = state;
138
+ if (!(selection instanceof NodeSelection))
139
+ return DecorationSet.empty;
140
+ const config = getConfig(selection.node);
141
+ if (!config)
142
+ return DecorationSet.empty;
143
+ const pos = selection.from;
144
+ const node = selection.node;
145
+ return DecorationSet.create(state.doc, [
146
+ Decoration.widget(pos + node.nodeSize, () => buildToolbar(pos, node, config, getEditorTranslator(this.editor)), { side: 1, key: toolbarKey(pos, node) })
147
+ ]);
148
+ },
149
+ handleDOMEvents: {
150
+ /*
151
+ * click 이 아니라 mousedown 이다 — 에디터가 mousedown 에서 선택을
152
+ * 옮기면 데코레이션이 사라져 click 이 영영 도착하지 않는다.
153
+ */
154
+ mousedown: (view, event) => {
155
+ if (!this.editor.isEditable)
156
+ return false;
157
+ if (!(event.target instanceof HTMLElement))
158
+ return false;
159
+ const button = event.target.closest(".hce-media-toolbar-chip");
160
+ if (!button)
161
+ return false;
162
+ event.preventDefault();
163
+ event.stopPropagation();
164
+ const pos = Number.parseInt(button.dataset.pos || "", 10);
165
+ if (!Number.isFinite(pos))
166
+ return true;
167
+ const node = view.state.doc.nodeAt(pos);
168
+ if (!node)
169
+ return true;
170
+ const config = getConfig(node);
171
+ if (!config)
172
+ return true;
173
+ const nextAttrs = applyMediaToolbarAction(node.attrs, button.dataset.action || "", button.dataset.value || "", config);
174
+ if (!nextAttrs)
175
+ return true;
176
+ const tr = view.state.tr.setNodeMarkup(pos, undefined, nextAttrs);
177
+ /* 선택을 유지해야 툴바가 새 상태로 다시 그려진다. */
178
+ const nodeSelection = (() => {
179
+ try {
180
+ return NodeSelection.create(tr.doc, pos);
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ })();
186
+ if (nodeSelection)
187
+ tr.setSelection(nodeSelection);
188
+ view.dispatch(tr);
189
+ return true;
190
+ }
191
+ }
192
+ }
193
+ })
194
+ ];
195
+ }
196
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MediaToolbar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaToolbar.test.d.ts","sourceRoot":"","sources":["../../src/extensions/MediaToolbar.test.ts"],"names":[],"mappings":""}