@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.
Files changed (42) hide show
  1. package/index.d.ts +1 -0
  2. package/index.esm.js +2238 -0
  3. package/package.json +15 -6
  4. package/src/constants/indent.constant.js +0 -1
  5. package/src/constants/index.js +0 -1
  6. package/src/constants/table.constant.js +0 -1
  7. package/src/extensions/clear-content.extension.js +0 -9
  8. package/src/extensions/indent.extension.js +0 -59
  9. package/src/extensions/index.js +0 -8
  10. package/src/extensions/ordered-list.extension.js +0 -47
  11. package/src/extensions/reset-format.extension.js +0 -9
  12. package/src/extensions/reset-on-enter.extension.js +0 -37
  13. package/src/extensions/restricted-editing.extension.js +0 -376
  14. package/src/extensions/table-resizing.extension.js +0 -164
  15. package/src/extensions/table-style.extension.js +0 -481
  16. package/src/extensions/text-case.extension.js +0 -39
  17. package/src/index.js +0 -9
  18. package/src/kit/default-extensions.js +0 -86
  19. package/src/kit/generate-html.js +0 -40
  20. package/src/kit/index.js +0 -2
  21. package/src/migrations/doc-migrations.js +0 -24
  22. package/src/migrations/index.js +0 -3
  23. package/src/migrations/migrate-doc.js +0 -32
  24. package/src/migrations/types.js +0 -4
  25. package/src/models/color.model.js +0 -30
  26. package/src/models/index.js +0 -1
  27. package/src/nodes/dynamic-field.node.js +0 -141
  28. package/src/nodes/heading.node.js +0 -13
  29. package/src/nodes/image-ref.node.js +0 -138
  30. package/src/nodes/index.js +0 -4
  31. package/src/nodes/page-break.node.js +0 -52
  32. package/src/types/index.js +0 -2
  33. package/src/types/ordered-list.type.js +0 -2
  34. package/src/types/text-case.type.js +0 -1
  35. package/src/types/tiptap-html-server.d.js +0 -10
  36. package/src/utils/color.util.js +0 -31
  37. package/src/utils/dom.util.js +0 -33
  38. package/src/utils/index.js +0 -4
  39. package/src/utils/table.util.js +0 -95
  40. package/src/utils/text.util.js +0 -46
  41. package/src/views/block-handler.js +0 -311
  42. package/src/views/index.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phuong-tran-redoc/document-engine-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Realestatedoc (Redoc)",
@@ -8,10 +8,20 @@
8
8
  "Duc Phuong (Jack) <tdp99.business@gmail.com>"
9
9
  ],
10
10
  "description": "Framework-agnostic core library for Document Engine",
11
- "main": "./src/index.js",
12
- "types": "./src/index.d.ts",
11
+ "main": "./index.esm.js",
12
+ "module": "./index.esm.js",
13
+ "types": "./index.d.ts",
14
+ "sideEffects": false,
15
+ "exports": {
16
+ "./package.json": "./package.json",
17
+ ".": {
18
+ "types": "./index.d.ts",
19
+ "default": "./index.esm.js"
20
+ }
21
+ },
13
22
  "files": [
14
- "src/**/*.js",
23
+ "index.esm.js",
24
+ "index.d.ts",
15
25
  "src/**/*.d.ts",
16
26
  "README.md",
17
27
  "LICENSE.md",
@@ -57,6 +67,5 @@
57
67
  },
58
68
  "peerDependencies": {
59
69
  "lodash-es": "^4.17.10"
60
- },
61
- "module": "./src/index.js"
70
+ }
62
71
  }
