@phuong-tran-redoc/document-engine-core 0.1.1 → 0.1.2
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/index.d.ts +1 -0
- package/index.esm.js +2238 -0
- package/package.json +15 -6
- package/src/constants/indent.constant.js +0 -1
- package/src/constants/index.js +0 -1
- package/src/constants/table.constant.js +0 -1
- package/src/extensions/clear-content.extension.js +0 -9
- package/src/extensions/indent.extension.js +0 -59
- package/src/extensions/index.js +0 -8
- package/src/extensions/ordered-list.extension.js +0 -47
- package/src/extensions/reset-format.extension.js +0 -9
- package/src/extensions/reset-on-enter.extension.js +0 -37
- package/src/extensions/restricted-editing.extension.js +0 -376
- package/src/extensions/table-resizing.extension.js +0 -164
- package/src/extensions/table-style.extension.js +0 -481
- package/src/extensions/text-case.extension.js +0 -39
- package/src/index.js +0 -9
- package/src/kit/default-extensions.js +0 -86
- package/src/kit/generate-html.js +0 -40
- package/src/kit/index.js +0 -2
- package/src/migrations/doc-migrations.js +0 -24
- package/src/migrations/index.js +0 -3
- package/src/migrations/migrate-doc.js +0 -32
- package/src/migrations/types.js +0 -4
- package/src/models/color.model.js +0 -30
- package/src/models/index.js +0 -1
- package/src/nodes/dynamic-field.node.js +0 -141
- package/src/nodes/heading.node.js +0 -13
- package/src/nodes/image-ref.node.js +0 -138
- package/src/nodes/index.js +0 -4
- package/src/nodes/page-break.node.js +0 -52
- package/src/types/index.js +0 -2
- package/src/types/ordered-list.type.js +0 -2
- package/src/types/text-case.type.js +0 -1
- package/src/types/tiptap-html-server.d.js +0 -10
- package/src/utils/color.util.js +0 -31
- package/src/utils/dom.util.js +0 -33
- package/src/utils/index.js +0 -4
- package/src/utils/table.util.js +0 -95
- package/src/utils/text.util.js +0 -46
- package/src/views/block-handler.js +0 -311
- package/src/views/index.js +0 -1
package/src/utils/dom.util.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export function getClosestDomElement(editor, selector) {
|
|
2
|
-
const { view, state } = editor;
|
|
3
|
-
const { selection } = state;
|
|
4
|
-
const { from } = selection;
|
|
5
|
-
if (!from) return null;
|
|
6
|
-
let domNodeAtPos = null;
|
|
7
|
-
// Ưu tiên tìm DOM node của 'nearest parent element' của vị trí from
|
|
8
|
-
// Nếu from là trong text, $from.parent.nodeDOM có thể chính xác hơn
|
|
9
|
-
try {
|
|
10
|
-
const resolvedPos = state.doc.resolve(from);
|
|
11
|
-
domNodeAtPos = view.domAtPos(resolvedPos.pos).node; // Bắt đầu từ node ở vị trí from
|
|
12
|
-
// Nếu domNodeAtPos không phải là HTMLElement (ví dụ, là một Text node),
|
|
13
|
-
// thì cố gắng lấy element cha của nó trong DOM
|
|
14
|
-
if (domNodeAtPos && !(domNodeAtPos instanceof HTMLElement)) {
|
|
15
|
-
domNodeAtPos = domNodeAtPos.parentElement;
|
|
16
|
-
}
|
|
17
|
-
} catch (e) {
|
|
18
|
-
// Xử lý trường hợp hiếm gặp khi nodeDOM(from) không trả về gì hợp lệ
|
|
19
|
-
console.warn('Could not find DOM node at cursor position:', e);
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
if (!domNodeAtPos) return null;
|
|
23
|
-
const boundary = editor.view.dom;
|
|
24
|
-
// Tìm kiếm từ domNodeAtPos lên trên cho đến khi gặp editor container
|
|
25
|
-
let currentElement = domNodeAtPos;
|
|
26
|
-
while(currentElement && currentElement !== boundary){
|
|
27
|
-
if (currentElement.matches(selector)) {
|
|
28
|
-
return currentElement;
|
|
29
|
-
}
|
|
30
|
-
currentElement = currentElement.parentElement;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
package/src/utils/index.js
DELETED
package/src/utils/table.util.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { CellSelection, findTable, selectionCell, TableMap } from '@tiptap/pm/tables';
|
|
2
|
-
import { isEqual } from 'lodash-es';
|
|
3
|
-
export function getCursorCellInfo(state) {
|
|
4
|
-
// 1. Dùng selectionCell để tìm vị trí bắt đầu của ô chứa con trỏ
|
|
5
|
-
try {
|
|
6
|
-
const cellInfo = selectionCell(state);
|
|
7
|
-
// Nếu không tìm thấy (con trỏ không nằm trong ô nào), trả về null
|
|
8
|
-
if (!cellInfo) return null;
|
|
9
|
-
// cellInfo.$pos là một ResolvedPos trỏ tới vị trí bắt đầu của ô
|
|
10
|
-
const table = findTable(cellInfo);
|
|
11
|
-
if (!table) return null;
|
|
12
|
-
const map = TableMap.get(table.node);
|
|
13
|
-
// 2. Lấy vị trí tương đối của ô so với bảng
|
|
14
|
-
const cellOffset = cellInfo.pos - table.start;
|
|
15
|
-
// 3. Tìm chỉ số (index) của ô này trong "bản đồ" của bảng
|
|
16
|
-
const cellIndex = map.map.indexOf(cellOffset);
|
|
17
|
-
if (cellIndex === -1) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
// 4. Từ chỉ số, tính ra rowIndex và colIndex
|
|
21
|
-
const rowIndex = Math.floor(cellIndex / map.width);
|
|
22
|
-
const colIndex = cellIndex % map.width;
|
|
23
|
-
return {
|
|
24
|
-
rowIndex,
|
|
25
|
-
colIndex,
|
|
26
|
-
cellIndex,
|
|
27
|
-
tableStart: table.start,
|
|
28
|
-
tableNode: table.node,
|
|
29
|
-
map
|
|
30
|
-
};
|
|
31
|
-
} catch {
|
|
32
|
-
// selectionCell throws RangeError when cursor is not in a table
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export function getSelectedCells(state) {
|
|
37
|
-
const { selection } = state;
|
|
38
|
-
const cells = [];
|
|
39
|
-
// Trường hợp chọn nhiều ô
|
|
40
|
-
if (selection instanceof CellSelection) {
|
|
41
|
-
selection.forEachCell((node, pos)=>cells.push({
|
|
42
|
-
pos,
|
|
43
|
-
node
|
|
44
|
-
}));
|
|
45
|
-
return cells;
|
|
46
|
-
}
|
|
47
|
-
// Trường hợp con trỏ nằm trong một ô
|
|
48
|
-
if (selection.empty) {
|
|
49
|
-
// Tìm node cell cha gần nhất
|
|
50
|
-
const cell = selection.$head.node(-1);
|
|
51
|
-
if (!cell || !(cell.type.name === 'tableCell' || cell.type.name === 'tableHeader')) return cells;
|
|
52
|
-
// Lấy vị trí bắt đầu của node cell
|
|
53
|
-
const cellPos = selection.$head.start(-1);
|
|
54
|
-
cells.push({
|
|
55
|
-
pos: cellPos,
|
|
56
|
-
node: cell
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return cells;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Quyết định giá trị thuộc tính cuối cùng từ một danh sách các cell đã chọn.
|
|
63
|
-
* @param cells Danh sách các cell được chọn.
|
|
64
|
-
* @param attributeName Tên của thuộc tính cần lấy (ví dụ: 'backgroundColor').
|
|
65
|
-
* @returns Giá trị chung, hoặc MIXED_VALUE nếu các giá trị không đồng nhất.
|
|
66
|
-
*/ export function getCombinedCellAttributeValue(cells, attributeName, fallbackValue) {
|
|
67
|
-
// Không có cell nào được chọn
|
|
68
|
-
if (cells.length === 0) return fallbackValue;
|
|
69
|
-
const firstValue = cells[0].node.attrs[attributeName];
|
|
70
|
-
for(let i = 1; i < cells.length; i++){
|
|
71
|
-
const currentValue = cells[i].node.attrs[attributeName];
|
|
72
|
-
// Dùng isEqual để so sánh object (quan trọng cho thuộc tính 'border')
|
|
73
|
-
if (!isEqual(currentValue, firstValue)) return undefined;
|
|
74
|
-
}
|
|
75
|
-
return firstValue;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Get the actual table DOM element from a node position
|
|
79
|
-
* Handles both direct table elements and NodeView wrappers
|
|
80
|
-
* Uses :scope selector to avoid selecting nested tables
|
|
81
|
-
*
|
|
82
|
-
* @export For use in other table-related extensions
|
|
83
|
-
*/ export function getTableDOMFromView(view, tablePos) {
|
|
84
|
-
const nodeDOMEl = view.nodeDOM(tablePos);
|
|
85
|
-
if (!nodeDOMEl) return null;
|
|
86
|
-
// Case 1: nodeDOMEl is the table itself
|
|
87
|
-
if (nodeDOMEl.tagName === 'TABLE') {
|
|
88
|
-
return nodeDOMEl;
|
|
89
|
-
}
|
|
90
|
-
// Case 2: nodeDOMEl is a NodeView wrapper (figure.widget)
|
|
91
|
-
// Structure: figure.widget > table.widget-content
|
|
92
|
-
// Use :scope to only get direct child, avoiding nested tables in cells
|
|
93
|
-
const tableDOM = nodeDOMEl.querySelector(':scope > table.widget-content');
|
|
94
|
-
return tableDOM;
|
|
95
|
-
}
|
package/src/utils/text.util.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { TextSelection } from '@tiptap/pm/state';
|
|
2
|
-
export function getSelectedText(editor) {
|
|
3
|
-
const { state } = editor;
|
|
4
|
-
const { selection } = state;
|
|
5
|
-
// 1. Kiểm tra nếu selection rỗng (chỉ là con trỏ) hoặc không phải là TextSelection
|
|
6
|
-
if (selection.empty || !(selection instanceof TextSelection)) {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
// 2. Lấy text giữa điểm đầu và điểm cuối của selection
|
|
10
|
-
const { from, to } = selection;
|
|
11
|
-
return state.doc.textBetween(from, to, ' '); // Trả về text
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Tìm instance và range (from, to) của một mark cụ thể tại vị trí con trỏ.
|
|
15
|
-
* @param editor The Tiptap editor instance.
|
|
16
|
-
* @param markName Tên của mark cần tìm (vd: 'link').
|
|
17
|
-
* @returns Object chứa from, to, và mark instance nếu tìm thấy, ngược lại trả về null.
|
|
18
|
-
*/ export function getActiveMarkRange(editor, markName) {
|
|
19
|
-
const { state } = editor;
|
|
20
|
-
const { $from } = state.selection;
|
|
21
|
-
const { doc } = state;
|
|
22
|
-
// 2. Lấy instance của mark tại vị trí con trỏ
|
|
23
|
-
const activeMarks = $from.marks();
|
|
24
|
-
const targetMark = activeMarks.find((mark)=>mark.type.name === markName);
|
|
25
|
-
// Nếu không có mark active, thoát ngay
|
|
26
|
-
if (!targetMark) return null;
|
|
27
|
-
const markType = targetMark.type;
|
|
28
|
-
// 3. Quét ngược về đầu để tìm vị trí `from`
|
|
29
|
-
let from = $from.pos;
|
|
30
|
-
// Giới hạn việc quét trong node cha hiện tại ($from.start())
|
|
31
|
-
while(from > $from.start() && doc.rangeHasMark(from - 1, from, markType)){
|
|
32
|
-
from--;
|
|
33
|
-
}
|
|
34
|
-
// 4. Quét xuôi về cuối để tìm vị trí `to`
|
|
35
|
-
let to = $from.pos;
|
|
36
|
-
// Giới hạn việc quét trong node cha hiện tại ($from.end())
|
|
37
|
-
while(to < $from.end() && doc.rangeHasMark(to, to + 1, markType)){
|
|
38
|
-
to++;
|
|
39
|
-
}
|
|
40
|
-
// 5. Trả về kết quả
|
|
41
|
-
return {
|
|
42
|
-
from,
|
|
43
|
-
to,
|
|
44
|
-
mark: targetMark
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { DOMSerializer } from '@tiptap/pm/model';
|
|
2
|
-
import { NodeSelection, TextSelection } from '@tiptap/pm/state';
|
|
3
|
-
export class HandleNodeView {
|
|
4
|
-
setupDragHandling(view, getPos, node) {
|
|
5
|
-
const handleDragStart = (event)=>{
|
|
6
|
-
const pos = getPos();
|
|
7
|
-
if (pos === undefined || !event.dataTransfer) return;
|
|
8
|
-
// Let ProseMirror know which node is being dragged
|
|
9
|
-
// Store node position and allow move effect
|
|
10
|
-
event.dataTransfer.effectAllowed = 'move';
|
|
11
|
-
// ProseMirror expects this format
|
|
12
|
-
const slice = view.state.doc.slice(pos, pos + node.nodeSize);
|
|
13
|
-
const serializer = DOMSerializer.fromSchema(view.state.schema);
|
|
14
|
-
const dom = document.createElement('div');
|
|
15
|
-
dom.appendChild(serializer.serializeFragment(slice.content));
|
|
16
|
-
event.dataTransfer.setDragImage(this.dom, 0, 0);
|
|
17
|
-
event.dataTransfer.setData('text/html', dom.innerHTML);
|
|
18
|
-
// Add dragging state
|
|
19
|
-
this.dom.classList.add('widget-dragging');
|
|
20
|
-
};
|
|
21
|
-
const handleDragEnd = ()=>{
|
|
22
|
-
this.dom.classList.remove('widget-dragging');
|
|
23
|
-
};
|
|
24
|
-
this.dom.addEventListener('dragstart', handleDragStart);
|
|
25
|
-
this.dom.addEventListener('dragend', handleDragEnd);
|
|
26
|
-
this.eventCleanups.push(()=>{
|
|
27
|
-
this.dom.removeEventListener('dragstart', handleDragStart);
|
|
28
|
-
this.dom.removeEventListener('dragend', handleDragEnd);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
createBlockHandle(view, getPos) {
|
|
32
|
-
const handle = document.createElement('div');
|
|
33
|
-
handle.className = 'widget__selection-handle';
|
|
34
|
-
handle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" fill="currentColor" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z"/></svg>';
|
|
35
|
-
handle.contentEditable = 'false';
|
|
36
|
-
// handle.draggable = true;
|
|
37
|
-
handle.setAttribute('data-drag-handle', '');
|
|
38
|
-
handle.setAttribute('data-block-handle', ''); // Mark để dễ check trong stopEvent
|
|
39
|
-
// Logic click-to-select (only on click, not drag)
|
|
40
|
-
let isDragging = false;
|
|
41
|
-
const handleMouseDown = ()=>{
|
|
42
|
-
isDragging = false;
|
|
43
|
-
// Let drag happen naturally, don't prevent
|
|
44
|
-
};
|
|
45
|
-
const handleDragStart = ()=>{
|
|
46
|
-
isDragging = true;
|
|
47
|
-
};
|
|
48
|
-
const handleClick = (event)=>{
|
|
49
|
-
// Only select if it was a click, not a drag
|
|
50
|
-
if (!isDragging && event.button === 0) {
|
|
51
|
-
event.preventDefault();
|
|
52
|
-
event.stopPropagation();
|
|
53
|
-
const pos = getPos() ?? 0;
|
|
54
|
-
const tr = view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos));
|
|
55
|
-
view.dispatch(tr);
|
|
56
|
-
}
|
|
57
|
-
isDragging = false;
|
|
58
|
-
};
|
|
59
|
-
handle.addEventListener('mousedown', handleMouseDown);
|
|
60
|
-
// handle.addEventListener('dragstart', handleDragStart);
|
|
61
|
-
handle.addEventListener('click', handleClick);
|
|
62
|
-
// Store cleanup
|
|
63
|
-
this.eventCleanups.push(()=>{
|
|
64
|
-
handle.removeEventListener('mousedown', handleMouseDown);
|
|
65
|
-
// handle.removeEventListener('dragstart', handleDragStart);
|
|
66
|
-
handle.removeEventListener('click', handleClick);
|
|
67
|
-
});
|
|
68
|
-
return handle;
|
|
69
|
-
}
|
|
70
|
-
createTypeAround(view, getPos, node) {
|
|
71
|
-
const wrapper = document.createElement('div');
|
|
72
|
-
wrapper.className = 'type-around';
|
|
73
|
-
const icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/></svg>';
|
|
74
|
-
const buttonBefore = document.createElement('div');
|
|
75
|
-
buttonBefore.className = 'widget__type-around__button widget__type-around__button_before';
|
|
76
|
-
buttonBefore.title = 'Insert paragraph before';
|
|
77
|
-
buttonBefore.innerHTML = icon;
|
|
78
|
-
const buttonAfter = document.createElement('div');
|
|
79
|
-
buttonAfter.className = 'widget__type-around__button widget__type-around__button_after';
|
|
80
|
-
buttonAfter.title = 'Insert paragraph after';
|
|
81
|
-
buttonAfter.innerHTML = icon;
|
|
82
|
-
// Logic chèn node 'paragraph' mới
|
|
83
|
-
const handleInsert = (position)=>{
|
|
84
|
-
const currentPos = getPos();
|
|
85
|
-
if (currentPos === undefined) return;
|
|
86
|
-
const newNode = view.state.schema.nodes['paragraph'].create();
|
|
87
|
-
let insertPos;
|
|
88
|
-
if (position === 'before') {
|
|
89
|
-
insertPos = currentPos;
|
|
90
|
-
} else {
|
|
91
|
-
const tableStartPos = view.state.doc.resolve(currentPos);
|
|
92
|
-
const tableIndex = tableStartPos.index();
|
|
93
|
-
insertPos = tableStartPos.posAtIndex(tableIndex + 1);
|
|
94
|
-
}
|
|
95
|
-
const tr = view.state.tr.insert(insertPos, newNode);
|
|
96
|
-
const mappedInsertPos = tr.mapping.map(insertPos, -1);
|
|
97
|
-
const selectionPos = mappedInsertPos + 1;
|
|
98
|
-
// Di chuyển con trỏ vào node mới
|
|
99
|
-
view.dispatch(tr.setSelection(TextSelection.create(tr.doc, selectionPos)));
|
|
100
|
-
view.focus();
|
|
101
|
-
};
|
|
102
|
-
// Button Before event handlers
|
|
103
|
-
const beforeMouseDown = (e)=>{
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
e.stopPropagation();
|
|
106
|
-
};
|
|
107
|
-
const beforeClick = (e)=>{
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
handleInsert('before');
|
|
111
|
-
};
|
|
112
|
-
buttonBefore.addEventListener('mousedown', beforeMouseDown);
|
|
113
|
-
buttonBefore.addEventListener('click', beforeClick);
|
|
114
|
-
// Button After event handlers
|
|
115
|
-
const afterMouseDown = (e)=>{
|
|
116
|
-
e.preventDefault();
|
|
117
|
-
e.stopPropagation();
|
|
118
|
-
};
|
|
119
|
-
const afterClick = (e)=>{
|
|
120
|
-
e.preventDefault();
|
|
121
|
-
e.stopPropagation();
|
|
122
|
-
handleInsert('after');
|
|
123
|
-
};
|
|
124
|
-
buttonAfter.addEventListener('mousedown', afterMouseDown);
|
|
125
|
-
buttonAfter.addEventListener('click', afterClick);
|
|
126
|
-
// Store cleanup functions
|
|
127
|
-
this.eventCleanups.push(()=>{
|
|
128
|
-
buttonBefore.removeEventListener('mousedown', beforeMouseDown);
|
|
129
|
-
buttonBefore.removeEventListener('click', beforeClick);
|
|
130
|
-
buttonAfter.removeEventListener('mousedown', afterMouseDown);
|
|
131
|
-
buttonAfter.removeEventListener('click', afterClick);
|
|
132
|
-
});
|
|
133
|
-
wrapper.append(buttonBefore, buttonAfter);
|
|
134
|
-
return wrapper;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Ngăn các sự kiện từ handle và type-around buttons lan truyền vào editor
|
|
138
|
-
* CHỈ chặn events từ các controls, KHÔNG chặn events từ content
|
|
139
|
-
*
|
|
140
|
-
* QUAN TRỌNG:
|
|
141
|
-
* - return true = ProseMirror BỎ QUA event này
|
|
142
|
-
* - return false = ProseMirror XỬ LÝ event này (table editing, selection, drag, etc.)
|
|
143
|
-
*/ stopEvent(event) {
|
|
144
|
-
const target = event.target;
|
|
145
|
-
// Chỉ chặn events từ control elements (handle, type-around buttons)
|
|
146
|
-
const isControl = target.closest('[data-block-handle]') || target.closest('.widget__type-around__button') || target.closest('.type-around');
|
|
147
|
-
if (!isControl) {
|
|
148
|
-
// Event từ table content → cho ProseMirror xử lý
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
// Event từ control → chặn hầu hết, NHƯNG cho phép một số events quan trọng
|
|
152
|
-
const eventType = event.type;
|
|
153
|
-
// Cho phép drag events để ProseMirror handle drag & drop
|
|
154
|
-
if (eventType === 'dragstart' || eventType === 'drag' || eventType === 'dragend') {
|
|
155
|
-
return false; // Let ProseMirror handle drag
|
|
156
|
-
}
|
|
157
|
-
// Cho phép focus events để không phá navigation
|
|
158
|
-
if (eventType === 'focus' || eventType === 'blur') {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
// Chặn các events khác từ controls
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
selectNode() {
|
|
165
|
-
this.dom.classList.add('ProseMirror-selectednode');
|
|
166
|
-
}
|
|
167
|
-
deselectNode() {
|
|
168
|
-
this.dom.classList.remove('ProseMirror-selectednode');
|
|
169
|
-
}
|
|
170
|
-
destroy() {
|
|
171
|
-
// Cleanup all event listeners
|
|
172
|
-
this.eventCleanups.forEach((cleanup)=>cleanup());
|
|
173
|
-
this.eventCleanups = [];
|
|
174
|
-
// Remove DOM
|
|
175
|
-
this.dom.remove();
|
|
176
|
-
}
|
|
177
|
-
constructor(node, view, getPos){
|
|
178
|
-
// Store event listeners for cleanup
|
|
179
|
-
this.eventCleanups = [];
|
|
180
|
-
this.node = node;
|
|
181
|
-
this.view = view;
|
|
182
|
-
this.getPos = getPos;
|
|
183
|
-
// 1. Dùng <figure> làm wrapper, KHÔNG set contentEditable vì sẽ chặn tất cả
|
|
184
|
-
this.dom = document.createElement('figure');
|
|
185
|
-
this.dom.classList.add('widget');
|
|
186
|
-
// Make the widget draggable (via the handle with data-drag-handle attribute)
|
|
187
|
-
// this.dom.draggable = true;
|
|
188
|
-
// 2. Tạo Handle kéo thả
|
|
189
|
-
const handle = this.createBlockHandle(view, getPos);
|
|
190
|
-
// 3. Tạo Nội dung (do lớp con định nghĩa)
|
|
191
|
-
const contentElement = this.createContentElement(node);
|
|
192
|
-
contentElement.classList.add('widget-content');
|
|
193
|
-
// 4. Tạo các nút "Type Around"
|
|
194
|
-
const typeAround = this.createTypeAround(view, getPos, node);
|
|
195
|
-
// 5. Lắp ráp cấu trúc: Handle -> Nội dung -> TypeAround
|
|
196
|
-
// this.dom.append(handle, contentElement, typeAround);
|
|
197
|
-
this.dom.append(contentElement, typeAround);
|
|
198
|
-
// 6. Setup drag handling for ProseMirror
|
|
199
|
-
// this.setupDragHandling(view, getPos, node);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
export class TableNodeView extends HandleNodeView {
|
|
203
|
-
createContentElement() {
|
|
204
|
-
// Create table element
|
|
205
|
-
const table = document.createElement('table');
|
|
206
|
-
// Apply table styles (border and backgroundColor)
|
|
207
|
-
this.updateTableStyles(table);
|
|
208
|
-
// Create tbody - this is the contentDOM where ProseMirror renders <tr>s
|
|
209
|
-
const tbody = document.createElement('tbody');
|
|
210
|
-
this.contentDOM = tbody;
|
|
211
|
-
table.appendChild(tbody);
|
|
212
|
-
// Render colgroup
|
|
213
|
-
this.updateColgroup(table);
|
|
214
|
-
return table;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Get table element from DOM
|
|
218
|
-
* Query from this.dom instead of storing reference to avoid stale references
|
|
219
|
-
*/ getTableElement() {
|
|
220
|
-
return this.dom.querySelector('table');
|
|
221
|
-
}
|
|
222
|
-
update(node) {
|
|
223
|
-
// Check if node actually changed
|
|
224
|
-
if (node.type !== this.node.type) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
// Update node reference
|
|
228
|
-
this.node = node;
|
|
229
|
-
// Get table element
|
|
230
|
-
const table = this.getTableElement();
|
|
231
|
-
if (!table) {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
// Update table styles if border or backgroundColor changed
|
|
235
|
-
this.updateTableStyles(table);
|
|
236
|
-
// Update colgroup if colwidths changed
|
|
237
|
-
this.updateColgroup(table);
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
updateTableStyles(table) {
|
|
241
|
-
const border = this.node.attrs['border'];
|
|
242
|
-
const backgroundColor = this.node.attrs['backgroundColor'];
|
|
243
|
-
// Reset styles
|
|
244
|
-
// (Chúng ta reset để xử lý trường hợp attribute bị xóa)
|
|
245
|
-
table.style.borderStyle = '';
|
|
246
|
-
table.style.borderColor = '';
|
|
247
|
-
table.style.borderWidth = '';
|
|
248
|
-
table.style.backgroundColor = '';
|
|
249
|
-
// Apply backgroundColor
|
|
250
|
-
if (backgroundColor) {
|
|
251
|
-
table.style.backgroundColor = backgroundColor;
|
|
252
|
-
}
|
|
253
|
-
// Apply border
|
|
254
|
-
if (border) {
|
|
255
|
-
if (border.style) table.style.borderStyle = border.style;
|
|
256
|
-
if (border.color) table.style.borderColor = border.color;
|
|
257
|
-
if (border.width) table.style.borderWidth = border.width;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
updateColgroup(table) {
|
|
261
|
-
// Remove existing colgroup if exists
|
|
262
|
-
const existingColgroup = table.querySelector('colgroup');
|
|
263
|
-
if (existingColgroup) {
|
|
264
|
-
existingColgroup.remove();
|
|
265
|
-
}
|
|
266
|
-
// Render new colgroup if colwidths exist
|
|
267
|
-
const colwidths = this.node.attrs['colwidths'];
|
|
268
|
-
if (colwidths && colwidths.length > 0) {
|
|
269
|
-
const colgroup = document.createElement('colgroup');
|
|
270
|
-
colwidths.forEach((width)=>{
|
|
271
|
-
const col = document.createElement('col');
|
|
272
|
-
if (width) {
|
|
273
|
-
col.style.width = `${width}%`;
|
|
274
|
-
}
|
|
275
|
-
colgroup.appendChild(col);
|
|
276
|
-
});
|
|
277
|
-
// Insert before tbody
|
|
278
|
-
const tbody = table.querySelector('tbody');
|
|
279
|
-
if (tbody) {
|
|
280
|
-
table.insertBefore(colgroup, tbody);
|
|
281
|
-
} else {
|
|
282
|
-
table.appendChild(colgroup);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// Factory function for Tiptap NodeView
|
|
288
|
-
export const createTableNodeView = (props)=>{
|
|
289
|
-
return new TableNodeView(props.node, props.view, props.getPos);
|
|
290
|
-
};
|
|
291
|
-
export class PageBreakNodeView extends HandleNodeView {
|
|
292
|
-
createContentElement() {
|
|
293
|
-
// Create page break container
|
|
294
|
-
const pageBreakDiv = document.createElement('div');
|
|
295
|
-
pageBreakDiv.className = 'document-engine-page-break';
|
|
296
|
-
pageBreakDiv.setAttribute('data-page-break', 'true');
|
|
297
|
-
pageBreakDiv.style.cssText = 'break-before: page;';
|
|
298
|
-
pageBreakDiv.contentEditable = 'false';
|
|
299
|
-
// Create label
|
|
300
|
-
const label = document.createElement('span');
|
|
301
|
-
label.className = 'document-engine-page-break-label';
|
|
302
|
-
label.textContent = 'Page break';
|
|
303
|
-
label.contentEditable = 'false';
|
|
304
|
-
pageBreakDiv.appendChild(label);
|
|
305
|
-
return pageBreakDiv;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Factory function for PageBreak NodeView
|
|
309
|
-
export const createPageBreakNodeView = (props)=>{
|
|
310
|
-
return new PageBreakNodeView(props.node, props.view, props.getPos);
|
|
311
|
-
};
|
package/src/views/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './block-handler';
|