@lumir-company/editor 0.4.17 → 0.4.19
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/README.md +20 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +419 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +417 -13
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +41 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1261,6 +1261,7 @@ interface LumirEditorProps {
|
|
|
1261
1261
|
tableHandles?: boolean; // 테이블 핸들 표시 (기본: true)
|
|
1262
1262
|
floatingMenu?: boolean; // 상단 고정 플로팅 메뉴 표시 (기본: false)
|
|
1263
1263
|
floatingMenuPosition?: "sticky" | "fixed"; // 플로팅 메뉴 위치 (기본: "sticky")
|
|
1264
|
+
columnDivider?: boolean; // 2단(다단) 컬럼 사이 중앙 세로 구분선 표시 (기본: false)
|
|
1264
1265
|
className?: string; // 컨테이너 CSS 클래스
|
|
1265
1266
|
|
|
1266
1267
|
// === 링크 프리뷰 설정 ===
|
|
@@ -1478,6 +1479,25 @@ const url = await uploader(imageFile);
|
|
|
1478
1479
|
|
|
1479
1480
|
## 변경 로그
|
|
1480
1481
|
|
|
1482
|
+
### v0.4.19
|
|
1483
|
+
|
|
1484
|
+
- **2단 컬럼 중앙 세로 구분선 (`columnDivider` 옵션)** *(신규)*
|
|
1485
|
+
- `columnDivider` prop(기본 `false`)으로 2단(다단) 컬럼 사이 중앙에 세로 구분선 표시
|
|
1486
|
+
- 구분선 양쪽에 드래그 핸들(grip) 너비만큼 여백을 둬 핸들과 겹치지 않음
|
|
1487
|
+
- 색·여백을 CSS 변수로 조절: `--lumir-column-divider-color`(기본 `#e5e7eb`), `--lumir-column-grip-space`(기본 28px)
|
|
1488
|
+
- **표 전체 종횡비 고정 스케일 (코너 드래그)** *(신규)*
|
|
1489
|
+
- 표 우하단 모서리 hover 시 대각 리사이즈 커서 → 드래그로 표 전체를 종횡비 고정 균일 배율로 확대/축소
|
|
1490
|
+
- 모든 열 너비(`colwidth`)·행 높이(`rowHeight`)에 동일 배율 적용 → 행·열 상대 비율과 표 종횡비 유지
|
|
1491
|
+
- 셀 focus 없이 표 hover만으로 동작(코어 hover 상태 기반), 기존 행/열 개별 리사이즈와 충돌 없음
|
|
1492
|
+
- `tableHandles` prop으로 게이트(행/열 리사이즈와 동일)
|
|
1493
|
+
|
|
1494
|
+
### v0.4.18
|
|
1495
|
+
|
|
1496
|
+
- **표 열 삭제 버그 수정 (병합 셀 포함)**
|
|
1497
|
+
- 그립 메뉴로 첫 열/행 삭제 시, 그립 클릭으로 ProseMirror 선택이 표 밖으로 빠져 삭제가 무시되던 문제 수정(삭제 대상 표를 포커스 표 기준으로 결정적으로 탐색)
|
|
1498
|
+
- **세로 병합(rowspan) 인접 열**이 있을 때 첫 열이 삭제되지 않던 문제 수정 — 코어 prosemirror-tables가 행의 유일 셀을 지울 때 빈 셀을 남겨(`tableRow+` 스키마) `fixTables`가 원복하던 케이스. 열 삭제를 표 재구성 방식으로 변경해 colspan/rowspan 병합을 안전하게 처리
|
|
1499
|
+
- 병합 셀이 행 축소로 collapse될 때 **행 높이를 보존**(원래 차지하던 행 높이 합을 유지)
|
|
1500
|
+
|
|
1481
1501
|
### v0.4.17
|
|
1482
1502
|
|
|
1483
1503
|
- **표 행 높이(세로) 리사이즈**
|
package/dist/index.d.mts
CHANGED
|
@@ -119,6 +119,7 @@ interface LumirEditorProps {
|
|
|
119
119
|
onSelectionChange?: () => void;
|
|
120
120
|
className?: string;
|
|
121
121
|
sideMenuAddButton?: boolean;
|
|
122
|
+
columnDivider?: boolean;
|
|
122
123
|
floatingMenu?: boolean;
|
|
123
124
|
floatingMenuPosition?: "sticky" | "fixed";
|
|
124
125
|
linkPreview?: {
|
|
@@ -255,7 +256,7 @@ declare class EditorConfig {
|
|
|
255
256
|
*/
|
|
256
257
|
static getDisabledExtensions(userExtensions?: string[], allowVideo?: boolean, allowAudio?: boolean, allowFile?: boolean): string[];
|
|
257
258
|
}
|
|
258
|
-
declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, maxImageFileSize, maxVideoFileSize, linkPreview, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, placeholder, sideMenuAddButton, floatingMenu, floatingMenuPosition, onContentChange, onError, onImageDelete, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
|
|
259
|
+
declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, maxImageFileSize, maxVideoFileSize, linkPreview, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, placeholder, sideMenuAddButton, columnDivider, floatingMenu, floatingMenuPosition, onContentChange, onError, onImageDelete, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
|
|
259
260
|
|
|
260
261
|
declare function cn(...inputs: (string | undefined | null | false)[]): string;
|
|
261
262
|
|
package/dist/index.d.ts
CHANGED
|
@@ -119,6 +119,7 @@ interface LumirEditorProps {
|
|
|
119
119
|
onSelectionChange?: () => void;
|
|
120
120
|
className?: string;
|
|
121
121
|
sideMenuAddButton?: boolean;
|
|
122
|
+
columnDivider?: boolean;
|
|
122
123
|
floatingMenu?: boolean;
|
|
123
124
|
floatingMenuPosition?: "sticky" | "fixed";
|
|
124
125
|
linkPreview?: {
|
|
@@ -255,7 +256,7 @@ declare class EditorConfig {
|
|
|
255
256
|
*/
|
|
256
257
|
static getDisabledExtensions(userExtensions?: string[], allowVideo?: boolean, allowAudio?: boolean, allowFile?: boolean): string[];
|
|
257
258
|
}
|
|
258
|
-
declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, maxImageFileSize, maxVideoFileSize, linkPreview, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, placeholder, sideMenuAddButton, floatingMenu, floatingMenuPosition, onContentChange, onError, onImageDelete, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
|
|
259
|
+
declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, maxImageFileSize, maxVideoFileSize, linkPreview, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, placeholder, sideMenuAddButton, columnDivider, floatingMenu, floatingMenuPosition, onContentChange, onError, onImageDelete, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
|
|
259
260
|
|
|
260
261
|
declare function cn(...inputs: (string | undefined | null | false)[]): string;
|
|
261
262
|
|
package/dist/index.js
CHANGED
|
@@ -3436,6 +3436,8 @@ var ALLOWED_VIDEO_EXTENSIONS = [
|
|
|
3436
3436
|
];
|
|
3437
3437
|
var ROW_RESIZE_MIN_HEIGHT = 24;
|
|
3438
3438
|
var ROW_RESIZE_HANDLE_WIDTH = 5;
|
|
3439
|
+
var TABLE_SCALE_MIN_COL_WIDTH = 24;
|
|
3440
|
+
var TABLE_SCALE_MAX = 6;
|
|
3439
3441
|
|
|
3440
3442
|
// src/extensions/rowResizing.ts
|
|
3441
3443
|
var rowResizingPluginKey = new import_prosemirror_state3.PluginKey(
|
|
@@ -3714,9 +3716,165 @@ function handleDecorations(state, pluginState) {
|
|
|
3714
3716
|
return import_prosemirror_view2.DecorationSet.create(state.doc, decorations);
|
|
3715
3717
|
}
|
|
3716
3718
|
|
|
3717
|
-
// src/extensions/
|
|
3719
|
+
// src/extensions/tableScaling.ts
|
|
3718
3720
|
var import_prosemirror_state4 = require("prosemirror-state");
|
|
3719
|
-
var
|
|
3721
|
+
var import_prosemirror_view3 = require("prosemirror-view");
|
|
3722
|
+
var import_prosemirror_tables2 = require("prosemirror-tables");
|
|
3723
|
+
var tableScalingPluginKey = new import_prosemirror_state4.PluginKey(
|
|
3724
|
+
"lumirTableScaling"
|
|
3725
|
+
);
|
|
3726
|
+
function tableScaling() {
|
|
3727
|
+
return new import_prosemirror_state4.Plugin({
|
|
3728
|
+
key: tableScalingPluginKey,
|
|
3729
|
+
state: {
|
|
3730
|
+
init: () => null,
|
|
3731
|
+
apply(tr, prev) {
|
|
3732
|
+
const meta = tr.getMeta(tableScalingPluginKey);
|
|
3733
|
+
if (meta !== void 0) return meta.preview;
|
|
3734
|
+
if (prev && tr.docChanged) {
|
|
3735
|
+
return { ...prev, tablePos: tr.mapping.map(prev.tablePos, -1) };
|
|
3736
|
+
}
|
|
3737
|
+
return prev;
|
|
3738
|
+
}
|
|
3739
|
+
},
|
|
3740
|
+
props: {
|
|
3741
|
+
decorations(state) {
|
|
3742
|
+
const p = tableScalingPluginKey.getState(state);
|
|
3743
|
+
return p ? buildHeightDecorations(state, p) : null;
|
|
3744
|
+
}
|
|
3745
|
+
},
|
|
3746
|
+
view: (view) => ({
|
|
3747
|
+
update: () => {
|
|
3748
|
+
const p = tableScalingPluginKey.getState(view.state);
|
|
3749
|
+
if (p) applyColgroupPreview(view, p);
|
|
3750
|
+
}
|
|
3751
|
+
})
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
function setTableScalePreview(view, preview) {
|
|
3755
|
+
view.dispatch(view.state.tr.setMeta(tableScalingPluginKey, { preview }));
|
|
3756
|
+
}
|
|
3757
|
+
function measureTableForScale(view, tablePos) {
|
|
3758
|
+
const tableEl = findTableEl(view.nodeDOM(tablePos));
|
|
3759
|
+
const node = view.state.doc.nodeAt(tablePos);
|
|
3760
|
+
if (!tableEl || !node || node.type.name !== "table") return null;
|
|
3761
|
+
const map = import_prosemirror_tables2.TableMap.get(node);
|
|
3762
|
+
const rect = tableEl.getBoundingClientRect();
|
|
3763
|
+
const body = tableEl.tBodies[0];
|
|
3764
|
+
const rowHeights = body ? Array.from(body.rows).map((tr) => tr.getBoundingClientRect().height) : [];
|
|
3765
|
+
const colWidths = measureColWidths(tableEl, map.width);
|
|
3766
|
+
if (rowHeights.length !== map.height || colWidths.length !== map.width) {
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3769
|
+
return {
|
|
3770
|
+
tablePos,
|
|
3771
|
+
colWidths,
|
|
3772
|
+
rowHeights,
|
|
3773
|
+
origW: rect.width,
|
|
3774
|
+
origH: rect.height,
|
|
3775
|
+
scale: 1
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function commitTableScale(view, preview) {
|
|
3779
|
+
const { state } = view;
|
|
3780
|
+
const { tablePos, colWidths, rowHeights, scale } = preview;
|
|
3781
|
+
const table = state.doc.nodeAt(tablePos);
|
|
3782
|
+
if (!table || table.type.name !== "table") return;
|
|
3783
|
+
const map = import_prosemirror_tables2.TableMap.get(table);
|
|
3784
|
+
const start = tablePos + 1;
|
|
3785
|
+
const tr = state.tr;
|
|
3786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3787
|
+
for (const relPos of map.map) {
|
|
3788
|
+
if (seen.has(relPos)) continue;
|
|
3789
|
+
seen.add(relPos);
|
|
3790
|
+
const node = table.nodeAt(relPos);
|
|
3791
|
+
if (!node) continue;
|
|
3792
|
+
const rect = map.findCell(relPos);
|
|
3793
|
+
const colwidth = [];
|
|
3794
|
+
for (let c = rect.left; c < rect.right; c++) {
|
|
3795
|
+
colwidth.push(Math.round((colWidths[c] ?? 0) * scale));
|
|
3796
|
+
}
|
|
3797
|
+
let h = 0;
|
|
3798
|
+
for (let r = rect.top; r < rect.bottom; r++) h += rowHeights[r] ?? 0;
|
|
3799
|
+
const rowHeight = Math.round(h * scale);
|
|
3800
|
+
tr.setNodeMarkup(start + relPos, void 0, {
|
|
3801
|
+
...node.attrs,
|
|
3802
|
+
colwidth: colwidth.some((w) => w > 0) ? colwidth : null,
|
|
3803
|
+
rowHeight: rowHeight > 0 ? rowHeight : null
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
if (tr.docChanged) view.dispatch(tr);
|
|
3807
|
+
}
|
|
3808
|
+
function measureColWidths(tableEl, width) {
|
|
3809
|
+
const widths = new Array(width).fill(0);
|
|
3810
|
+
const colgroup = tableEl.querySelector("colgroup");
|
|
3811
|
+
if (colgroup && colgroup.children.length === width) {
|
|
3812
|
+
let allSet = true;
|
|
3813
|
+
for (let i = 0; i < width; i++) {
|
|
3814
|
+
const w = parseFloat(colgroup.children[i].style.width);
|
|
3815
|
+
if (Number.isFinite(w) && w > 0) widths[i] = w;
|
|
3816
|
+
else allSet = false;
|
|
3817
|
+
}
|
|
3818
|
+
if (allSet) return widths;
|
|
3819
|
+
}
|
|
3820
|
+
const firstRow = tableEl.tBodies[0]?.rows[0];
|
|
3821
|
+
if (firstRow) {
|
|
3822
|
+
let col = 0;
|
|
3823
|
+
for (const cell of Array.from(firstRow.cells)) {
|
|
3824
|
+
const span = cell.colSpan || 1;
|
|
3825
|
+
const w = cell.getBoundingClientRect().width / span;
|
|
3826
|
+
for (let s = 0; s < span && col < width; s++) widths[col++] = w;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
return widths;
|
|
3830
|
+
}
|
|
3831
|
+
function applyColgroupPreview(view, p) {
|
|
3832
|
+
const tableEl = findTableEl(view.nodeDOM(p.tablePos));
|
|
3833
|
+
const colgroup = tableEl?.querySelector("colgroup");
|
|
3834
|
+
if (!colgroup) return;
|
|
3835
|
+
const cols = colgroup.children;
|
|
3836
|
+
for (let i = 0; i < cols.length && i < p.colWidths.length; i++) {
|
|
3837
|
+
cols[i].style.width = Math.round(p.colWidths[i] * p.scale) + "px";
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
function buildHeightDecorations(state, p) {
|
|
3841
|
+
const table = state.doc.nodeAt(p.tablePos);
|
|
3842
|
+
if (!table || table.type.name !== "table") return import_prosemirror_view3.DecorationSet.empty;
|
|
3843
|
+
const map = import_prosemirror_tables2.TableMap.get(table);
|
|
3844
|
+
const start = p.tablePos + 1;
|
|
3845
|
+
const decorations = [];
|
|
3846
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3847
|
+
for (const relPos of map.map) {
|
|
3848
|
+
if (seen.has(relPos)) continue;
|
|
3849
|
+
seen.add(relPos);
|
|
3850
|
+
const node = table.nodeAt(relPos);
|
|
3851
|
+
if (!node) continue;
|
|
3852
|
+
const rect = map.findCell(relPos);
|
|
3853
|
+
let h = 0;
|
|
3854
|
+
for (let r = rect.top; r < rect.bottom; r++) h += p.rowHeights[r] ?? 0;
|
|
3855
|
+
const from = start + relPos;
|
|
3856
|
+
const to = from + node.nodeSize;
|
|
3857
|
+
decorations.push(
|
|
3858
|
+
import_prosemirror_view3.Decoration.node(from, to, {
|
|
3859
|
+
class: "lumir-table-scale-dragging",
|
|
3860
|
+
style: `height: ${Math.round(h * p.scale)}px`
|
|
3861
|
+
})
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3864
|
+
return import_prosemirror_view3.DecorationSet.create(state.doc, decorations);
|
|
3865
|
+
}
|
|
3866
|
+
function findTableEl(dom) {
|
|
3867
|
+
if (!dom) return null;
|
|
3868
|
+
const el = dom;
|
|
3869
|
+
if (el.nodeName === "TABLE") return el;
|
|
3870
|
+
const inner = el.querySelector?.("table");
|
|
3871
|
+
if (inner) return inner;
|
|
3872
|
+
return el.closest?.("table") ?? null;
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// src/extensions/tableCellAttrPreserve.ts
|
|
3876
|
+
var import_prosemirror_state5 = require("prosemirror-state");
|
|
3877
|
+
var tableCellAttrPreserveKey = new import_prosemirror_state5.PluginKey(
|
|
3720
3878
|
"lumirTableCellAttrPreserve"
|
|
3721
3879
|
);
|
|
3722
3880
|
var PRESERVED_ATTRS = [
|
|
@@ -3773,7 +3931,7 @@ function collectCellAttrs(doc) {
|
|
|
3773
3931
|
return result;
|
|
3774
3932
|
}
|
|
3775
3933
|
function tableCellAttrPreserve() {
|
|
3776
|
-
return new
|
|
3934
|
+
return new import_prosemirror_state5.Plugin({
|
|
3777
3935
|
key: tableCellAttrPreserveKey,
|
|
3778
3936
|
appendTransaction(transactions, oldState, newState) {
|
|
3779
3937
|
if (!transactions.some((tr2) => tr2.docChanged)) {
|
|
@@ -3879,6 +4037,7 @@ var RowHeightExtension = import_core5.Extension.create({
|
|
|
3879
4037
|
const plugins = [tableCellAttrPreserve()];
|
|
3880
4038
|
if (this.options.resizable) {
|
|
3881
4039
|
plugins.push(rowResizing());
|
|
4040
|
+
plugins.push(tableScaling());
|
|
3882
4041
|
}
|
|
3883
4042
|
return plugins;
|
|
3884
4043
|
}
|
|
@@ -3886,11 +4045,11 @@ var RowHeightExtension = import_core5.Extension.create({
|
|
|
3886
4045
|
|
|
3887
4046
|
// src/extensions/TableAlignmentExtension.ts
|
|
3888
4047
|
var import_core6 = require("@tiptap/core");
|
|
3889
|
-
var
|
|
3890
|
-
var
|
|
3891
|
-
var tableAlignmentDecoKey = new
|
|
4048
|
+
var import_prosemirror_state6 = require("prosemirror-state");
|
|
4049
|
+
var import_prosemirror_view4 = require("prosemirror-view");
|
|
4050
|
+
var tableAlignmentDecoKey = new import_prosemirror_state6.PluginKey("lumirTableAlignmentDeco");
|
|
3892
4051
|
function tableAlignmentDecorationPlugin() {
|
|
3893
|
-
return new
|
|
4052
|
+
return new import_prosemirror_state6.Plugin({
|
|
3894
4053
|
key: tableAlignmentDecoKey,
|
|
3895
4054
|
props: {
|
|
3896
4055
|
decorations(state) {
|
|
@@ -3900,7 +4059,7 @@ function tableAlignmentDecorationPlugin() {
|
|
|
3900
4059
|
const align = node.attrs.tableAlignment;
|
|
3901
4060
|
if (align && align !== "left") {
|
|
3902
4061
|
decorations.push(
|
|
3903
|
-
|
|
4062
|
+
import_prosemirror_view4.Decoration.node(pos, pos + node.nodeSize, {
|
|
3904
4063
|
"data-table-alignment": align
|
|
3905
4064
|
})
|
|
3906
4065
|
);
|
|
@@ -3909,7 +4068,7 @@ function tableAlignmentDecorationPlugin() {
|
|
|
3909
4068
|
}
|
|
3910
4069
|
return void 0;
|
|
3911
4070
|
});
|
|
3912
|
-
return
|
|
4071
|
+
return import_prosemirror_view4.DecorationSet.create(state.doc, decorations);
|
|
3913
4072
|
}
|
|
3914
4073
|
}
|
|
3915
4074
|
});
|
|
@@ -3941,7 +4100,7 @@ var TableAlignmentExtension = import_core6.Extension.create({
|
|
|
3941
4100
|
});
|
|
3942
4101
|
|
|
3943
4102
|
// src/blocks/columns/insertColumns.ts
|
|
3944
|
-
var
|
|
4103
|
+
var import_prosemirror_state7 = require("prosemirror-state");
|
|
3945
4104
|
function insertTwoColumns(editor) {
|
|
3946
4105
|
const tiptap = editor?._tiptapEditor;
|
|
3947
4106
|
if (!tiptap) {
|
|
@@ -3973,7 +4132,7 @@ function insertTwoColumns(editor) {
|
|
|
3973
4132
|
try {
|
|
3974
4133
|
let tr = state.tr.insert(insertPos, list);
|
|
3975
4134
|
try {
|
|
3976
|
-
tr = tr.setSelection(
|
|
4135
|
+
tr = tr.setSelection(import_prosemirror_state7.TextSelection.create(tr.doc, insertPos + 4));
|
|
3977
4136
|
} catch {
|
|
3978
4137
|
}
|
|
3979
4138
|
tiptap.view.dispatch(tr.scrollIntoView());
|
|
@@ -4787,9 +4946,14 @@ var import_react33 = require("react");
|
|
|
4787
4946
|
function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
|
|
4788
4947
|
const { refs, floatingStyles, context, update } = (0, import_react32.useFloating)({
|
|
4789
4948
|
open: show,
|
|
4790
|
-
placement: orientation === "row" ? "left" : orientation === "col" ? "top" : "right",
|
|
4791
|
-
// col/row
|
|
4792
|
-
|
|
4949
|
+
placement: orientation === "row" ? "left" : orientation === "col" ? "top" : orientation === "corner" ? "bottom-start" : "right",
|
|
4950
|
+
// col/row/cell: 가장자리에 14px hit-area 중앙 정렬(-7).
|
|
4951
|
+
// corner: 18px hit-zone이 표 우하단 모서리에 걸치도록 위/좌로 살짝 당김(모서리 hover 자연).
|
|
4952
|
+
middleware: [
|
|
4953
|
+
(0, import_react32.offset)(
|
|
4954
|
+
orientation === "corner" ? { mainAxis: -6, crossAxis: -6 } : -7
|
|
4955
|
+
)
|
|
4956
|
+
],
|
|
4793
4957
|
whileElementsMounted: import_react32.autoUpdate
|
|
4794
4958
|
});
|
|
4795
4959
|
const { isMounted, styles } = (0, import_react32.useTransitionStyles)(context);
|
|
@@ -4817,6 +4981,9 @@ function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
|
|
|
4817
4981
|
if (orientation === "row") {
|
|
4818
4982
|
return new DOMRect(t.left, c.top, 0, c.height);
|
|
4819
4983
|
}
|
|
4984
|
+
if (orientation === "corner") {
|
|
4985
|
+
return new DOMRect(t.right, t.bottom, 0, 0);
|
|
4986
|
+
}
|
|
4820
4987
|
return c;
|
|
4821
4988
|
}
|
|
4822
4989
|
});
|
|
@@ -4834,6 +5001,33 @@ function useFocusedCellHandlePositioning(cellEl, tbodyEl, orientation, show) {
|
|
|
4834
5001
|
[floatingStyles, isMounted, refs.setFloating, styles]
|
|
4835
5002
|
);
|
|
4836
5003
|
}
|
|
5004
|
+
function useTableCornerPositioning(referencePosTable, show) {
|
|
5005
|
+
const { refs, floatingStyles, context } = (0, import_react32.useFloating)({
|
|
5006
|
+
open: show,
|
|
5007
|
+
placement: "bottom-start",
|
|
5008
|
+
// 18px hit-zone을 모서리에서 안쪽(위/좌)으로 당겨 표 위에 걸치게 한다.
|
|
5009
|
+
middleware: [(0, import_react32.offset)({ mainAxis: -12, crossAxis: -12 })],
|
|
5010
|
+
whileElementsMounted: import_react32.autoUpdate
|
|
5011
|
+
});
|
|
5012
|
+
const { isMounted, styles } = (0, import_react32.useTransitionStyles)(context);
|
|
5013
|
+
(0, import_react33.useEffect)(() => {
|
|
5014
|
+
if (!referencePosTable) {
|
|
5015
|
+
refs.setReference(null);
|
|
5016
|
+
return;
|
|
5017
|
+
}
|
|
5018
|
+
refs.setReference({
|
|
5019
|
+
getBoundingClientRect: () => new DOMRect(referencePosTable.right, referencePosTable.bottom, 0, 0)
|
|
5020
|
+
});
|
|
5021
|
+
}, [referencePosTable, refs]);
|
|
5022
|
+
return (0, import_react33.useMemo)(
|
|
5023
|
+
() => ({
|
|
5024
|
+
isMounted,
|
|
5025
|
+
ref: refs.setFloating,
|
|
5026
|
+
style: { ...styles, ...floatingStyles }
|
|
5027
|
+
}),
|
|
5028
|
+
[floatingStyles, isMounted, refs.setFloating, styles]
|
|
5029
|
+
);
|
|
5030
|
+
}
|
|
4837
5031
|
|
|
4838
5032
|
// src/components/LumirTableHandlesController.tsx
|
|
4839
5033
|
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
@@ -4858,6 +5052,9 @@ function LumirTableHandlesController() {
|
|
|
4858
5052
|
const frozenRef = (0, import_react36.useRef)(false);
|
|
4859
5053
|
const menuOpenRef = (0, import_react36.useRef)(false);
|
|
4860
5054
|
const draggingRef = (0, import_react36.useRef)(false);
|
|
5055
|
+
(0, import_react36.useEffect)(() => {
|
|
5056
|
+
editor.__lumirActiveTableId = focused?.block?.id ?? null;
|
|
5057
|
+
}, [editor, focused]);
|
|
4861
5058
|
const recompute = (0, import_react36.useCallback)(() => {
|
|
4862
5059
|
if (frozenRef.current) {
|
|
4863
5060
|
return;
|
|
@@ -4981,6 +5178,10 @@ function LumirTableHandlesController() {
|
|
|
4981
5178
|
const onEndExtend = (0, import_react36.useCallback)(() => {
|
|
4982
5179
|
editor.tableHandles?.unfreezeHandles();
|
|
4983
5180
|
}, [editor]);
|
|
5181
|
+
const tableCorner = useTableCornerPositioning(
|
|
5182
|
+
coreState?.referencePosTable ?? null,
|
|
5183
|
+
!!coreState?.widgetContainer
|
|
5184
|
+
);
|
|
4984
5185
|
const menuHandlers = (0, import_react36.useMemo)(() => {
|
|
4985
5186
|
const mk = (kind) => ({
|
|
4986
5187
|
freeze: () => {
|
|
@@ -5030,6 +5231,48 @@ function LumirTableHandlesController() {
|
|
|
5030
5231
|
}, [editor, recompute]);
|
|
5031
5232
|
const noop = (0, import_react36.useCallback)(() => {
|
|
5032
5233
|
}, []);
|
|
5234
|
+
const onScaleStart = (0, import_react36.useCallback)(
|
|
5235
|
+
(e) => {
|
|
5236
|
+
e.preventDefault();
|
|
5237
|
+
e.stopPropagation();
|
|
5238
|
+
const view = editor.prosemirrorView;
|
|
5239
|
+
const blockId = coreState?.block?.id;
|
|
5240
|
+
if (!view || !blockId) return;
|
|
5241
|
+
const tablePos = findTableNodePos(editor._tiptapEditor, blockId);
|
|
5242
|
+
if (tablePos < 0) return;
|
|
5243
|
+
const base = measureTableForScale(view, tablePos);
|
|
5244
|
+
if (!base) return;
|
|
5245
|
+
const minScale = Math.max(
|
|
5246
|
+
TABLE_SCALE_MIN_COL_WIDTH / Math.max(1, Math.min(...base.colWidths)),
|
|
5247
|
+
ROW_RESIZE_MIN_HEIGHT / Math.max(1, Math.min(...base.rowHeights))
|
|
5248
|
+
);
|
|
5249
|
+
const startX = e.clientX;
|
|
5250
|
+
const startY = e.clientY;
|
|
5251
|
+
let current = base;
|
|
5252
|
+
editor.tableHandles?.freezeHandles();
|
|
5253
|
+
const onMove = (me) => {
|
|
5254
|
+
if (me.buttons === 0) return onUp();
|
|
5255
|
+
const newW = base.origW + (me.clientX - startX);
|
|
5256
|
+
const newH = base.origH + (me.clientY - startY);
|
|
5257
|
+
const scale = Math.min(
|
|
5258
|
+
TABLE_SCALE_MAX,
|
|
5259
|
+
Math.max(minScale, Math.max(newW / base.origW, newH / base.origH))
|
|
5260
|
+
);
|
|
5261
|
+
current = { ...base, scale };
|
|
5262
|
+
setTableScalePreview(view, current);
|
|
5263
|
+
};
|
|
5264
|
+
const onUp = () => {
|
|
5265
|
+
window.removeEventListener("pointermove", onMove);
|
|
5266
|
+
window.removeEventListener("pointerup", onUp);
|
|
5267
|
+
setTableScalePreview(view, null);
|
|
5268
|
+
commitTableScale(view, current);
|
|
5269
|
+
editor.tableHandles?.unfreezeHandles();
|
|
5270
|
+
};
|
|
5271
|
+
window.addEventListener("pointermove", onMove);
|
|
5272
|
+
window.addEventListener("pointerup", onUp);
|
|
5273
|
+
},
|
|
5274
|
+
[editor, coreState]
|
|
5275
|
+
);
|
|
5033
5276
|
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
|
|
5034
5277
|
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { ref: setMenuContainerRef }),
|
|
5035
5278
|
th && focused && menuContainerRef && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react35.FloatingPortal, { root: focused.widgetContainer, children: [
|
|
@@ -5145,6 +5388,16 @@ function LumirTableHandlesController() {
|
|
|
5145
5388
|
}
|
|
5146
5389
|
)
|
|
5147
5390
|
}
|
|
5391
|
+
),
|
|
5392
|
+
tableCorner.isMounted && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
5393
|
+
"div",
|
|
5394
|
+
{
|
|
5395
|
+
ref: tableCorner.ref,
|
|
5396
|
+
style: tableCorner.style,
|
|
5397
|
+
className: "lumir-tbl-scale-handle",
|
|
5398
|
+
title: "\uD45C \uD06C\uAE30 \uC870\uC808 (\uC885\uD6A1\uBE44 \uACE0\uC815)",
|
|
5399
|
+
onPointerDown: onScaleStart
|
|
5400
|
+
}
|
|
5148
5401
|
)
|
|
5149
5402
|
] })
|
|
5150
5403
|
] });
|
|
@@ -5400,6 +5653,140 @@ function liftFontSize(blocks) {
|
|
|
5400
5653
|
return mapPreservingRef(blocks, (b) => mapBlock(b, "lift"));
|
|
5401
5654
|
}
|
|
5402
5655
|
|
|
5656
|
+
// src/utils/table-delete.ts
|
|
5657
|
+
var import_prosemirror_tables3 = require("prosemirror-tables");
|
|
5658
|
+
function measureRowHeights(view, tablePos) {
|
|
5659
|
+
try {
|
|
5660
|
+
const at = view?.domAtPos?.(tablePos + 1);
|
|
5661
|
+
let el = at?.node;
|
|
5662
|
+
if (el && el.nodeType === 3) el = el.parentElement;
|
|
5663
|
+
const tableEl = el?.closest?.("table") ?? null;
|
|
5664
|
+
const body = tableEl?.tBodies?.[0];
|
|
5665
|
+
if (!body) return null;
|
|
5666
|
+
return Array.from(body.rows).map(
|
|
5667
|
+
(tr) => Math.round(tr.getBoundingClientRect().height)
|
|
5668
|
+
);
|
|
5669
|
+
} catch {
|
|
5670
|
+
return null;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
function buildDeleteColumnTr(state, tablePos, col, rowPx) {
|
|
5674
|
+
const table = state.doc.nodeAt(tablePos);
|
|
5675
|
+
if (!table || table.type.name !== "table") return null;
|
|
5676
|
+
const map = import_prosemirror_tables3.TableMap.get(table);
|
|
5677
|
+
const W = map.width;
|
|
5678
|
+
const H = map.height;
|
|
5679
|
+
if (col < 0 || col >= W || W <= 1) return null;
|
|
5680
|
+
const cells = [];
|
|
5681
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5682
|
+
for (let r = 0; r < H; r++) {
|
|
5683
|
+
for (let c = 0; c < W; c++) {
|
|
5684
|
+
const pos = map.map[r * W + c];
|
|
5685
|
+
if (seen.has(pos)) continue;
|
|
5686
|
+
seen.add(pos);
|
|
5687
|
+
const node = table.nodeAt(pos);
|
|
5688
|
+
if (!node) continue;
|
|
5689
|
+
cells.push({
|
|
5690
|
+
node,
|
|
5691
|
+
top: r,
|
|
5692
|
+
left: c,
|
|
5693
|
+
rowspan: node.attrs.rowspan ?? 1,
|
|
5694
|
+
colspan: node.attrs.colspan ?? 1
|
|
5695
|
+
});
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
const spansCol = (c) => c.left <= col && col < c.left + c.colspan;
|
|
5699
|
+
const isDropped = (c) => spansCol(c) && c.colspan === 1;
|
|
5700
|
+
const survivingByRow = new Array(H).fill(0);
|
|
5701
|
+
for (const c of cells) if (!isDropped(c)) survivingByRow[c.top]++;
|
|
5702
|
+
const rowDies = survivingByRow.map((n) => n === 0);
|
|
5703
|
+
const newRowIndex = [];
|
|
5704
|
+
let ni = 0;
|
|
5705
|
+
for (let r = 0; r < H; r++) newRowIndex[r] = rowDies[r] ? -1 : ni++;
|
|
5706
|
+
const newH = ni;
|
|
5707
|
+
if (newH === 0) return null;
|
|
5708
|
+
const newRows = Array.from({ length: newH }, () => []);
|
|
5709
|
+
for (const c of cells) {
|
|
5710
|
+
if (isDropped(c)) continue;
|
|
5711
|
+
const newColspan = c.colspan - (spansCol(c) ? 1 : 0);
|
|
5712
|
+
let dyingWithin = 0;
|
|
5713
|
+
for (let r = c.top; r < c.top + c.rowspan; r++) if (rowDies[r]) dyingWithin++;
|
|
5714
|
+
const newRowspan = Math.max(1, c.rowspan - dyingWithin);
|
|
5715
|
+
const attrs = { ...c.node.attrs, colspan: newColspan, rowspan: newRowspan };
|
|
5716
|
+
if (spansCol(c) && Array.isArray(attrs.colwidth) && attrs.colwidth.length) {
|
|
5717
|
+
const cw = attrs.colwidth.slice();
|
|
5718
|
+
cw.splice(col - c.left, 1);
|
|
5719
|
+
attrs.colwidth = cw.some((w) => w > 0) ? cw : null;
|
|
5720
|
+
}
|
|
5721
|
+
if (dyingWithin > 0 && rowPx) {
|
|
5722
|
+
let sum = 0;
|
|
5723
|
+
let ok = true;
|
|
5724
|
+
for (let r = c.top; r < c.top + c.rowspan; r++) {
|
|
5725
|
+
if (typeof rowPx[r] !== "number") {
|
|
5726
|
+
ok = false;
|
|
5727
|
+
break;
|
|
5728
|
+
}
|
|
5729
|
+
sum += rowPx[r];
|
|
5730
|
+
}
|
|
5731
|
+
if (ok && sum > 0) attrs.rowHeight = Math.round(sum);
|
|
5732
|
+
}
|
|
5733
|
+
const target = newRowIndex[c.top];
|
|
5734
|
+
if (target >= 0) newRows[target].push(c.node.type.create(attrs, c.node.content));
|
|
5735
|
+
}
|
|
5736
|
+
const rowType = table.child(0)?.type;
|
|
5737
|
+
if (!rowType) return null;
|
|
5738
|
+
const rowNodes = newRows.map((rowCells) => rowType.create(null, rowCells));
|
|
5739
|
+
const newTable = table.type.create(table.attrs, rowNodes);
|
|
5740
|
+
const tr = state.tr;
|
|
5741
|
+
tr.replaceWith(tablePos, tablePos + table.nodeSize, newTable);
|
|
5742
|
+
return tr.docChanged ? tr : null;
|
|
5743
|
+
}
|
|
5744
|
+
function removeFocusedRowOrColumn(editor, index, direction) {
|
|
5745
|
+
const tiptap = editor?._tiptapEditor;
|
|
5746
|
+
if (!tiptap) return false;
|
|
5747
|
+
const { state } = tiptap;
|
|
5748
|
+
let tablePos = -1;
|
|
5749
|
+
const activeId = editor?.__lumirActiveTableId;
|
|
5750
|
+
if (activeId) {
|
|
5751
|
+
const p = findTableNodePos(tiptap, activeId);
|
|
5752
|
+
if (p >= 0 && state.doc.nodeAt(p)?.type.name === "table") tablePos = p;
|
|
5753
|
+
}
|
|
5754
|
+
if (tablePos < 0) {
|
|
5755
|
+
const $from = state.selection.$from;
|
|
5756
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
5757
|
+
if ($from.node(d).type.name === "table") {
|
|
5758
|
+
tablePos = $from.before(d);
|
|
5759
|
+
break;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
if (tablePos < 0) {
|
|
5764
|
+
const vp = editor?.tableHandles?.view?.tablePos;
|
|
5765
|
+
if (typeof vp === "number" && state.doc.nodeAt(vp)?.type.name === "table") {
|
|
5766
|
+
tablePos = vp;
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
if (tablePos < 0) return false;
|
|
5770
|
+
try {
|
|
5771
|
+
if (direction === "column") {
|
|
5772
|
+
const rowPx = measureRowHeights(tiptap.view, tablePos);
|
|
5773
|
+
const tr = buildDeleteColumnTr(state, tablePos, index, rowPx);
|
|
5774
|
+
if (!tr) return false;
|
|
5775
|
+
tiptap.view.dispatch(tr);
|
|
5776
|
+
return true;
|
|
5777
|
+
}
|
|
5778
|
+
const tableInside = state.doc.resolve(tablePos + 1);
|
|
5779
|
+
const rowStart = state.doc.resolve(tableInside.posAtIndex(index) + 1);
|
|
5780
|
+
const cellPos = state.doc.resolve(rowStart.posAtIndex(0));
|
|
5781
|
+
const selState = state.apply(
|
|
5782
|
+
state.tr.setSelection(new import_prosemirror_tables3.CellSelection(cellPos))
|
|
5783
|
+
);
|
|
5784
|
+
return (0, import_prosemirror_tables3.deleteRow)(selState, (tr) => tiptap.view.dispatch(tr));
|
|
5785
|
+
} catch {
|
|
5786
|
+
return false;
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5403
5790
|
// src/utils/excel-paste.ts
|
|
5404
5791
|
var NAMED_COLORS = {
|
|
5405
5792
|
black: "#000000",
|
|
@@ -5941,6 +6328,7 @@ function LumirEditor({
|
|
|
5941
6328
|
className = "",
|
|
5942
6329
|
placeholder,
|
|
5943
6330
|
sideMenuAddButton = false,
|
|
6331
|
+
columnDivider = false,
|
|
5944
6332
|
floatingMenu = false,
|
|
5945
6333
|
floatingMenuPosition = "sticky",
|
|
5946
6334
|
// callbacks / refs
|
|
@@ -6253,6 +6641,18 @@ function LumirEditor({
|
|
|
6253
6641
|
editor.isEditable = editable;
|
|
6254
6642
|
}
|
|
6255
6643
|
}, [editor, editable]);
|
|
6644
|
+
(0, import_react37.useEffect)(() => {
|
|
6645
|
+
if (!editor) return;
|
|
6646
|
+
const th = editor.tableHandles;
|
|
6647
|
+
if (!th || th.__lumirColDelPatched || typeof th.removeRowOrColumn !== "function")
|
|
6648
|
+
return;
|
|
6649
|
+
const orig = th.removeRowOrColumn.bind(th);
|
|
6650
|
+
th.removeRowOrColumn = (index, dir) => {
|
|
6651
|
+
if (removeFocusedRowOrColumn(editor, index, dir)) return;
|
|
6652
|
+
return orig(index, dir);
|
|
6653
|
+
};
|
|
6654
|
+
th.__lumirColDelPatched = true;
|
|
6655
|
+
}, [editor]);
|
|
6256
6656
|
(0, import_react37.useEffect)(() => {
|
|
6257
6657
|
if (!editor || !floatingMenu) return;
|
|
6258
6658
|
const ft = editor.formattingToolbar;
|
|
@@ -6443,7 +6843,11 @@ function LumirEditor({
|
|
|
6443
6843
|
return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
|
|
6444
6844
|
"div",
|
|
6445
6845
|
{
|
|
6446
|
-
className: cn(
|
|
6846
|
+
className: cn(
|
|
6847
|
+
"lumirEditor",
|
|
6848
|
+
columnDivider && "lumir-column-divider",
|
|
6849
|
+
className
|
|
6850
|
+
),
|
|
6447
6851
|
style: { position: "relative", display: "flex", flexDirection: "column" },
|
|
6448
6852
|
children: [
|
|
6449
6853
|
floatingMenu && editor && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
|