@lumir-company/editor 0.4.19 → 0.4.21

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.
package/dist/index.mjs CHANGED
@@ -1466,6 +1466,16 @@ var ColumnList = createStronglyTypedTiptapNode({
1466
1466
  name: "columnList",
1467
1467
  group: "childContainer bnBlock blockGroupChild",
1468
1468
  content: "column column+",
1469
+ addAttributes() {
1470
+ return {
1471
+ // 블록별 중앙 세로 구분선 표시 여부(드래그핸들 메뉴에서 토글). data-divider로 렌더.
1472
+ showDivider: {
1473
+ default: false,
1474
+ parseHTML: (element) => element.getAttribute("data-divider") === "true",
1475
+ renderHTML: (attributes) => attributes.showDivider ? { "data-divider": "true" } : {}
1476
+ }
1477
+ };
1478
+ },
1469
1479
  parseHTML() {
1470
1480
  return [{ tag: 'div[data-node-type="columnList"]' }];
1471
1481
  },
@@ -1914,7 +1924,8 @@ var HtmlPreviewBlock = createReactBlockSpec3(
1914
1924
  );
1915
1925
  var ColumnListBlock = createBlockSpecFromStronglyTypedTiptapNode(
1916
1926
  ColumnList,
1917
- {}
1927
+ // showDivider를 블록 prop으로 등록 → onContentChange JSON 직렬화 + 재로드 라운드트립.
1928
+ { showDivider: { default: false } }
1918
1929
  );
1919
1930
  var ColumnBlock = createBlockSpecFromStronglyTypedTiptapNode(Column, {});
1920
1931
  var schema = BlockNoteSchema.create({
@@ -4075,9 +4086,56 @@ var TableAlignmentExtension = Extension3.create({
4075
4086
  }
4076
4087
  });
4077
4088
 
4089
+ // src/extensions/TableSelectAllExtension.ts
4090
+ import { Extension as Extension4 } from "@tiptap/core";
4091
+ import { CellSelection, TableMap as TableMap3 } from "prosemirror-tables";
4092
+ var TableSelectAllExtension = Extension4.create({
4093
+ name: "lumirTableSelectAll",
4094
+ priority: 1e3,
4095
+ addKeyboardShortcuts() {
4096
+ return {
4097
+ "Mod-a": () => {
4098
+ const view = this.editor.view;
4099
+ const { state } = view;
4100
+ const sel = state.selection;
4101
+ const $from = sel.$from;
4102
+ let depth = -1;
4103
+ for (let d = $from.depth; d > 0; d--) {
4104
+ if ($from.node(d).type.name === "table") {
4105
+ depth = d;
4106
+ break;
4107
+ }
4108
+ }
4109
+ if (depth < 0) return false;
4110
+ const table = $from.node(depth);
4111
+ const tableStart = $from.start(depth);
4112
+ const map = TableMap3.get(table);
4113
+ const firstRel = map.map[0];
4114
+ const lastRel = map.map[map.map.length - 1];
4115
+ if (sel instanceof CellSelection) {
4116
+ const a = sel.$anchorCell.pos - tableStart;
4117
+ const h = sel.$headCell.pos - tableStart;
4118
+ if (Math.min(a, h) === firstRel && Math.max(a, h) === lastRel) {
4119
+ return false;
4120
+ }
4121
+ }
4122
+ const tr = state.tr.setSelection(
4123
+ CellSelection.create(
4124
+ state.doc,
4125
+ tableStart + firstRel,
4126
+ tableStart + lastRel
4127
+ )
4128
+ );
4129
+ view.dispatch(tr);
4130
+ return true;
4131
+ }
4132
+ };
4133
+ }
4134
+ });
4135
+
4078
4136
  // src/blocks/columns/insertColumns.ts
4079
4137
  import { TextSelection } from "prosemirror-state";
4080
- function insertTwoColumns(editor) {
4138
+ function insertTwoColumns(editor, showDivider = false) {
4081
4139
  const tiptap = editor?._tiptapEditor;
4082
4140
  if (!tiptap) {
4083
4141
  return false;
@@ -4104,7 +4162,7 @@ function insertTwoColumns(editor) {
4104
4162
  const insertPos = $from.after(depth);
4105
4163
  const mkBlock = () => blockContainer.create(null, paragraph.create());
4106
4164
  const mkColumn = () => column.create(null, mkBlock());
4107
- const list = columnList.create(null, [mkColumn(), mkColumn()]);
4165
+ const list = columnList.create({ showDivider }, [mkColumn(), mkColumn()]);
4108
4166
  try {
4109
4167
  let tr = state.tr.insert(insertPos, list);
4110
4168
  try {
@@ -5697,7 +5755,7 @@ function liftFontSize(blocks) {
5697
5755
  }
5698
5756
 
5699
5757
  // src/utils/table-delete.ts
5700
- import { CellSelection, TableMap as TableMap3, deleteRow } from "prosemirror-tables";
5758
+ import { CellSelection as CellSelection2, TableMap as TableMap4, deleteRow } from "prosemirror-tables";
5701
5759
  function measureRowHeights(view, tablePos) {
5702
5760
  try {
5703
5761
  const at = view?.domAtPos?.(tablePos + 1);
@@ -5716,7 +5774,7 @@ function measureRowHeights(view, tablePos) {
5716
5774
  function buildDeleteColumnTr(state, tablePos, col, rowPx) {
5717
5775
  const table = state.doc.nodeAt(tablePos);
5718
5776
  if (!table || table.type.name !== "table") return null;
5719
- const map = TableMap3.get(table);
5777
+ const map = TableMap4.get(table);
5720
5778
  const W = map.width;
5721
5779
  const H = map.height;
5722
5780
  if (col < 0 || col >= W || W <= 1) return null;
@@ -5822,7 +5880,7 @@ function removeFocusedRowOrColumn(editor, index, direction) {
5822
5880
  const rowStart = state.doc.resolve(tableInside.posAtIndex(index) + 1);
5823
5881
  const cellPos = state.doc.resolve(rowStart.posAtIndex(0));
5824
5882
  const selState = state.apply(
5825
- state.tr.setSelection(new CellSelection(cellPos))
5883
+ state.tr.setSelection(new CellSelection2(cellPos))
5826
5884
  );
5827
5885
  return deleteRow(selState, (tr) => tiptap.view.dispatch(tr));
5828
5886
  } catch {
@@ -5974,6 +6032,20 @@ function normalizeAlign(ta) {
5974
6032
  if (v === "justify") return "justify";
5975
6033
  return "";
5976
6034
  }
6035
+ function mapVerticalAlign(v) {
6036
+ const s = (v || "").trim().toLowerCase();
6037
+ if (s === "middle" || s === "center") return "middle";
6038
+ if (s === "bottom") return "bottom";
6039
+ return null;
6040
+ }
6041
+ function fontSizeToPx(raw) {
6042
+ if (!raw) return null;
6043
+ const m = String(raw).trim().match(/^([\d.]+)\s*(px|pt)?$/i);
6044
+ if (!m) return null;
6045
+ const v = parseFloat(m[1]);
6046
+ if (!Number.isFinite(v) || v <= 0) return null;
6047
+ return (m[2] || "px").toLowerCase() === "pt" ? v * (96 / 72) : v;
6048
+ }
5977
6049
  function applyCellFormatting(el, fmt) {
5978
6050
  if (fmt.bgRgb && !fmt.bgTransparent) {
5979
6051
  const v = nearestBackgroundColorValue(fmt.bgRgb);
@@ -5993,6 +6065,13 @@ function applyCellFormatting(el, fmt) {
5993
6065
  if (fmt.bold) inner = `<strong>${inner}</strong>`;
5994
6066
  el.innerHTML = inner;
5995
6067
  }
6068
+ if (fmt.verticalAlign) {
6069
+ el.setAttribute("data-vertical-alignment", fmt.verticalAlign);
6070
+ }
6071
+ if (fmt.fontSizePx && Math.abs(fmt.fontSizePx - 14) > 1 && el.innerHTML.trim()) {
6072
+ const v = `${Math.round(fmt.fontSizePx)}px`;
6073
+ el.innerHTML = `<span data-style-type="fontSize" data-value="${v}" style="font-size:${v}">` + el.innerHTML + `</span>`;
6074
+ }
5996
6075
  }
5997
6076
  function readComputedFormat(el) {
5998
6077
  const cs = getComputedStyle(el);
@@ -6006,7 +6085,13 @@ function readComputedFormat(el) {
6006
6085
  align: normalizeAlign(cs.textAlign),
6007
6086
  bold: fw === "bold" || fw === "bolder" || !isNaN(fwNum) && fwNum >= 600,
6008
6087
  italic: (cs.fontStyle || "").toLowerCase().includes("italic"),
6009
- underline: decoration.toLowerCase().includes("underline")
6088
+ underline: decoration.toLowerCase().includes("underline"),
6089
+ fontSizePx: fontSizeToPx(cs.fontSize),
6090
+ // ⚠️ computed vertical-align은 td 기본값이 "middle"이라 셀마다 잘못 붙는다.
6091
+ // 명시적 inline style / valign 속성만 읽는다(기본값 노이즈 방지).
6092
+ verticalAlign: mapVerticalAlign(
6093
+ el.style?.verticalAlign || el.getAttribute("valign")
6094
+ )
6010
6095
  };
6011
6096
  }
6012
6097
  function readInlineFormat(el) {
@@ -6022,7 +6107,11 @@ function readInlineFormat(el) {
6022
6107
  align: normalizeAlign(sm["text-align"] || el.getAttribute("align")),
6023
6108
  bold: fw === "bold" || fw === "bolder" || parseInt(fw, 10) >= 600,
6024
6109
  italic: (sm["font-style"] || "").toLowerCase().includes("italic"),
6025
- underline: decoration.toLowerCase().includes("underline")
6110
+ underline: decoration.toLowerCase().includes("underline"),
6111
+ fontSizePx: fontSizeToPx(sm["font-size"]),
6112
+ verticalAlign: mapVerticalAlign(
6113
+ sm["vertical-align"] || el.getAttribute("valign")
6114
+ )
6026
6115
  };
6027
6116
  }
6028
6117
  function normalizeExcelTableHtml(html) {
@@ -6036,7 +6125,7 @@ function normalizeExcelTableHtml(html) {
6036
6125
  try {
6037
6126
  host = document.createElement("div");
6038
6127
  host.setAttribute("aria-hidden", "true");
6039
- host.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none";
6128
+ host.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;font-size:14px";
6040
6129
  const shadow = host.attachShadow({ mode: "open" });
6041
6130
  const styles = Array.from(doc.querySelectorAll("style")).map((s) => s.outerHTML).join("");
6042
6131
  shadow.innerHTML = styles + doc.body.innerHTML;
@@ -6059,6 +6148,100 @@ function normalizeExcelTableHtml(html) {
6059
6148
  }
6060
6149
  }
6061
6150
 
6151
+ // src/utils/table-paste-fit.ts
6152
+ var MIN_COL_PX = 24;
6153
+ function toPx(raw, maxWidth) {
6154
+ if (!raw) return null;
6155
+ const m = String(raw).trim().match(/^([\d.]+)\s*(pt|px|%)?$/i);
6156
+ if (!m) return null;
6157
+ const v = parseFloat(m[1]);
6158
+ if (!Number.isFinite(v) || v <= 0) return null;
6159
+ const unit = (m[2] || "px").toLowerCase();
6160
+ if (unit === "pt") return v * (96 / 72);
6161
+ if (unit === "%") return v / 100 * maxWidth;
6162
+ return v;
6163
+ }
6164
+ function elWidthPx(el, maxWidth) {
6165
+ const styleW = el.style?.width;
6166
+ return toPx(styleW, maxWidth) ?? toPx(el.getAttribute("width"), maxWidth);
6167
+ }
6168
+ function readColumnWidths(table, maxWidth) {
6169
+ const colEls = table.querySelector("colgroup")?.querySelectorAll("col");
6170
+ if (colEls && colEls.length > 0) {
6171
+ const widths2 = [];
6172
+ let ok2 = true;
6173
+ colEls.forEach((c) => {
6174
+ const span = parseInt(c.getAttribute("span") || "1", 10) || 1;
6175
+ const w = elWidthPx(c, maxWidth);
6176
+ if (w == null) ok2 = false;
6177
+ for (let i = 0; i < span; i++) widths2.push(w ?? 0);
6178
+ });
6179
+ if (ok2 && widths2.length > 0) return widths2;
6180
+ }
6181
+ const firstRow = table.querySelector("tr");
6182
+ if (!firstRow) return null;
6183
+ const widths = [];
6184
+ let ok = true;
6185
+ Array.from(firstRow.children).forEach((cell) => {
6186
+ if (cell.tagName !== "TD" && cell.tagName !== "TH") return;
6187
+ const span = parseInt(cell.getAttribute("colspan") || "1", 10) || 1;
6188
+ const w = elWidthPx(cell, maxWidth);
6189
+ if (w == null) ok = false;
6190
+ const per = (w ?? 0) / span;
6191
+ for (let i = 0; i < span; i++) widths.push(per);
6192
+ });
6193
+ return ok && widths.length > 0 ? widths : null;
6194
+ }
6195
+ function fitWidths(widths, maxWidth) {
6196
+ const total = widths.reduce((a, b) => a + b, 0);
6197
+ if (total <= 0) return widths;
6198
+ const scale = total > maxWidth ? maxWidth / total : 1;
6199
+ return widths.map((w) => Math.max(MIN_COL_PX, Math.round(w * scale)));
6200
+ }
6201
+ function computeFittedColumnWidthsPerTable(html, maxWidth) {
6202
+ if (!html || typeof DOMParser === "undefined" || !(maxWidth > 0)) return [];
6203
+ let doc;
6204
+ try {
6205
+ doc = new DOMParser().parseFromString(html, "text/html");
6206
+ } catch {
6207
+ return [];
6208
+ }
6209
+ return Array.from(doc.querySelectorAll("table")).map((t) => {
6210
+ const widths = readColumnWidths(t, maxWidth);
6211
+ return widths ? fitWidths(widths, maxWidth) : null;
6212
+ });
6213
+ }
6214
+ function collectTableBlocks(blocks) {
6215
+ const out = [];
6216
+ const walk = (bs) => {
6217
+ for (const b of bs) {
6218
+ if (b?.type === "table") out.push(b);
6219
+ if (b?.children?.length) walk(b.children);
6220
+ }
6221
+ };
6222
+ walk(blocks || []);
6223
+ return out;
6224
+ }
6225
+ function applyFittedWidthsToNewTables(editor, beforeIds, perTable) {
6226
+ if (!editor || perTable.length === 0) return;
6227
+ const newTables = collectTableBlocks(editor.document).filter(
6228
+ (b) => !beforeIds.has(b.id)
6229
+ );
6230
+ newTables.forEach((tb, i) => {
6231
+ const widths = perTable[i];
6232
+ const current = tb?.content?.columnWidths;
6233
+ if (widths && Array.isArray(current) && current.length === widths.length) {
6234
+ try {
6235
+ editor.updateBlock(tb, {
6236
+ type: "table",
6237
+ content: { ...tb.content, columnWidths: widths }
6238
+ });
6239
+ } catch {
6240
+ }
6241
+ }
6242
+ });
6243
+ }
6244
+
6062
6245
  // src/components/LumirEditor.tsx
6063
6246
  import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
6064
6247
  var DEBUG_LOG = (loc, msg, data) => {
@@ -6485,7 +6668,9 @@ function LumirEditor({
6485
6668
  // tableHandles prop으로 게이트(기존 grip 컨트롤러와 동일 게이트).
6486
6669
  RowHeightExtension.configure({ resizable: tableHandles }),
6487
6670
  // 표 블록 정렬(좌/가운데/우) attr.
6488
- TableAlignmentExtension
6671
+ TableAlignmentExtension,
6672
+ // 셀 포커스 시 Ctrl/Cmd+A → 표 전체 선택.
6673
+ TableSelectAllExtension
6489
6674
  ]
6490
6675
  },
6491
6676
  placeholders: placeholder ? { default: placeholder, emptyDocument: placeholder } : void 0,
@@ -6591,7 +6776,14 @@ function LumirEditor({
6591
6776
  hasFiles: !!event?.clipboardData?.files?.length
6592
6777
  });
6593
6778
  event.preventDefault();
6779
+ const pmDom = editor2.prosemirrorView?.dom;
6780
+ const maxWidth = pmDom?.clientWidth ? pmDom.clientWidth - 8 : 0;
6781
+ const fittedWidths = computeFittedColumnWidthsPerTable(pastedHtml, maxWidth);
6782
+ const beforeTableIds = new Set(
6783
+ collectTableBlocks(editor2.document).map((b) => b.id)
6784
+ );
6594
6785
  editor2.pasteHTML(normalizeExcelTableHtml(pastedHtml));
6786
+ applyFittedWidthsToNewTables(editor2, beforeTableIds, fittedWidths);
6595
6787
  return true;
6596
6788
  }
6597
6789
  const fileList = event?.clipboardData?.files ?? null;
@@ -7087,33 +7279,52 @@ function LumirEditor({
7087
7279
  ),
7088
7280
  subtext: "HTML \uD30C\uC77C\uC744 \uBBF8\uB9AC\uBCF4\uAE30\uB85C \uC0BD\uC785"
7089
7281
  };
7282
+ const columnIcon = (withDivider) => /* @__PURE__ */ jsxs19(
7283
+ "svg",
7284
+ {
7285
+ width: "18",
7286
+ height: "18",
7287
+ viewBox: "0 0 24 24",
7288
+ fill: "none",
7289
+ stroke: "currentColor",
7290
+ strokeWidth: "2",
7291
+ strokeLinecap: "round",
7292
+ strokeLinejoin: "round",
7293
+ children: [
7294
+ /* @__PURE__ */ jsx27("rect", { x: "3", y: "4", width: "7", height: "16", rx: "1" }),
7295
+ /* @__PURE__ */ jsx27("rect", { x: "14", y: "4", width: "7", height: "16", rx: "1" }),
7296
+ withDivider && /* @__PURE__ */ jsx27("line", { x1: "12", y1: "3", x2: "12", y2: "21", strokeDasharray: "2 2" })
7297
+ ]
7298
+ }
7299
+ );
7090
7300
  const columnItem = {
7091
7301
  title: "2\uB2E8 \uCEEC\uB7FC",
7092
- onItemClick: () => {
7093
- insertTwoColumns(editor);
7094
- },
7302
+ onItemClick: () => insertTwoColumns(editor, false),
7095
7303
  aliases: ["columns", "column", "2col", "\uB2E8", "\uCEEC\uB7FC", "\uB2E4\uB2E8", "\uBD84\uD560"],
7096
7304
  group: "Basic blocks",
7097
- icon: /* @__PURE__ */ jsxs19(
7098
- "svg",
7099
- {
7100
- width: "18",
7101
- height: "18",
7102
- viewBox: "0 0 24 24",
7103
- fill: "none",
7104
- stroke: "currentColor",
7105
- strokeWidth: "2",
7106
- strokeLinecap: "round",
7107
- strokeLinejoin: "round",
7108
- children: [
7109
- /* @__PURE__ */ jsx27("rect", { x: "3", y: "4", width: "7", height: "16", rx: "1" }),
7110
- /* @__PURE__ */ jsx27("rect", { x: "14", y: "4", width: "7", height: "16", rx: "1" })
7111
- ]
7112
- }
7113
- ),
7305
+ icon: columnIcon(false),
7114
7306
  subtext: "\uBE14\uB85D\uC744 \uC88C\uC6B0 2\uB2E8\uC73C\uB85C \uBC30\uCE58"
7115
7307
  };
7116
- const allItems = [...filtered, htmlPreviewItem, columnItem];
7308
+ const columnDividerItem = {
7309
+ title: "2\uB2E8 \uCEEC\uB7FC (\uAD6C\uBD84\uC120)",
7310
+ onItemClick: () => insertTwoColumns(editor, true),
7311
+ aliases: [
7312
+ "columns divider",
7313
+ "\uAD6C\uBD84\uC120",
7314
+ "\uCEEC\uB7FC \uAD6C\uBD84\uC120",
7315
+ "2\uB2E8 \uAD6C\uBD84\uC120",
7316
+ "divider"
7317
+ ],
7318
+ group: "Basic blocks",
7319
+ icon: columnIcon(true),
7320
+ subtext: "\uAC00\uC6B4\uB370 \uC138\uB85C \uAD6C\uBD84\uC120\uC774 \uC788\uB294 2\uB2E8 \uCEEC\uB7FC"
7321
+ };
7322
+ const allItems = [
7323
+ ...filtered,
7324
+ htmlPreviewItem,
7325
+ columnItem,
7326
+ columnDividerItem
7327
+ ];
7117
7328
  if (linkPreview?.apiEndpoint) {
7118
7329
  allItems.push({
7119
7330
  title: "Link Preview",