inkpen 0.7.1

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rubocop.yml +8 -0
  4. data/.yardopts +11 -0
  5. data/CLAUDE.md +141 -0
  6. data/README.md +409 -0
  7. data/Rakefile +19 -0
  8. data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
  9. data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
  10. data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
  11. data/app/assets/javascripts/inkpen/export/html.js +637 -0
  12. data/app/assets/javascripts/inkpen/export/index.js +30 -0
  13. data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
  14. data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
  15. data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
  16. data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
  17. data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
  18. data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
  19. data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
  20. data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
  21. data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
  22. data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
  23. data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
  24. data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
  25. data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
  26. data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
  27. data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
  28. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
  29. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
  30. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
  31. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
  32. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
  33. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
  34. data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
  35. data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
  36. data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
  37. data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
  38. data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
  39. data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
  40. data/app/assets/javascripts/inkpen/index.js +87 -0
  41. data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
  42. data/app/assets/stylesheets/inkpen/animations.css +626 -0
  43. data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
  44. data/app/assets/stylesheets/inkpen/callout.css +359 -0
  45. data/app/assets/stylesheets/inkpen/columns.css +314 -0
  46. data/app/assets/stylesheets/inkpen/database.css +658 -0
  47. data/app/assets/stylesheets/inkpen/document_section.css +305 -0
  48. data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
  49. data/app/assets/stylesheets/inkpen/editor.css +652 -0
  50. data/app/assets/stylesheets/inkpen/embed.css +468 -0
  51. data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
  52. data/app/assets/stylesheets/inkpen/export.css +499 -0
  53. data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
  54. data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
  55. data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
  56. data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
  57. data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
  58. data/app/assets/stylesheets/inkpen/section.css +236 -0
  59. data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
  60. data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
  61. data/app/assets/stylesheets/inkpen/toc.css +386 -0
  62. data/app/assets/stylesheets/inkpen/toggle.css +260 -0
  63. data/app/helpers/inkpen/editor_helper.rb +114 -0
  64. data/app/views/inkpen/_editor.html.erb +139 -0
  65. data/config/importmap.rb +170 -0
  66. data/docs/.DS_Store +0 -0
  67. data/docs/CHANGELOG.md +571 -0
  68. data/docs/FEATURES.md +436 -0
  69. data/docs/ROADMAP.md +3029 -0
  70. data/docs/VISION.md +235 -0
  71. data/docs/extensions/INKPEN_TABLE.md +482 -0
  72. data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
  73. data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
  74. data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
  75. data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
  76. data/docs/thinking/README_START_HERE.md +341 -0
  77. data/lib/inkpen/configuration.rb +175 -0
  78. data/lib/inkpen/editor.rb +204 -0
  79. data/lib/inkpen/engine.rb +32 -0
  80. data/lib/inkpen/extensions/base.rb +109 -0
  81. data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
  82. data/lib/inkpen/extensions/document_section.rb +111 -0
  83. data/lib/inkpen/extensions/forced_document.rb +183 -0
  84. data/lib/inkpen/extensions/mention.rb +155 -0
  85. data/lib/inkpen/extensions/preformatted.rb +111 -0
  86. data/lib/inkpen/extensions/section.rb +139 -0
  87. data/lib/inkpen/extensions/slash_commands.rb +100 -0
  88. data/lib/inkpen/extensions/table.rb +182 -0
  89. data/lib/inkpen/extensions/task_list.rb +145 -0
  90. data/lib/inkpen/sticky_toolbar.rb +157 -0
  91. data/lib/inkpen/toolbar.rb +145 -0
  92. data/lib/inkpen/version.rb +5 -0
  93. data/lib/inkpen.rb +101 -0
  94. data/sig/inkpen.rbs +4 -0
  95. metadata +165 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * InkpenTable Extension
