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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +11 -0
- data/CLAUDE.md +141 -0
- data/README.md +409 -0
- data/Rakefile +19 -0
- data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
- data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
- data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
- data/app/assets/javascripts/inkpen/export/html.js +637 -0
- data/app/assets/javascripts/inkpen/export/index.js +30 -0
- data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
- data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
- data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
- data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
- data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
- data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
- data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
- data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
- data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
- data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
- data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
- data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
- data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
- data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
- data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
- data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
- data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
- data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
- data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
- data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
- data/app/assets/javascripts/inkpen/index.js +87 -0
- data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
- data/app/assets/stylesheets/inkpen/animations.css +626 -0
- data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
- data/app/assets/stylesheets/inkpen/callout.css +359 -0
- data/app/assets/stylesheets/inkpen/columns.css +314 -0
- data/app/assets/stylesheets/inkpen/database.css +658 -0
- data/app/assets/stylesheets/inkpen/document_section.css +305 -0
- data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
- data/app/assets/stylesheets/inkpen/editor.css +652 -0
- data/app/assets/stylesheets/inkpen/embed.css +468 -0
- data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
- data/app/assets/stylesheets/inkpen/export.css +499 -0
- data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
- data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
- data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
- data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
- data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
- data/app/assets/stylesheets/inkpen/section.css +236 -0
- data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
- data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
- data/app/assets/stylesheets/inkpen/toc.css +386 -0
- data/app/assets/stylesheets/inkpen/toggle.css +260 -0
- data/app/helpers/inkpen/editor_helper.rb +114 -0
- data/app/views/inkpen/_editor.html.erb +139 -0
- data/config/importmap.rb +170 -0
- data/docs/.DS_Store +0 -0
- data/docs/CHANGELOG.md +571 -0
- data/docs/FEATURES.md +436 -0
- data/docs/ROADMAP.md +3029 -0
- data/docs/VISION.md +235 -0
- data/docs/extensions/INKPEN_TABLE.md +482 -0
- data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
- data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
- data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
- data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
- data/docs/thinking/README_START_HERE.md +341 -0
- data/lib/inkpen/configuration.rb +175 -0
- data/lib/inkpen/editor.rb +204 -0
- data/lib/inkpen/engine.rb +32 -0
- data/lib/inkpen/extensions/base.rb +109 -0
- data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
- data/lib/inkpen/extensions/document_section.rb +111 -0
- data/lib/inkpen/extensions/forced_document.rb +183 -0
- data/lib/inkpen/extensions/mention.rb +155 -0
- data/lib/inkpen/extensions/preformatted.rb +111 -0
- data/lib/inkpen/extensions/section.rb +139 -0
- data/lib/inkpen/extensions/slash_commands.rb +100 -0
- data/lib/inkpen/extensions/table.rb +182 -0
- data/lib/inkpen/extensions/task_list.rb +145 -0
- data/lib/inkpen/sticky_toolbar.rb +157 -0
- data/lib/inkpen/toolbar.rb +145 -0
- data/lib/inkpen/version.rb +5 -0
- data/lib/inkpen.rb +101 -0
- data/sig/inkpen.rbs +4 -0
- 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
|