@phuong-tran-redoc/document-engine-core 0.0.12
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/LICENSE.md +19 -0
- package/README.md +224 -0
- package/package.json +30 -0
- package/src/constants/indent.constant.d.ts +1 -0
- package/src/constants/indent.constant.js +11 -0
- package/src/constants/index.d.ts +1 -0
- package/src/constants/index.js +18 -0
- package/src/constants/table.constant.d.ts +1 -0
- package/src/constants/table.constant.js +11 -0
- package/src/extensions/clear-content.extension.d.ts +9 -0
- package/src/extensions/clear-content.extension.js +19 -0
- package/src/extensions/indent.extension.d.ts +16 -0
- package/src/extensions/indent.extension.js +69 -0
- package/src/extensions/index.d.ts +8 -0
- package/src/extensions/index.js +25 -0
- package/src/extensions/ordered-list.extension.d.ts +13 -0
- package/src/extensions/ordered-list.extension.js +48 -0
- package/src/extensions/reset-format.extension.d.ts +9 -0
- package/src/extensions/reset-format.extension.js +19 -0
- package/src/extensions/reset-on-enter.extension.d.ts +2 -0
- package/src/extensions/reset-on-enter.extension.js +42 -0
- package/src/extensions/restricted-editing.extension.d.ts +17 -0
- package/src/extensions/restricted-editing.extension.js +384 -0
- package/src/extensions/table-resizing.extension.d.ts +6 -0
- package/src/extensions/table-resizing.extension.js +171 -0
- package/src/extensions/table-style.extension.d.ts +41 -0
- package/src/extensions/table-style.extension.js +499 -0
- package/src/extensions/text-case.extension.d.ts +13 -0
- package/src/extensions/text-case.extension.js +49 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +24 -0
- package/src/models/color.model.d.ts +14 -0
- package/src/models/color.model.js +40 -0
- package/src/models/index.d.ts +1 -0
- package/src/models/index.js +18 -0
- package/src/nodes/dynamic-field.node.d.ts +49 -0
- package/src/nodes/dynamic-field.node.js +99 -0
- package/src/nodes/heading.node.d.ts +8 -0
- package/src/nodes/heading.node.js +23 -0
- package/src/nodes/index.d.ts +3 -0
- package/src/nodes/index.js +20 -0
- package/src/nodes/page-break.node.d.ts +9 -0
- package/src/nodes/page-break.node.js +50 -0
- package/src/types/index.d.ts +2 -0
- package/src/types/index.js +19 -0
- package/src/types/ordered-list.type.d.ts +1 -0
- package/src/types/ordered-list.type.js +4 -0
- package/src/types/text-case.type.d.ts +1 -0
- package/src/types/text-case.type.js +4 -0
- package/src/utils/color.util.d.ts +14 -0
- package/src/utils/color.util.js +41 -0
- package/src/utils/dom.util.d.ts +2 -0
- package/src/utils/dom.util.js +43 -0
- package/src/utils/index.d.ts +4 -0
- package/src/utils/index.js +21 -0
- package/src/utils/table.util.d.ts +36 -0
- package/src/utils/table.util.js +103 -0
- package/src/utils/text.util.d.ts +9 -0
- package/src/utils/text.util.js +59 -0
- package/src/views/block-handler.d.ts +44 -0
- package/src/views/block-handler.js +310 -0
- package/src/views/index.d.ts +1 -0
- package/src/views/index.js +18 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
/**
|
|
3
|
+
* Interface for Dynamic Field node attributes
|
|
4
|
+
*/
|
|
5
|
+
export interface DynamicFieldAttributes {
|
|
6
|
+
fieldId: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Interface for Dynamic Field item
|
|
11
|
+
*/
|
|
12
|
+
export interface DynamicFieldItem {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Interface for Dynamic Field category
|
|
19
|
+
*/
|
|
20
|
+
export interface DynamicFieldCategory {
|
|
21
|
+
key: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
fields: DynamicFieldItem[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Interface for Dynamic Field options
|
|
28
|
+
*/
|
|
29
|
+
export interface DynamicFieldOptions {
|
|
30
|
+
HTMLAttributes: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* TypeScript declaration for the insertDynamicField command
|
|
34
|
+
*/
|
|
35
|
+
declare module '@tiptap/core' {
|
|
36
|
+
interface Commands<ReturnType> {
|
|
37
|
+
dynamicField: {
|
|
38
|
+
insertDynamicField: (attributes: DynamicFieldAttributes) => ReturnType;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Dynamic Field Node Extension
|
|
44
|
+
*
|
|
45
|
+
* This extension creates atomic, non-editable inline nodes that represent
|
|
46
|
+
* dynamic fields (e.g., {{customer_name}}) in document templates.
|
|
47
|
+
* These fields can be programmatically replaced with actual data later.
|
|
48
|
+
*/
|
|
49
|
+
export declare const DynamicField: Node<DynamicFieldOptions, any>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "DynamicField", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return DynamicField;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _core = require("@tiptap/core");
|
|
12
|
+
const DynamicField = _core.Node.create({
|
|
13
|
+
name: 'dynamicField',
|
|
14
|
+
/**
|
|
15
|
+
* Node group configuration
|
|
16
|
+
* - 'inline': Makes the node behave as inline content
|
|
17
|
+
* - 'atom': true is critical for non-editable, atomic behavior
|
|
18
|
+
* - 'selectable': true allows users to select the entire node
|
|
19
|
+
* - 'draggable': false prevents users from dragging the node
|
|
20
|
+
*/ group: 'inline',
|
|
21
|
+
inline: true,
|
|
22
|
+
atom: true,
|
|
23
|
+
selectable: true,
|
|
24
|
+
draggable: false,
|
|
25
|
+
/**
|
|
26
|
+
* Define the attributes that this node can store
|
|
27
|
+
*/ addAttributes () {
|
|
28
|
+
return {
|
|
29
|
+
fieldId: {
|
|
30
|
+
default: null,
|
|
31
|
+
parseHTML: (element)=>element.getAttribute('data-field-id'),
|
|
32
|
+
renderHTML: (attributes)=>{
|
|
33
|
+
if (!attributes['fieldId']) return {};
|
|
34
|
+
return {
|
|
35
|
+
'data-field-id': attributes['fieldId']
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
label: {
|
|
40
|
+
default: null,
|
|
41
|
+
parseHTML: (element)=>element.getAttribute('data-label'),
|
|
42
|
+
renderHTML: (attributes)=>{
|
|
43
|
+
if (!attributes['label']) return {};
|
|
44
|
+
return {
|
|
45
|
+
'data-label': attributes['label']
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Parse HTML to recognize dynamic field spans when loading/pasting content
|
|
53
|
+
*/ parseHTML () {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
tag: 'span[data-field-id]'
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Render the node as HTML
|
|
62
|
+
* Creates a span element with Tailwind classes for styling
|
|
63
|
+
* and stores attributes in data-* attributes for robustness
|
|
64
|
+
*/ renderHTML ({ HTMLAttributes, node }) {
|
|
65
|
+
const { fieldId, label } = node.attrs;
|
|
66
|
+
return [
|
|
67
|
+
'span',
|
|
68
|
+
(0, _core.mergeAttributes)(HTMLAttributes, {
|
|
69
|
+
class: 'dynamic-field',
|
|
70
|
+
'data-field-id': fieldId,
|
|
71
|
+
'data-label': label
|
|
72
|
+
}),
|
|
73
|
+
label || ''
|
|
74
|
+
];
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* Add custom commands for inserting dynamic fields
|
|
78
|
+
*/ addCommands () {
|
|
79
|
+
return {
|
|
80
|
+
insertDynamicField: (attributes)=>({ chain })=>{
|
|
81
|
+
if (!attributes.fieldId || !attributes.label) {
|
|
82
|
+
console.error('[DynamicField] Both fieldId and label are required.');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return chain().insertContent({
|
|
86
|
+
type: this.name,
|
|
87
|
+
attrs: attributes
|
|
88
|
+
}).run();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Default options
|
|
94
|
+
*/ addOptions () {
|
|
95
|
+
return {
|
|
96
|
+
HTMLAttributes: {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "NotumHeading", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return NotumHeading;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _extensionheading = require("@tiptap/extension-heading");
|
|
12
|
+
require("@tiptap/extension-paragraph");
|
|
13
|
+
const NotumHeading = _extensionheading.Heading.extend({
|
|
14
|
+
addCommands () {
|
|
15
|
+
return {
|
|
16
|
+
...this.parent?.(),
|
|
17
|
+
removeHeading: ()=>({ editor, chain })=>{
|
|
18
|
+
if (!editor.isActive('heading')) return false;
|
|
19
|
+
return chain().setParagraph().run();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./heading.node"), exports);
|
|
6
|
+
_export_star(require("./dynamic-field.node"), exports);
|
|
7
|
+
_export_star(require("./page-break.node"), exports);
|
|
8
|
+
function _export_star(from, to) {
|
|
9
|
+
Object.keys(from).forEach(function(k) {
|
|
10
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
11
|
+
Object.defineProperty(to, k, {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function() {
|
|
14
|
+
return from[k];
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return from;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "PageBreak", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return PageBreak;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _core = require("@tiptap/core");
|
|
12
|
+
const PageBreak = _core.Node.create({
|
|
13
|
+
name: 'pageBreak',
|
|
14
|
+
group: 'block',
|
|
15
|
+
atom: true,
|
|
16
|
+
selectable: true,
|
|
17
|
+
parseHTML () {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
tag: 'div[data-page-break]'
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
},
|
|
24
|
+
renderHTML ({ HTMLAttributes }) {
|
|
25
|
+
return [
|
|
26
|
+
'div',
|
|
27
|
+
(0, _core.mergeAttributes)(HTMLAttributes, {
|
|
28
|
+
'data-page-break': 'true',
|
|
29
|
+
class: 'document-engine-page-break',
|
|
30
|
+
style: 'break-before: page;'
|
|
31
|
+
}),
|
|
32
|
+
[
|
|
33
|
+
'span',
|
|
34
|
+
{
|
|
35
|
+
class: 'document-engine-page-break-label'
|
|
36
|
+
},
|
|
37
|
+
'Page break'
|
|
38
|
+
]
|
|
39
|
+
];
|
|
40
|
+
},
|
|
41
|
+
addCommands () {
|
|
42
|
+
return {
|
|
43
|
+
insertPageBreak: ()=>({ chain })=>{
|
|
44
|
+
return chain().insertContent({
|
|
45
|
+
type: this.name
|
|
46
|
+
}).run();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./ordered-list.type"), exports);
|
|
6
|
+
_export_star(require("./text-case.type"), exports);
|
|
7
|
+
function _export_star(from, to) {
|
|
8
|
+
Object.keys(from).forEach(function(k) {
|
|
9
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
10
|
+
Object.defineProperty(to, k, {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function() {
|
|
13
|
+
return from[k];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return from;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ListStyleType = 'decimal' | 'lower-alpha-dot' | 'lower-alpha-parens' | 'lower-roman-parens';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type TextCaseType = 'uppercase' | 'lowercase' | 'capitalize';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a color value to lowercase hex format
|
|
3
|
+
* Supports hex (#RGB, #RRGGBB) and rgb(r, g, b) formats
|
|
4
|
+
*
|
|
5
|
+
* @param color - Color value to normalize
|
|
6
|
+
* @returns Normalized hex color or null if invalid
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* normalizeColor('#FF0000') // '#ff0000'
|
|
10
|
+
* normalizeColor('rgb(255, 0, 0)') // '#ff0000'
|
|
11
|
+
* normalizeColor('#F00') // '#f00'
|
|
12
|
+
* normalizeColor('invalid') // null
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeColor(color: string): string | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a color value to lowercase hex format
|
|
3
|
+
* Supports hex (#RGB, #RRGGBB) and rgb(r, g, b) formats
|
|
4
|
+
*
|
|
5
|
+
* @param color - Color value to normalize
|
|
6
|
+
* @returns Normalized hex color or null if invalid
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* normalizeColor('#FF0000') // '#ff0000'
|
|
10
|
+
* normalizeColor('rgb(255, 0, 0)') // '#ff0000'
|
|
11
|
+
* normalizeColor('#F00') // '#f00'
|
|
12
|
+
* normalizeColor('invalid') // null
|
|
13
|
+
*/ "use strict";
|
|
14
|
+
Object.defineProperty(exports, "__esModule", {
|
|
15
|
+
value: true
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(exports, "normalizeColor", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function() {
|
|
20
|
+
return normalizeColor;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
function normalizeColor(color) {
|
|
24
|
+
if (!color) return null;
|
|
25
|
+
const trimmed = color.trim().toLowerCase();
|
|
26
|
+
if (trimmed === 'transparent') return trimmed;
|
|
27
|
+
// Check if it's a valid hex color
|
|
28
|
+
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) {
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
// Try to parse RGB
|
|
32
|
+
const rgbMatch = trimmed.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
|
|
33
|
+
if (rgbMatch) {
|
|
34
|
+
const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1], 10)));
|
|
35
|
+
const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2], 10)));
|
|
36
|
+
const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3], 10)));
|
|
37
|
+
const toHex = (n)=>n.toString(16).padStart(2, '0');
|
|
38
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "getClosestDomElement", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return getClosestDomElement;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
function getClosestDomElement(editor, selector) {
|
|
12
|
+
const { view, state } = editor;
|
|
13
|
+
const { selection } = state;
|
|
14
|
+
const { from } = selection;
|
|
15
|
+
if (!from) return null;
|
|
16
|
+
let domNodeAtPos = null;
|
|
17
|
+
// Ưu tiên tìm DOM node của 'nearest parent element' của vị trí from
|
|
18
|
+
// Nếu from là trong text, $from.parent.nodeDOM có thể chính xác hơn
|
|
19
|
+
try {
|
|
20
|
+
const resolvedPos = state.doc.resolve(from);
|
|
21
|
+
domNodeAtPos = view.domAtPos(resolvedPos.pos).node; // Bắt đầu từ node ở vị trí from
|
|
22
|
+
// Nếu domNodeAtPos không phải là HTMLElement (ví dụ, là một Text node),
|
|
23
|
+
// thì cố gắng lấy element cha của nó trong DOM
|
|
24
|
+
if (domNodeAtPos && !(domNodeAtPos instanceof HTMLElement)) {
|
|
25
|
+
domNodeAtPos = domNodeAtPos.parentElement;
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Xử lý trường hợp hiếm gặp khi nodeDOM(from) không trả về gì hợp lệ
|
|
29
|
+
console.warn('Could not find DOM node at cursor position:', e);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!domNodeAtPos) return null;
|
|
33
|
+
const boundary = editor.view.dom;
|
|
34
|
+
// Tìm kiếm từ domNodeAtPos lên trên cho đến khi gặp editor container
|
|
35
|
+
let currentElement = domNodeAtPos;
|
|
36
|
+
while(currentElement && currentElement !== boundary){
|
|
37
|
+
if (currentElement.matches(selector)) {
|
|
38
|
+
return currentElement;
|
|
39
|
+
}
|
|
40
|
+
currentElement = currentElement.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./color.util"), exports);
|
|
6
|
+
_export_star(require("./dom.util"), exports);
|
|
7
|
+
_export_star(require("./table.util"), exports);
|
|
8
|
+
_export_star(require("./text.util"), exports);
|
|
9
|
+
function _export_star(from, to) {
|
|
10
|
+
Object.keys(from).forEach(function(k) {
|
|
11
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
12
|
+
Object.defineProperty(to, k, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function() {
|
|
15
|
+
return from[k];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return from;
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Node as PMNode } from '@tiptap/pm/model';
|
|
2
|
+
import { EditorState } from '@tiptap/pm/state';
|
|
3
|
+
import { TableMap } from '@tiptap/pm/tables';
|
|
4
|
+
import { EditorView } from '@tiptap/pm/view';
|
|
5
|
+
export declare function getCursorCellInfo(state: EditorState): {
|
|
6
|
+
rowIndex: number;
|
|
7
|
+
colIndex: number;
|
|
8
|
+
cellIndex: number;
|
|
9
|
+
tableStart: number;
|
|
10
|
+
tableNode: PMNode;
|
|
11
|
+
map: TableMap;
|
|
12
|
+
} | null;
|
|
13
|
+
/**
|
|
14
|
+
* Lấy tất cả các node cell (tableCell, tableHeader) trong vùng chọn hiện tại.
|
|
15
|
+
*/
|
|
16
|
+
interface SelectedCell {
|
|
17
|
+
pos: number;
|
|
18
|
+
node: PMNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function getSelectedCells(state: EditorState): SelectedCell[];
|
|
21
|
+
/**
|
|
22
|
+
* Quyết định giá trị thuộc tính cuối cùng từ một danh sách các cell đã chọn.
|
|
23
|
+
* @param cells Danh sách các cell được chọn.
|
|
24
|
+
* @param attributeName Tên của thuộc tính cần lấy (ví dụ: 'backgroundColor').
|
|
25
|
+
* @returns Giá trị chung, hoặc MIXED_VALUE nếu các giá trị không đồng nhất.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getCombinedCellAttributeValue(cells: SelectedCell[], attributeName: string, fallbackValue?: unknown): unknown;
|
|
28
|
+
/**
|
|
29
|
+
* Get the actual table DOM element from a node position
|
|
30
|
+
* Handles both direct table elements and NodeView wrappers
|
|
31
|
+
* Uses :scope selector to avoid selecting nested tables
|
|
32
|
+
*
|
|
33
|
+
* @export For use in other table-related extensions
|
|
34
|
+
*/
|
|
35
|
+
export declare function getTableDOMFromView(view: EditorView, tablePos: number): HTMLTableElement | null;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
getCombinedCellAttributeValue: function() {
|
|
13
|
+
return getCombinedCellAttributeValue;
|
|
14
|
+
},
|
|
15
|
+
getCursorCellInfo: function() {
|
|
16
|
+
return getCursorCellInfo;
|
|
17
|
+
},
|
|
18
|
+
getSelectedCells: function() {
|
|
19
|
+
return getSelectedCells;
|
|
20
|
+
},
|
|
21
|
+
getTableDOMFromView: function() {
|
|
22
|
+
return getTableDOMFromView;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _tables = require("@tiptap/pm/tables");
|
|
26
|
+
const _lodashes = require("lodash-es");
|
|
27
|
+
function getCursorCellInfo(state) {
|
|
28
|
+
// 1. Dùng selectionCell để tìm vị trí bắt đầu của ô chứa con trỏ
|
|
29
|
+
const cellInfo = (0, _tables.selectionCell)(state);
|
|
30
|
+
// Nếu không tìm thấy (con trỏ không nằm trong ô nào), trả về null
|
|
31
|
+
if (!cellInfo) return null;
|
|
32
|
+
// cellInfo.$pos là một ResolvedPos trỏ tới vị trí bắt đầu của ô
|
|
33
|
+
const table = (0, _tables.findTable)(cellInfo);
|
|
34
|
+
if (!table) return null;
|
|
35
|
+
const map = _tables.TableMap.get(table.node);
|
|
36
|
+
// 2. Lấy vị trí tương đối của ô so với bảng
|
|
37
|
+
const cellOffset = cellInfo.pos - table.start;
|
|
38
|
+
// 3. Tìm chỉ số (index) của ô này trong "bản đồ" của bảng
|
|
39
|
+
const cellIndex = map.map.indexOf(cellOffset);
|
|
40
|
+
if (cellIndex === -1) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// 4. Từ chỉ số, tính ra rowIndex và colIndex
|
|
44
|
+
const rowIndex = Math.floor(cellIndex / map.width);
|
|
45
|
+
const colIndex = cellIndex % map.width;
|
|
46
|
+
return {
|
|
47
|
+
rowIndex,
|
|
48
|
+
colIndex,
|
|
49
|
+
cellIndex,
|
|
50
|
+
tableStart: table.start,
|
|
51
|
+
tableNode: table.node,
|
|
52
|
+
map
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function getSelectedCells(state) {
|
|
56
|
+
const { selection } = state;
|
|
57
|
+
const cells = [];
|
|
58
|
+
// Trường hợp chọn nhiều ô
|
|
59
|
+
if (selection instanceof _tables.CellSelection) {
|
|
60
|
+
selection.forEachCell((node, pos)=>cells.push({
|
|
61
|
+
pos,
|
|
62
|
+
node
|
|
63
|
+
}));
|
|
64
|
+
return cells;
|
|
65
|
+
}
|
|
66
|
+
// Trường hợp con trỏ nằm trong một ô
|
|
67
|
+
if (selection.empty) {
|
|
68
|
+
// Tìm node cell cha gần nhất
|
|
69
|
+
const cell = selection.$head.node(-1);
|
|
70
|
+
if (!cell || !(cell.type.name === 'tableCell' || cell.type.name === 'tableHeader')) return cells;
|
|
71
|
+
// Lấy vị trí bắt đầu của node cell
|
|
72
|
+
const cellPos = selection.$head.start(-1);
|
|
73
|
+
cells.push({
|
|
74
|
+
pos: cellPos,
|
|
75
|
+
node: cell
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return cells;
|
|
79
|
+
}
|
|
80
|
+
function getCombinedCellAttributeValue(cells, attributeName, fallbackValue) {
|
|
81
|
+
// Không có cell nào được chọn
|
|
82
|
+
if (cells.length === 0) return fallbackValue;
|
|
83
|
+
const firstValue = cells[0].node.attrs[attributeName];
|
|
84
|
+
for(let i = 1; i < cells.length; i++){
|
|
85
|
+
const currentValue = cells[i].node.attrs[attributeName];
|
|
86
|
+
// Dùng isEqual để so sánh object (quan trọng cho thuộc tính 'border')
|
|
87
|
+
if (!(0, _lodashes.isEqual)(currentValue, firstValue)) return undefined;
|
|
88
|
+
}
|
|
89
|
+
return firstValue;
|
|
90
|
+
}
|
|
91
|
+
function getTableDOMFromView(view, tablePos) {
|
|
92
|
+
const nodeDOMEl = view.nodeDOM(tablePos);
|
|
93
|
+
if (!nodeDOMEl) return null;
|
|
94
|
+
// Case 1: nodeDOMEl is the table itself
|
|
95
|
+
if (nodeDOMEl.tagName === 'TABLE') {
|
|
96
|
+
return nodeDOMEl;
|
|
97
|
+
}
|
|
98
|
+
// Case 2: nodeDOMEl is a NodeView wrapper (figure.widget)
|
|
99
|
+
// Structure: figure.widget > table.widget-content
|
|
100
|
+
// Use :scope to only get direct child, avoiding nested tables in cells
|
|
101
|
+
const tableDOM = nodeDOMEl.querySelector(':scope > table.widget-content');
|
|
102
|
+
return tableDOM;
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Editor, MarkRange } from '@tiptap/core';
|
|
2
|
+
export declare function getSelectedText(editor: Editor): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Tìm instance và range (from, to) của một mark cụ thể tại vị trí con trỏ.
|
|
5
|
+
* @param editor The Tiptap editor instance.
|
|
6
|
+
* @param markName Tên của mark cần tìm (vd: 'link').
|
|
7
|
+
* @returns Object chứa from, to, và mark instance nếu tìm thấy, ngược lại trả về null.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getActiveMarkRange(editor: Editor, markName: string): MarkRange | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
getActiveMarkRange: function() {
|
|
13
|
+
return getActiveMarkRange;
|
|
14
|
+
},
|
|
15
|
+
getSelectedText: function() {
|
|
16
|
+
return getSelectedText;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const _state = require("@tiptap/pm/state");
|
|
20
|
+
function getSelectedText(editor) {
|
|
21
|
+
const { state } = editor;
|
|
22
|
+
const { selection } = state;
|
|
23
|
+
// 1. Kiểm tra nếu selection rỗng (chỉ là con trỏ) hoặc không phải là TextSelection
|
|
24
|
+
if (selection.empty || !(selection instanceof _state.TextSelection)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
// 2. Lấy text giữa điểm đầu và điểm cuối của selection
|
|
28
|
+
const { from, to } = selection;
|
|
29
|
+
return state.doc.textBetween(from, to, ' '); // Trả về text
|
|
30
|
+
}
|
|
31
|
+
function getActiveMarkRange(editor, markName) {
|
|
32
|
+
const { state } = editor;
|
|
33
|
+
const { $from } = state.selection;
|
|
34
|
+
const { doc } = state;
|
|
35
|
+
// 2. Lấy instance của mark tại vị trí con trỏ
|
|
36
|
+
const activeMarks = $from.marks();
|
|
37
|
+
const targetMark = activeMarks.find((mark)=>mark.type.name === markName);
|
|
38
|
+
// Nếu không có mark active, thoát ngay
|
|
39
|
+
if (!targetMark) return null;
|
|
40
|
+
const markType = targetMark.type;
|
|
41
|
+
// 3. Quét ngược về đầu để tìm vị trí `from`
|
|
42
|
+
let from = $from.pos;
|
|
43
|
+
// Giới hạn việc quét trong node cha hiện tại ($from.start())
|
|
44
|
+
while(from > $from.start() && doc.rangeHasMark(from - 1, from, markType)){
|
|
45
|
+
from--;
|
|
46
|
+
}
|
|
47
|
+
// 4. Quét xuôi về cuối để tìm vị trí `to`
|
|
48
|
+
let to = $from.pos;
|
|
49
|
+
// Giới hạn việc quét trong node cha hiện tại ($from.end())
|
|
50
|
+
while(to < $from.end() && doc.rangeHasMark(to, to + 1, markType)){
|
|
51
|
+
to++;
|
|
52
|
+
}
|
|
53
|
+
// 5. Trả về kết quả
|
|
54
|
+
return {
|
|
55
|
+
from,
|
|
56
|
+
to,
|
|
57
|
+
mark: targetMark
|
|
58
|
+
};
|
|
59
|
+
}
|