@@ -1 +0,0 @@
1
- export const INDENT_DEFAULT = 40; // in pixels
@@ -1 +0,0 @@
1
- export * from './indent.constant';
@@ -1 +0,0 @@
1
- export const MIN_NEW_COL_WIDTH = 5.0;
@@ -1,9 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- export const ClearContent = Extension.create({
3
- name: 'clearContent',
4
- addCommands () {
5
- return {
6
- clear: ()=>({ chain })=>chain().focus().clearContent().run()
7
- };
8
- }
9
- });
@@ -1,59 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- import { INDENT_DEFAULT } from '../constants';
3
- export const Indent = Extension.create({
4
- name: 'indent',
5
- addOptions () {
6
- return {
7
- types: [
8
- 'paragraph',
9
- 'heading'
10
- ],
11
- indent: INDENT_DEFAULT
12
- };
13
- },
14
- addGlobalAttributes () {
15
- return [
16
- {
17
- types: this.options.types,
18
- attributes: {
19
- indent: {
20
- parseHTML: (element)=>{
21
- return Number(element.style.marginLeft.replace('px', ''));
22
- },
23
- renderHTML: (attributes)=>{
24
- return {
25
- style: `margin-left: ${attributes['indent']}px`
26
- };
27
- }
28
- }
29
- }
30
- }
31
- ];
32
- },
33
- addCommands () {
34
- return {
35
- increaseIndent: ()=>({ state, chain })=>{
36
- const currentNode = state.selection.$head.parent;
37
- const nodeType = currentNode.type;
38
- if (!this.options.types.includes(nodeType.name)) return false;
39
- const currentIndent = currentNode.attrs['indent'] || 0;
40
- const newIndent = currentIndent + this.options.indent;
41
- return chain().updateAttributes(nodeType, {
42
- indent: newIndent
43
- }).run();
44
- },
45
- decreaseIndent: ()=>({ state, chain })=>{
46
- const currentNode = state.selection.$head.parent;
47
- const nodeType = currentNode.type;
48
- if (!this.options.types.includes(nodeType.name)) return false;
49
- const currentIndent = currentNode.attrs['indent'] || 0;
50
- const newIndent = currentIndent - this.options.indent;
51
- if (newIndent < 0) return false;
52
- if (newIndent === 0) return chain().resetAttributes(nodeType, 'indent').run();
53
- return chain().updateAttributes(nodeType, {
54
- indent: newIndent
55
- }).run();
56
- }
57
- };
58
- }
59
- });
@@ -1,8 +0,0 @@
1
- export * from './clear-content.extension';
2
- export * from './indent.extension';
3
- export * from './ordered-list.extension';
4
- export * from './reset-format.extension';
5
- export * from './reset-on-enter.extension';
6
- export * from './restricted-editing.extension';
7
- export * from './table-style.extension';
8
- export * from './text-case.extension';
@@ -1,47 +0,0 @@
1
- import { OrderedList } from '@tiptap/extension-list';
2
- export const CustomOrderedList = OrderedList.extend({
3
- name: 'customOrderedList',
4
- addOptions () {
5
- return {
6
- ...this.parent?.(),
7
- listStyleType: 'decimal'
8
- };
9
- },
10
- addAttributes () {
11
- return {
12
- ...this.parent?.(),
13
- 'data-list-style-type': {
14
- default: this.options.listStyleType,
15
- parseHTML: (element)=>{
16
- // Priority 1: Parse from data-list-style-type attribute (our format)
17
- const dataAttr = element.getAttribute('data-list-style-type');
18
- if (dataAttr) return dataAttr;
19
- // Priority 2: Parse from CSS list-style-type property (other editors' format)
20
- const styleAttr = element.style.listStyleType;
21
- if (styleAttr) return styleAttr;
22
- // Default fallback
23
- return this.options.listStyleType;
24
- },
25
- renderHTML: (attributes)=>{
26
- const styleType = attributes['data-list-style-type'];
27
- if (!styleType || styleType === 'decimal') return {
28
- 'data-list-style-type': 'decimal'
29
- };
30
- return {
31
- 'data-list-style-type': attributes['data-list-style-type']
32
- };
33
- }
34
- }
35
- };
36
- },
37
- addCommands () {
38
- return {
39
- ...this.parent?.(),
40
- setListStyle: (listStyleType)=>({ chain })=>{
41
- return chain().focus().updateAttributes('customOrderedList', {
42
- 'data-list-style-type': listStyleType
43
- }).run();
44
- }
45
- };
46
- }
47
- });
@@ -1,9 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- export const ResetFormat = Extension.create({
3
- name: 'resetFormat',
4
- addCommands () {
5
- return {
6
- resetFormat: ()=>({ chain })=>chain().focus().unsetAllMarks().clearNodes().run()
7
- };
8
- }
9
- });
@@ -1,37 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- /*
3
- * Clear:
4
- * - textStyle mark
5
- * - indent attribute
6
- */ // Clears formats on Enter so the new line has default formats
7
- export const ResetOnEnter = Extension.create({
8
- name: 'resetOnEnter',
9
- addKeyboardShortcuts () {
10
- return {
11
- Enter: ()=>{
12
- const disabledNodes = [
13
- 'listItem',
14
- 'blockquote'
15
- ];
16
- if (disabledNodes.some((type)=>this.editor.isActive(type))) {
17
- return false; //Let the default Enter behavior proceed
18
- }
19
- // Handle Enter ourselves so we can both clear marks and reset indent on the new line
20
- return this.editor.chain().focus().unsetAllMarks().splitBlock({
21
- keepMarks: false
22
- }).command(({ state, commands })=>{
23
- const parent = state.selection.$head.parent;
24
- const nodeType = parent.type;
25
- const typesToReset = [
26
- 'paragraph',
27
- 'heading'
28
- ];
29
- if (typesToReset.includes(nodeType.name)) {
30
- return commands.resetAttributes(nodeType, 'indent');
31
- }
32
- return true;
33
- }).run();
34
- }
35
- };
36
- }
37
- });
@@ -1,376 +0,0 @@
1
- import { Extension, Node, mergeAttributes } from '@tiptap/core';
2
- import { Plugin, PluginKey } from '@tiptap/pm/state';
3
- // Editable inline region where typing is allowed
4
- export const EditableRegion = Node.create({
5
- name: 'editableRegion',
6
- group: 'inline',
7
- inline: true,
8
- content: 'text*',
9
- selectable: false,
10
- parseHTML () {
11
- return [
12
- // ----------------------------------------------------------
13
- // RULE 1: Standard Tiptap Format (highest priority)
14
- // ----------------------------------------------------------
15
- {
16
- tag: 'span[data-editable-region]'
17
- },
18
- // ----------------------------------------------------------
19
- // RULE 2: CKEditor Format (lowest priority)
20
- // ----------------------------------------------------------
21
- {
22
- tag: 'span.restricted-editing-exception'
23
- }
24
- ];
25
- },
26
- renderHTML ({ HTMLAttributes }) {
27
- return [
28
- 'span',
29
- mergeAttributes(HTMLAttributes, {
30
- 'data-editable-region': 'true',
31
- class: 'editable-region'
32
- }),
33
- 0
34
- ];
35
- }
36
- });
37
- export const RestrictedEditing = Extension.create({
38
- name: 'restrictedEditing',
39
- addOptions () {
40
- return {
41
- editableRegionNodeName: 'editableRegion',
42
- disabledNodeTypes: [
43
- 'listItem',
44
- 'blockquote'
45
- ],
46
- initialMode: 'restricted'
47
- };
48
- },
49
- addStorage () {
50
- return {
51
- mode: this.options.initialMode
52
- };
53
- },
54
- addCommands () {
55
- return {
56
- wrapSelectionInEditableRegion: ()=>({ state, dispatch })=>{
57
- // 0. Always return false if the editable region node is not registered
58
- const type = state.schema.nodes[this.options.editableRegionNodeName];
59
- if (!type) return false;
60
- const { from, to, empty } = state.selection;
61
- // 1. Insert an empty editable region with a zero-width space
62
- if (empty) {
63
- const node = type.create(null, state.schema.text('\u200b'));
64
- const tr = state.tr.replaceSelectionWith(node, false);
65
- tr.setMeta('restrictedEditing', {
66
- allow: true
67
- });
68
- dispatch?.(tr.scrollIntoView());
69
- return true;
70
- }
71
- // Disallow wrapping when selection contains non-text inline nodes
72
- let containsNonTextInline = false;
73
- state.doc.nodesBetween(from, to, (node)=>{
74
- if (node.isInline && !node.isText) {
75
- containsNonTextInline = true;
76
- return false;
77
- }
78
- return undefined;
79
- });
80
- if (containsNonTextInline) {
81
- return false;
82
- }
83
- const multiLine = state.doc.textBetween(from, to, '\n', '\n').includes('\n');
84
- let tr = state.tr;
85
- if (multiLine) {
86
- // Wrap each textblock segment independently
87
- const ranges = [];
88
- state.doc.nodesBetween(from, to, (node, pos)=>{
89
- if (node.isTextblock) {
90
- const s = Math.max(from, pos + 1);
91
- const e = Math.min(to, pos + node.nodeSize - 1);
92
- if (e > s) ranges.push({
93
- s,
94
- e
95
- });
96
- }
97
- return undefined;
98
- });
99
- // Apply replacements from end to start to keep positions stable
100
- for(let i = ranges.length - 1; i >= 0; i--){
101
- const { s, e } = ranges[i];
102
- const text = state.doc.textBetween(s, e, '\n', '\n');
103
- if (text.length === 0) continue;
104
- const node = type.create(null, state.schema.text(text));
105
- tr = tr.replaceRangeWith(s, e, node);
106
- }
107
- } else {
108
- const text = state.doc.textBetween(from, to, '\n', '\n');
109
- if (text.length === 0) return false;
110
- const node = type.create(null, state.schema.text(text));
111
- tr = tr.replaceRangeWith(from, to, node);
112
- }
113
- tr.setMeta('restrictedEditing', {
114
- allow: true
115
- });
116
- dispatch?.(tr.scrollIntoView());
117
- return true;
118
- },
119
- removeEditableRegion: ()=>({ state, dispatch })=>{
120
- const type = state.schema.nodes[this.options.editableRegionNodeName];
121
- if (!type) return false;
122
- const $pos = state.selection.$head;
123
- for(let depth = $pos.depth; depth >= 0; depth--){
124
- const node = $pos.node(depth);
125
- if (node.type === type) {
126
- const from = $pos.before(depth);
127
- const to = $pos.after(depth);
128
- const tr = state.tr.replaceWith(from, to, node.content);
129
- tr.setMeta('restrictedEditing', {
130
- allow: true
131
- });
132
- dispatch?.(tr.scrollIntoView());
133
- return true;
134
- }
135
- }
136
- return false;
137
- },
138
- toggleEditableRegion: ()=>({ state, dispatch, commands })=>{
139
- const type = state.schema.nodes[this.options.editableRegionNodeName];
140
- if (!type) return false;
141
- const { from, to, empty } = state.selection;
142
- const findRegionAtPos = (pos)=>{
143
- const $pos = state.doc.resolve(pos);
144
- for(let depth = $pos.depth; depth >= 0; depth--){
145
- const node = $pos.node(depth);
146
- if (node.type === type) {
147
- const nodeFrom = $pos.before(depth);
148
- const nodeTo = $pos.after(depth);
149
- return {
150
- from: nodeFrom,
151
- to: nodeTo,
152
- node
153
- };
154
- }
155
- }
156
- return null;
157
- };
158
- // Cursor case: inside a region -> unwrap that region; else insert empty region
159
- if (empty) {
160
- const region = findRegionAtPos(from);
161
- if (region) {
162
- const tr = state.tr.replaceWith(region.from, region.to, region.node.content);
163
- tr.setMeta('restrictedEditing', {
164
- allow: true
165
- });
166
- dispatch?.(tr.scrollIntoView());
167
- return true;
168
- }
169
- const node = type.create(null, state.schema.text('\u200b'));
170
- const tr = state.tr.replaceSelectionWith(node, false);
171
- tr.setMeta('restrictedEditing', {
172
- allow: true
173
- });
174
- dispatch?.(tr.scrollIntoView());
175
- return true;
176
- }
177
- // Range case: unwrap only if the entire selection is inside the SAME region; otherwise
178
- // if selection is completely outside all regions, wrap it; else ignore
179
- const regionStart = findRegionAtPos(from);
180
- const regionEnd = findRegionAtPos(Math.max(from, to - 1));
181
- if (regionStart && regionEnd && regionStart.from === regionEnd.from && regionStart.to === regionEnd.to) {
182
- const tr = state.tr.replaceWith(regionStart.from, regionStart.to, regionStart.node.content);
183
- tr.setMeta('restrictedEditing', {
184
- allow: true
185
- });
186
- dispatch?.(tr.scrollIntoView());
187
- return true;
188
- }
189
- // If neither endpoint is in a region, ensure there is no region intersecting the range
190
- if (!regionStart && !regionEnd) {
191
- let hasRegionInside = false;
192
- state.doc.nodesBetween(from, to, (node)=>{
193
- if (node.type === type) {
194
- hasRegionInside = true;
195
- return false;
196
- }
197
- return undefined;
198
- });
199
- if (!hasRegionInside) {
200
- return commands.wrapSelectionInEditableRegion();
201
- }
202
- }
203
- return false;
204
- }
205
- };
206
- },
207
- addKeyboardShortcuts () {
208
- return {
209
- Enter: ()=>{
210
- // Get the current selection
211
- const selection = this.editor.state.selection;
212
- const $from = selection.$from;
213
- if (!selection.empty) return false;
214
- let editableRegionInfo = null;
215
- for(let d = $from.depth; d > 0; d--){
216
- const node = $from.node(d);
217
- if (node.type.name === 'editableRegion') {
218
- editableRegionInfo = {
219
- node: node,
220
- depth: d,
221
- // Position of the entire node (before the opening tag)
222
- posBefore: $from.before(d),
223
- // Position of the entire node (after the closing tag)
224
- posAfter: $from.after(d),
225
- // Position of the start of the content inside the node
226
- contentStartPos: $from.start(d),
227
- // Position of the end of the content inside the node
228
- contentEndPos: $from.end(d)
229
- };
230
- break;
231
- }
232
- }
233
- if (!editableRegionInfo) return false;
234
- const atStart = $from.pos === editableRegionInfo.contentStartPos;
235
- const atEnd = $from.pos === editableRegionInfo.contentEndPos;
236
- // If cursor is at the start of the editable region, add a new paragraph before the editable region
237
- if (atStart) {
238
- return this.editor.chain().insertContentAt(editableRegionInfo.posBefore, '<p></p>').focus(editableRegionInfo.posBefore - 2).run();
239
- }
240
- // If cursor is at the end of the editable region, add a new paragraph after the editable region
241
- if (atEnd) {
242
- return this.editor.chain().insertContentAt(editableRegionInfo.posAfter, '<p></p>').focus(editableRegionInfo.posAfter + 1).run();
243
- }
244
- // If cursor is in the middle of the editable region, return false
245
- return false;
246
- }
247
- };
248
- },
249
- addProseMirrorPlugins () {
250
- const key = new PluginKey('restricted-editing');
251
- const editor = this.editor;
252
- const editableRegionNodeName = this.options.editableRegionNodeName;
253
- const disabledNodeTypes = this.options.disabledNodeTypes ?? [];
254
- const getEditableRegionBounds = (doc, schema, pos)=>{
255
- const $pos = doc.resolve(pos);
256
- const type = schema.nodes[editableRegionNodeName];
257
- for(let depth = $pos.depth; depth >= 0; depth--){
258
- if ($pos.node(depth).type === type) {
259
- return {
260
- depth,
261
- from: $pos.start(depth),
262
- to: $pos.end(depth)
263
- };
264
- }
265
- }
266
- return null;
267
- };
268
- const rangesFromTransaction = (tr)=>{
269
- const ranges = [];
270
- for(let i = 0; i < tr.steps.length; i++){
271
- const map = tr.mapping.maps[i];
272
- const forward = tr.mapping.slice(i + 1);
273
- map.forEach((oldStart, oldEnd, newStart, newEnd)=>{
274
- const finalNewFrom = forward.map(newStart, 1);
275
- const finalNewTo = forward.map(newEnd, -1);
276
- ranges.push({
277
- oldFrom: oldStart,
278
- oldTo: oldEnd,
279
- newFrom: finalNewFrom,
280
- newTo: finalNewTo
281
- });
282
- });
283
- }
284
- return ranges;
285
- };
286
- return [
287
- new Plugin({
288
- key,
289
- filterTransaction: (tr, state)=>{
290
- // In standard mode, do not restrict edits
291
- if (this.storage.mode === 'standard') return true;
292
- if (!tr.docChanged) return true;
293
- if (tr.getMeta('restrictedEditing')?.allow) return true;
294
- // Allow while the document is still the default empty state
295
- if (editor.isEmpty) return true;
296
- // In restricted mode: disable mark/style/attr changes from other extensions entirely
297
- const disallowedStepTypes = new Set([
298
- 'addMark',
299
- 'removeMark',
300
- 'setNodeMarkup'
301
- ]);
302
- for (const step of tr.steps){
303
- const t = step?.toJSON?.().stepType;
304
- if (t && disallowedStepTypes.has(t)) {
305
- return false;
306
- }
307
- }
308
- if (disabledNodeTypes.some((type)=>editor.isActive(type))) {
309
- return true;
310
- }
311
- const ranges = rangesFromTransaction(tr);
312
- if (ranges.length === 0) return true;
313
- for (const range of ranges){
314
- // Pre-change validation (against state.doc)
315
- const preStart = getEditableRegionBounds(state.doc, state.schema, range.oldFrom);
316
- const preEnd = getEditableRegionBounds(state.doc, state.schema, range.oldTo);
317
- if (!preStart || !preEnd) return false;
318
- if (preStart.from !== preEnd.from || preStart.to !== preEnd.to) return false;
319
- // Post-change validation (against final tr.doc)
320
- const postStart = getEditableRegionBounds(tr.doc, state.schema, range.newFrom);
321
- const postEnd = getEditableRegionBounds(tr.doc, state.schema, range.newTo);
322
- if (!postStart || !postEnd) return false;
323
- if (postStart.from !== postEnd.from || postStart.to !== postEnd.to) return false;
324
- }
325
- return true;
326
- },
327
- appendTransaction: (_transactions, _oldState, newState)=>{
328
- const type = newState.schema.nodes[editableRegionNodeName];
329
- if (!type) return null;
330
- const isRestricted = this.storage.mode === 'restricted';
331
- const edits = [];
332
- newState.doc.descendants((node, pos)=>{
333
- if (node.type === type) {
334
- const contentText = node.textContent ?? '';
335
- if (contentText.length === 0) {
336
- if (isRestricted) {
337
- // Ensure a single space exists inside empty region
338
- const innerFrom = pos + 1;
339
- const innerTo = pos + node.nodeSize - 1;
340
- edits.push({
341
- kind: 'fill',
342
- from: innerFrom,
343
- to: innerTo
344
- });
345
- } else {
346
- // Remove empty region
347
- edits.push({
348
- kind: 'delete',
349
- from: pos,
350
- to: pos + node.nodeSize
351
- });
352
- }
353
- }
354
- }
355
- });
356
- if (edits.length === 0) return null;
357
- // Apply in descending order of from
358
- edits.sort((a, b)=>b.from - a.from);
359
- const tr = newState.tr;
360
- for (const e of edits){
361
- if (e.kind === 'delete') {
362
- tr.delete(e.from, e.to);
363
- } else {
364
- const textNode = newState.schema.text(' ');
365
- tr.replaceWith(e.from, e.to, textNode);
366
- }
367
- }
368
- tr.setMeta('restrictedEditing', {
369
- allow: true
370
- });
371
- return tr;
372
- }
373
- })
374
- ];
375
- }
376
- });