3
+ *
4
+ * Notion/Airtable-style enhanced table with:
5
+ * - Row/column handles with context menus
6
+ * - Quick add buttons
7
+ * - Text color and background color
8
+ * - Alignment controls
9
+ * - Caption and variants
10
+ * - Sticky header option
11
+ *
12
+ * @since 0.8.0
13
+ */
14
+
15
+ import Table from "@tiptap/extension-table"
16
+ import TableRow from "@tiptap/extension-table-row"
17
+ import {
18
+ createElement,
19
+ getTableDimensions,
20
+ nextFrame
21
+ } from "inkpen/extensions/inkpen_table/table_helpers"
22
+ import { TableMenu } from "inkpen/extensions/inkpen_table/table_menu"
23
+ import { CSS_CLASSES, DEFAULT_CONFIG, TABLE_VARIANTS } from "inkpen/extensions/inkpen_table/table_constants"
24
+
25
+ // =============================================================================
26
+ // InkpenTableRow Extension
27
+ // =============================================================================
28
+
29
+ export const InkpenTableRow = TableRow.extend({
30
+ name: "tableRow"
31
+ })
32
+
33
+ // =============================================================================
34
+ // InkpenTable Extension
35
+ // =============================================================================
36
+
37
+ export const InkpenTable = Table.extend({
38
+ name: "table",
39
+
40
+ addOptions() {
41
+ return {
42
+ ...this.parent?.(),
43
+ ...DEFAULT_CONFIG,
44
+ HTMLAttributes: {
45
+ class: "inkpen-table"
46
+ }
47
+ }
48
+ },
49
+
50
+ addAttributes() {
51
+ return {
52
+ ...this.parent?.(),
53
+
54
+ caption: {
55
+ default: null,
56
+ parseHTML: element => {
57
+ const caption = element.querySelector("caption")
58
+ return caption ? caption.textContent : null
59
+ },
60
+ renderHTML: () => ({})
61
+ },
62
+
63
+ variant: {
64
+ default: "default",
65
+ parseHTML: element => element.getAttribute("data-variant") || "default",
66
+ renderHTML: attributes => ({ "data-variant": attributes.variant })
67
+ },
68
+
69
+ stickyHeader: {
70
+ default: false,
71
+ parseHTML: element => element.hasAttribute("data-sticky-header"),
72
+ renderHTML: attributes => attributes.stickyHeader ? { "data-sticky-header": "" } : {}
73
+ }
74
+ }
75
+ },
76
+
77
+ addNodeView() {
78
+ return ({ node, editor, getPos }) => {
79
+ const options = this.options
80
+ const { caption, variant, stickyHeader } = node.attrs
81
+
82
+ // State
83
+ let menu = null
84
+ let currentRowIndex = -1
85
+ let currentColIndex = -1
86
+
87
+ // Main wrapper
88
+ const wrapper = createElement("div", {
89
+ className: `${CSS_CLASSES.wrapper}${stickyHeader ? " " + CSS_CLASSES.wrapper + "--sticky-header" : ""}`
90
+ })
91
+
92
+ // Caption (if enabled)
93
+ let captionEl = null
94
+ if (options.showCaption) {
95
+ captionEl = createElement("div", {
96
+ className: CSS_CLASSES.caption,
97
+ contentEditable: editor.isEditable ? "true" : "false",
98
+ placeholder: "Add table caption..."
99
+ }, [caption || ""])
100
+
101
+ if (editor.isEditable) {
102
+ captionEl.addEventListener("input", () => {
103
+ updateCaption(captionEl.textContent || null)
104
+ })
105
+
106
+ captionEl.addEventListener("keydown", (e) => {
107
+ if (e.key === "Enter") {
108
+ e.preventDefault()
109
+ captionEl.blur()
110
+ }
111
+ })
112
+ }
113
+
114
+ wrapper.appendChild(captionEl)
115
+ }
116
+
117
+ // Column handles container
118
+ let colHandlesEl = null
119
+ if (options.showHandles && editor.isEditable) {
120
+ colHandlesEl = createElement("div", { className: CSS_CLASSES.colHandles })
121
+ wrapper.appendChild(colHandlesEl)
122
+ }
123
+
124
+ // Body container (row handles + table)
125
+ const bodyEl = createElement("div", { className: CSS_CLASSES.body })
126
+
127
+ // Row handles container
128
+ let rowHandlesEl = null
129
+ if (options.showHandles && editor.isEditable) {
130
+ rowHandlesEl = createElement("div", { className: CSS_CLASSES.rowHandles })
131
+ bodyEl.appendChild(rowHandlesEl)
132
+ }
133
+
134
+ // Table content (contentDOM)
135
+ const contentEl = createElement("div", {
136
+ className: `${CSS_CLASSES.content} inkpen-table--${variant}`
137
+ })
138
+ bodyEl.appendChild(contentEl)
139
+ wrapper.appendChild(bodyEl)
140
+
141
+ // Add row button
142
+ let addRowBtn = null
143
+ if (options.showAddButtons && editor.isEditable) {
144
+ addRowBtn = createElement("button", {
145
+ className: CSS_CLASSES.addRow,
146
+ type: "button"
147
+ }, ["+ New row"])
148
+
149
+ addRowBtn.addEventListener("click", (e) => {
150
+ e.preventDefault()
151
+ editor.chain().focus().addRowAfter().run()
152
+ })
153
+
154
+ wrapper.appendChild(addRowBtn)
155
+ }
156
+
157
+ // Initialize menu
158
+ if (options.showHandles && editor.isEditable) {
159
+ menu = new TableMenu({
160
+ onAction: handleMenuAction
161
+ })
162
+ }
163
+
164
+ // Update handles when table structure changes
165
+ function updateHandles() {
166
+ if (!options.showHandles || !editor.isEditable) return
167
+
168
+ const tableEl = contentEl.querySelector("table")
169
+ if (!tableEl) return
170
+
171
+ const { rowCount, colCount } = getTableDimensions(tableEl)
172
+
173
+ // Update column handles
174
+ colHandlesEl.innerHTML = ""
175
+ for (let i = 0; i < colCount; i++) {
176
+ const handle = createElement("button", {
177
+ className: CSS_CLASSES.handle,
178
+ type: "button",
179
+ dataset: { col: String(i) },
180
+ title: "Column options"
181
+ }, ["\u22EE\u22EE"])
182
+
183
+ handle.addEventListener("click", (e) => {
184
+ e.preventDefault()
185
+ e.stopPropagation()
186
+ currentColIndex = i
187
+ currentRowIndex = -1
188
+ menu.showColumnMenu(handle, { colIndex: i })
189
+ })
190
+
191
+ colHandlesEl.appendChild(handle)
192
+ }
193
+
194
+ // Add column button
195
+ if (options.showAddButtons) {
196
+ const addColBtn = createElement("button", {
197
+ className: CSS_CLASSES.addCol,
198
+ type: "button",
199
+ title: "Add column"
200
+ }, ["+"])
201
+
202
+ addColBtn.addEventListener("click", (e) => {
203
+ e.preventDefault()
204
+ editor.chain().focus().addColumnAfter().run()
205
+ })
206
+
207
+ colHandlesEl.appendChild(addColBtn)
208
+ }
209
+
210
+ // Update row handles
211
+ rowHandlesEl.innerHTML = ""
212
+ for (let i = 0; i < rowCount; i++) {
213
+ const handle = createElement("button", {
214
+ className: CSS_CLASSES.handle,
215
+ type: "button",
216
+ dataset: { row: String(i) },
217
+ title: "Row options"
218
+ }, ["\u22EE\u22EE"])
219
+
220
+ handle.addEventListener("click", (e) => {
221
+ e.preventDefault()
222
+ e.stopPropagation()
223
+ currentRowIndex = i
224
+ currentColIndex = -1
225
+ menu.showRowMenu(handle, { rowIndex: i })
226
+ })
227
+
228
+ rowHandlesEl.appendChild(handle)
229
+ }
230
+ }
231
+
232
+ // Handle menu actions
233
+ function handleMenuAction({ action, menuType, color, value }) {
234
+ const chain = editor.chain().focus()
235
+
236
+ switch (action) {
237
+ // Row actions
238
+ case "addRowAbove":
239
+ selectRowByIndex(currentRowIndex)
240
+ chain.addRowBefore().run()
241
+ break
242
+
243
+ case "addRowBelow":
244
+ selectRowByIndex(currentRowIndex)
245
+ chain.addRowAfter().run()
246
+ break
247
+
248
+ case "duplicateRow":
249
+ duplicateRow(currentRowIndex)
250
+ break
251
+
252
+ case "deleteRow":
253
+ selectRowByIndex(currentRowIndex)
254
+ chain.deleteRow().run()
255
+ break
256
+
257
+ case "moveRowUp":
258
+ moveRow(currentRowIndex, -1)
259
+ break
260
+
261
+ case "moveRowDown":
262
+ moveRow(currentRowIndex, 1)
263
+ break
264
+
265
+ case "toggleHeaderRow":
266
+ selectRowByIndex(currentRowIndex)
267
+ chain.toggleHeaderRow().run()
268
+ break
269
+
270
+ // Column actions
271
+ case "addColumnLeft":
272
+ selectColumnByIndex(currentColIndex)
273
+ chain.addColumnBefore().run()
274
+ break
275
+
276
+ case "addColumnRight":
277
+ selectColumnByIndex(currentColIndex)
278
+ chain.addColumnAfter().run()
279
+ break
280
+
281
+ case "duplicateColumn":
282
+ duplicateColumn(currentColIndex)
283
+ break
284
+
285
+ case "deleteColumn":
286
+ selectColumnByIndex(currentColIndex)
287
+ chain.deleteColumn().run()
288
+ break
289
+
290
+ case "moveColumnLeft":
291
+ moveColumn(currentColIndex, -1)
292
+ break
293
+
294
+ case "moveColumnRight":
295
+ moveColumn(currentColIndex, 1)
296
+ break
297
+
298
+ // Styling
299
+ case "alignment":
300
+ if (menuType === "column") {
301
+ selectColumnByIndex(currentColIndex)
302
+ } else {
303
+ selectRowByIndex(currentRowIndex)
304
+ }
305
+ chain.setCellAlignment(value).run()
306
+ break
307
+
308
+ case "textColor":
309
+ if (menuType === "column") {
310
+ selectColumnByIndex(currentColIndex)
311
+ } else {
312
+ selectRowByIndex(currentRowIndex)
313
+ }
314
+ chain.setCellTextColor(color).run()
315
+ break
316
+
317
+ case "backgroundColor":
318
+ if (menuType === "column") {
319
+ selectColumnByIndex(currentColIndex)
320
+ } else {
321
+ selectRowByIndex(currentRowIndex)
322
+ }
323
+ chain.setCellBackground(color).run()
324
+ break
325
+ }
326
+
327
+ // Update handles after any structural change
328
+ nextFrame().then(updateHandles)
329
+ }
330
+
331
+ // Select a row by index (set cursor in first cell of that row)
332
+ function selectRowByIndex(rowIndex) {
333
+ if (typeof getPos !== "function") return
334
+
335
+ const pos = getPos()
336
+ if (pos === undefined) return
337
+
338
+ const tableNode = editor.state.doc.nodeAt(pos)
339
+ if (!tableNode) return
340
+
341
+ let cellPos = pos + 1 // Start after table open tag
342
+ let currentRow = 0
343
+
344
+ tableNode.forEach((rowNode, rowOffset) => {
345
+ if (currentRow === rowIndex) {
346
+ // Found our row, set selection to first cell
347
+ const firstCellPos = pos + 1 + rowOffset + 1 // table + row + first cell
348
+ editor.commands.setTextSelection(firstCellPos + 1)
349
+ return false
350
+ }
351
+ currentRow++
352
+ })
353
+ }
354
+
355
+ // Select a column by index
356
+ function selectColumnByIndex(colIndex) {
357
+ if (typeof getPos !== "function") return
358
+
359
+ const pos = getPos()
360
+ if (pos === undefined) return
361
+
362
+ const tableNode = editor.state.doc.nodeAt(pos)
363
+ if (!tableNode || tableNode.childCount === 0) return
364
+
365
+ // Get the first row
366
+ const firstRow = tableNode.firstChild
367
+ if (!firstRow || colIndex >= firstRow.childCount) return
368
+
369
+ // Navigate to the cell at colIndex in the first row
370
+ let cellPos = pos + 2 // table open + row open
371
+ for (let i = 0; i < colIndex; i++) {
372
+ cellPos += firstRow.child(i).nodeSize
373
+ }
374
+
375
+ editor.commands.setTextSelection(cellPos + 1)
376
+ }
377
+
378
+ // Duplicate a row
379
+ function duplicateRow(rowIndex) {
380
+ // For now, add row below (full duplication requires transaction manipulation)
381
+ selectRowByIndex(rowIndex)
382
+ editor.chain().focus().addRowAfter().run()
383
+ }
384
+
385
+ // Duplicate a column
386
+ function duplicateColumn(colIndex) {
387
+ // For now, add column after (full duplication requires transaction manipulation)
388
+ selectColumnByIndex(colIndex)
389
+ editor.chain().focus().addColumnAfter().run()
390
+ }
391
+
392
+ // Move a row up or down
393
+ function moveRow(rowIndex, direction) {
394
+ // Row movement requires complex transaction - defer to future version
395
+ // For now, just focus the row
396
+ selectRowByIndex(rowIndex)
397
+ }
398
+
399
+ // Move a column left or right
400
+ function moveColumn(colIndex, direction) {
401
+ // Column movement requires complex transaction - defer to future version
402
+ // For now, just focus the column
403
+ selectColumnByIndex(colIndex)
404
+ }
405
+
406
+ // Update caption
407
+ function updateCaption(newCaption) {
408
+ if (typeof getPos !== "function") return
409
+
410
+ const pos = getPos()
411
+ if (pos === undefined) return
412
+
413
+ editor.chain().command(({ tr }) => {
414
+ const tableNode = tr.doc.nodeAt(pos)
415
+ if (tableNode) {
416
+ tr.setNodeMarkup(pos, undefined, {
417
+ ...tableNode.attrs,
418
+ caption: newCaption
419
+ })
420
+ }
421
+ return true
422
+ }).run()
423
+ }
424
+
425
+ // Cleanup
426
+ function destroy() {
427
+ if (menu) {
428
+ menu.destroy()
429
+ menu = null
430
+ }
431
+ }
432
+
433
+ // Initial handle setup
434
+ if (options.showHandles && editor.isEditable) {
435
+ nextFrame().then(updateHandles)
436
+ }
437
+
438
+ return {
439
+ dom: wrapper,
440
+ contentDOM: contentEl,
441
+
442
+ update: (updatedNode) => {
443
+ if (updatedNode.type.name !== "table") return false
444
+
445
+ const newVariant = updatedNode.attrs.variant || "default"
446
+ const newCaption = updatedNode.attrs.caption
447
+ const newSticky = updatedNode.attrs.stickyHeader
448
+
449
+ // Update variant class
450
+ contentEl.className = `${CSS_CLASSES.content} inkpen-table--${newVariant}`
451
+
452
+ // Update sticky class
453
+ wrapper.classList.toggle(`${CSS_CLASSES.wrapper}--sticky-header`, newSticky)
454
+
455
+ // Update caption
456
+ if (captionEl && captionEl.textContent !== newCaption) {
457
+ captionEl.textContent = newCaption || ""
458
+ }
459
+
460
+ // Update handles
461
+ if (options.showHandles && editor.isEditable) {
462
+ nextFrame().then(updateHandles)
463
+ }
464
+
465
+ return true
466
+ },
467
+
468
+ destroy
469
+ }
470
+ }
471
+ },
472
+
473
+ addCommands() {
474
+ return {
475
+ ...this.parent?.(),
476
+
477
+ setTableCaption: (caption) => ({ tr, state, dispatch }) => {
478
+ const tableNode = findParentTable(state.selection)
479
+ if (!tableNode) return false
480
+
481
+ if (dispatch) {
482
+ tr.setNodeMarkup(tableNode.pos, undefined, {
483
+ ...tableNode.node.attrs,
484
+ caption
485
+ })
486
+ }
487
+
488
+ return true
489
+ },
490
+
491
+ setTableVariant: (variant) => ({ tr, state, dispatch }) => {
492
+ const tableNode = findParentTable(state.selection)
493
+ if (!tableNode) return false
494
+
495
+ if (dispatch) {
496
+ tr.setNodeMarkup(tableNode.pos, undefined, {
497
+ ...tableNode.node.attrs,
498
+ variant
499
+ })
500
+ }
501
+
502
+ return true
503
+ },
504
+
505
+ toggleStickyHeader: () => ({ tr, state, dispatch }) => {
506
+ const tableNode = findParentTable(state.selection)
507
+ if (!tableNode) return false
508
+
509
+ if (dispatch) {
510
+ tr.setNodeMarkup(tableNode.pos, undefined, {
511
+ ...tableNode.node.attrs,
512
+ stickyHeader: !tableNode.node.attrs.stickyHeader
513
+ })
514
+ }
515
+
516
+ return true
517
+ },
518
+
519
+ setCellAlignment: (align) => ({ tr, state, dispatch }) => {
520
+ const cells = getSelectedCellPositions(state)
521
+ if (cells.length === 0) return false
522
+
523
+ if (dispatch) {
524
+ cells.forEach(({ pos, node }) => {
525
+ tr.setNodeMarkup(pos, undefined, {
526
+ ...node.attrs,
527
+ align
528
+ })
529
+ })
530
+ }
531
+
532
+ return true
533
+ },
534
+
535
+ setCellBackground: (background) => ({ tr, state, dispatch }) => {
536
+ const cells = getSelectedCellPositions(state)
537
+ if (cells.length === 0) return false
538
+
539
+ if (dispatch) {
540
+ cells.forEach(({ pos, node }) => {
541
+ tr.setNodeMarkup(pos, undefined, {
542
+ ...node.attrs,
543
+ background
544
+ })
545
+ })
546
+ }
547
+
548
+ return true
549
+ },
550
+
551
+ clearCellBackground: () => ({ commands }) => {
552
+ return commands.setCellBackground(null)
553
+ },
554
+
555
+ setCellTextColor: (textColor) => ({ tr, state, dispatch }) => {
556
+ const cells = getSelectedCellPositions(state)
557
+ if (cells.length === 0) return false
558
+
559
+ if (dispatch) {
560
+ cells.forEach(({ pos, node }) => {
561
+ tr.setNodeMarkup(pos, undefined, {
562
+ ...node.attrs,
563
+ textColor
564
+ })
565
+ })
566
+ }
567
+
568
+ return true
569
+ },
570
+
571
+ clearCellTextColor: () => ({ commands }) => {
572
+ return commands.setCellTextColor(null)
573
+ }
574
+ }
575
+ },
576
+
577
+ addKeyboardShortcuts() {
578
+ return {
579
+ ...this.parent?.(),
580
+ "Mod-Shift-l": () => this.editor.commands.setCellAlignment("left"),
581
+ "Mod-Shift-e": () => this.editor.commands.setCellAlignment("center"),
582
+ "Mod-Shift-r": () => this.editor.commands.setCellAlignment("right")
583
+ }
584
+ }
585
+ })
586
+
587
+ // =============================================================================
588
+ // Helper Functions
589
+ // =============================================================================
590
+
591
+ function findParentTable(selection) {
592
+ const { $from } = selection
593
+ for (let d = $from.depth; d > 0; d--) {
594
+ const node = $from.node(d)
595
+ if (node.type.name === "table") {
596
+ return { node, pos: $from.before(d) }
597
+ }
598
+ }
599
+ return null
600
+ }
601
+
602
+ function getSelectedCellPositions(state) {
603
+ const cells = []
604
+ const { selection, doc } = state
605
+
606
+ // Check for CellSelection (multiple cells selected)
607
+ if (selection.$anchorCell && selection.$headCell) {
608
+ const cellSelection = selection
609
+ const map = {}
610
+
611
+ doc.nodesBetween(
612
+ cellSelection.$anchorCell.start(-1),
613
+ cellSelection.$headCell.start(-1) + cellSelection.$headCell.parent.nodeSize,
614
+ (node, pos) => {
615
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
616
+ if (!map[pos]) {
617
+ map[pos] = true
618
+ cells.push({ node, pos })
619
+ }
620
+ }
621
+ }
622
+ )
623
+ } else {
624
+ // Single cell - find the cell we're in
625
+ const { $from } = selection
626
+ for (let d = $from.depth; d > 0; d--) {
627
+ const node = $from.node(d)
628
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
629
+ cells.push({ node, pos: $from.before(d) })
630
+ break
631
+ }
632
+ }
633
+ }
634
+
635
+ return cells
636
+ }
637
+
638
+ export default InkpenTable