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,640 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core"
|
|
2
|
+
import Table from "@tiptap/extension-table"
|
|
3
|
+
import TableRow from "@tiptap/extension-table-row"
|
|
4
|
+
import TableCell from "@tiptap/extension-table-cell"
|
|
5
|
+
import TableHeader from "@tiptap/extension-table-header"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Advanced Table Extension for TipTap
|
|
9
|
+
*
|
|
10
|
+
* Extends TipTap's table with professional features:
|
|
11
|
+
* - Column alignment (left, center, right)
|
|
12
|
+
* - Table caption/title
|
|
13
|
+
* - Striped rows option
|
|
14
|
+
* - Border style variants (default, striped, borderless, minimal)
|
|
15
|
+
* - Cell background colors
|
|
16
|
+
* - Table toolbar on selection
|
|
17
|
+
* - Sticky header option
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
21
|
+
* editor.commands.setTableVariant('striped')
|
|
22
|
+
* editor.commands.setCellAlignment('center')
|
|
23
|
+
*
|
|
24
|
+
* @since 0.6.0
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Cell background color options
|
|
28
|
+
const CELL_BACKGROUNDS = {
|
|
29
|
+
gray: { label: "Gray", color: "var(--inkpen-table-bg-gray)" },
|
|
30
|
+
red: { label: "Red", color: "var(--inkpen-table-bg-red)" },
|
|
31
|
+
orange: { label: "Orange", color: "var(--inkpen-table-bg-orange)" },
|
|
32
|
+
yellow: { label: "Yellow", color: "var(--inkpen-table-bg-yellow)" },
|
|
33
|
+
green: { label: "Green", color: "var(--inkpen-table-bg-green)" },
|
|
34
|
+
blue: { label: "Blue", color: "var(--inkpen-table-bg-blue)" },
|
|
35
|
+
purple: { label: "Purple", color: "var(--inkpen-table-bg-purple)" }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Table variant styles
|
|
39
|
+
const TABLE_VARIANTS = {
|
|
40
|
+
default: { label: "Default", description: "Standard bordered table" },
|
|
41
|
+
striped: { label: "Striped", description: "Alternating row colors" },
|
|
42
|
+
borderless: { label: "Borderless", description: "No vertical borders" },
|
|
43
|
+
minimal: { label: "Minimal", description: "Clean, no borders" }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Alignment options
|
|
47
|
+
const ALIGNMENTS = {
|
|
48
|
+
left: { label: "Left", icon: "⬅" },
|
|
49
|
+
center: { label: "Center", icon: "⬌" },
|
|
50
|
+
right: { label: "Right", icon: "➡" }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extended TableCell with alignment and background attributes
|
|
55
|
+
*/
|
|
56
|
+
export const AdvancedTableCell = TableCell.extend({
|
|
57
|
+
addAttributes() {
|
|
58
|
+
return {
|
|
59
|
+
...this.parent?.(),
|
|
60
|
+
align: {
|
|
61
|
+
default: null,
|
|
62
|
+
parseHTML: element => element.getAttribute("data-align") || element.style.textAlign || null,
|
|
63
|
+
renderHTML: attributes => {
|
|
64
|
+
if (!attributes.align) return {}
|
|
65
|
+
return {
|
|
66
|
+
"data-align": attributes.align,
|
|
67
|
+
style: `text-align: ${attributes.align}`
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
background: {
|
|
72
|
+
default: null,
|
|
73
|
+
parseHTML: element => element.getAttribute("data-background"),
|
|
74
|
+
renderHTML: attributes => {
|
|
75
|
+
if (!attributes.background) return {}
|
|
76
|
+
return { "data-background": attributes.background }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extended TableHeader with alignment and background attributes
|
|
85
|
+
*/
|
|
86
|
+
export const AdvancedTableHeader = TableHeader.extend({
|
|
87
|
+
addAttributes() {
|
|
88
|
+
return {
|
|
89
|
+
...this.parent?.(),
|
|
90
|
+
align: {
|
|
91
|
+
default: null,
|
|
92
|
+
parseHTML: element => element.getAttribute("data-align") || element.style.textAlign || null,
|
|
93
|
+
renderHTML: attributes => {
|
|
94
|
+
if (!attributes.align) return {}
|
|
95
|
+
return {
|
|
96
|
+
"data-align": attributes.align,
|
|
97
|
+
style: `text-align: ${attributes.align}`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
background: {
|
|
102
|
+
default: null,
|
|
103
|
+
parseHTML: element => element.getAttribute("data-background"),
|
|
104
|
+
renderHTML: attributes => {
|
|
105
|
+
if (!attributes.background) return {}
|
|
106
|
+
return { "data-background": attributes.background }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extended TableRow (unchanged, included for completeness)
|
|
115
|
+
*/
|
|
116
|
+
export const AdvancedTableRow = TableRow.extend({
|
|
117
|
+
// No changes needed, but exported for consistency
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Advanced Table Extension
|
|
122
|
+
*/
|
|
123
|
+
export const AdvancedTable = Table.extend({
|
|
124
|
+
addOptions() {
|
|
125
|
+
return {
|
|
126
|
+
...this.parent?.(),
|
|
127
|
+
resizable: true,
|
|
128
|
+
showControls: true,
|
|
129
|
+
defaultVariant: "default",
|
|
130
|
+
variants: TABLE_VARIANTS,
|
|
131
|
+
cellBackgrounds: CELL_BACKGROUNDS,
|
|
132
|
+
alignments: ALIGNMENTS,
|
|
133
|
+
HTMLAttributes: {
|
|
134
|
+
class: "inkpen-table"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
addAttributes() {
|
|
140
|
+
return {
|
|
141
|
+
...this.parent?.(),
|
|
142
|
+
caption: {
|
|
143
|
+
default: null,
|
|
144
|
+
parseHTML: element => {
|
|
145
|
+
const caption = element.querySelector("caption")
|
|
146
|
+
return caption ? caption.textContent : null
|
|
147
|
+
},
|
|
148
|
+
renderHTML: () => ({}) // Caption rendered separately in NodeView
|
|
149
|
+
},
|
|
150
|
+
variant: {
|
|
151
|
+
default: "default",
|
|
152
|
+
parseHTML: element => element.getAttribute("data-variant") || "default",
|
|
153
|
+
renderHTML: attributes => ({ "data-variant": attributes.variant })
|
|
154
|
+
},
|
|
155
|
+
stickyHeader: {
|
|
156
|
+
default: false,
|
|
157
|
+
parseHTML: element => element.hasAttribute("data-sticky-header"),
|
|
158
|
+
renderHTML: attributes => attributes.stickyHeader ? { "data-sticky-header": "" } : {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
addNodeView() {
|
|
164
|
+
return ({ node, editor, getPos }) => {
|
|
165
|
+
const { caption, variant, stickyHeader } = node.attrs
|
|
166
|
+
|
|
167
|
+
// Wrapper container
|
|
168
|
+
const wrapper = document.createElement("div")
|
|
169
|
+
wrapper.className = "inkpen-table-wrapper"
|
|
170
|
+
if (stickyHeader) wrapper.classList.add("inkpen-table-wrapper--sticky-header")
|
|
171
|
+
|
|
172
|
+
// Caption (if present)
|
|
173
|
+
let captionEl = null
|
|
174
|
+
if (caption || editor.isEditable) {
|
|
175
|
+
captionEl = document.createElement("div")
|
|
176
|
+
captionEl.className = "inkpen-table__caption"
|
|
177
|
+
captionEl.contentEditable = editor.isEditable ? "true" : "false"
|
|
178
|
+
captionEl.textContent = caption || ""
|
|
179
|
+
captionEl.setAttribute("placeholder", "Add table caption...")
|
|
180
|
+
|
|
181
|
+
if (editor.isEditable) {
|
|
182
|
+
captionEl.addEventListener("input", () => {
|
|
183
|
+
if (typeof getPos === "function") {
|
|
184
|
+
const pos = getPos()
|
|
185
|
+
if (pos !== undefined) {
|
|
186
|
+
editor.chain().command(({ tr }) => {
|
|
187
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
188
|
+
...node.attrs,
|
|
189
|
+
caption: captionEl.textContent || null
|
|
190
|
+
})
|
|
191
|
+
return true
|
|
192
|
+
}).run()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
captionEl.addEventListener("keydown", (e) => {
|
|
198
|
+
if (e.key === "Enter") {
|
|
199
|
+
e.preventDefault()
|
|
200
|
+
captionEl.blur()
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
wrapper.appendChild(captionEl)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Table controls toolbar (shown on hover/focus)
|
|
209
|
+
if (this.options.showControls && editor.isEditable) {
|
|
210
|
+
const controls = this.createTableControls(node, editor, getPos)
|
|
211
|
+
wrapper.appendChild(controls)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Table container (contentDOM for ProseMirror)
|
|
215
|
+
const tableContainer = document.createElement("div")
|
|
216
|
+
tableContainer.className = `inkpen-table__container inkpen-table--${variant}`
|
|
217
|
+
wrapper.appendChild(tableContainer)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
dom: wrapper,
|
|
221
|
+
contentDOM: tableContainer,
|
|
222
|
+
update: (updatedNode) => {
|
|
223
|
+
if (updatedNode.type.name !== "table") return false
|
|
224
|
+
|
|
225
|
+
const newVariant = updatedNode.attrs.variant || "default"
|
|
226
|
+
const newCaption = updatedNode.attrs.caption
|
|
227
|
+
const newSticky = updatedNode.attrs.stickyHeader
|
|
228
|
+
|
|
229
|
+
// Update variant class
|
|
230
|
+
tableContainer.className = `inkpen-table__container inkpen-table--${newVariant}`
|
|
231
|
+
|
|
232
|
+
// Update sticky class
|
|
233
|
+
wrapper.classList.toggle("inkpen-table-wrapper--sticky-header", newSticky)
|
|
234
|
+
|
|
235
|
+
// Update caption
|
|
236
|
+
if (captionEl && captionEl.textContent !== newCaption) {
|
|
237
|
+
captionEl.textContent = newCaption || ""
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
addCommands() {
|
|
247
|
+
return {
|
|
248
|
+
...this.parent?.(),
|
|
249
|
+
|
|
250
|
+
setTableCaption: (caption) => ({ tr, state, dispatch }) => {
|
|
251
|
+
const { selection } = state
|
|
252
|
+
const tableNode = findParentTable(selection)
|
|
253
|
+
|
|
254
|
+
if (!tableNode) return false
|
|
255
|
+
|
|
256
|
+
if (dispatch) {
|
|
257
|
+
tr.setNodeMarkup(tableNode.pos, undefined, {
|
|
258
|
+
...tableNode.node.attrs,
|
|
259
|
+
caption
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return true
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
setTableVariant: (variant) => ({ tr, state, dispatch }) => {
|
|
267
|
+
const { selection } = state
|
|
268
|
+
const tableNode = findParentTable(selection)
|
|
269
|
+
|
|
270
|
+
if (!tableNode) return false
|
|
271
|
+
|
|
272
|
+
if (dispatch) {
|
|
273
|
+
tr.setNodeMarkup(tableNode.pos, undefined, {
|
|
274
|
+
...tableNode.node.attrs,
|
|
275
|
+
variant
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return true
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
toggleStickyHeader: () => ({ tr, state, dispatch }) => {
|
|
283
|
+
const { selection } = state
|
|
284
|
+
const tableNode = findParentTable(selection)
|
|
285
|
+
|
|
286
|
+
if (!tableNode) return false
|
|
287
|
+
|
|
288
|
+
if (dispatch) {
|
|
289
|
+
tr.setNodeMarkup(tableNode.pos, undefined, {
|
|
290
|
+
...tableNode.node.attrs,
|
|
291
|
+
stickyHeader: !tableNode.node.attrs.stickyHeader
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return true
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
setCellAlignment: (align) => ({ tr, state, dispatch }) => {
|
|
299
|
+
const { selection } = state
|
|
300
|
+
const cellPositions = getSelectedCellPositions(state)
|
|
301
|
+
|
|
302
|
+
if (cellPositions.length === 0) return false
|
|
303
|
+
|
|
304
|
+
if (dispatch) {
|
|
305
|
+
cellPositions.forEach(({ pos, node }) => {
|
|
306
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
307
|
+
...node.attrs,
|
|
308
|
+
align
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return true
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
setCellBackground: (background) => ({ tr, state, dispatch }) => {
|
|
317
|
+
const { selection } = state
|
|
318
|
+
const cellPositions = getSelectedCellPositions(state)
|
|
319
|
+
|
|
320
|
+
if (cellPositions.length === 0) return false
|
|
321
|
+
|
|
322
|
+
if (dispatch) {
|
|
323
|
+
cellPositions.forEach(({ pos, node }) => {
|
|
324
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
325
|
+
...node.attrs,
|
|
326
|
+
background
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return true
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
clearCellBackground: () => ({ commands }) => {
|
|
335
|
+
return commands.setCellBackground(null)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
addKeyboardShortcuts() {
|
|
341
|
+
return {
|
|
342
|
+
...this.parent?.(),
|
|
343
|
+
"Mod-Shift-l": () => this.editor.commands.setCellAlignment("left"),
|
|
344
|
+
"Mod-Shift-e": () => this.editor.commands.setCellAlignment("center"),
|
|
345
|
+
"Mod-Shift-r": () => this.editor.commands.setCellAlignment("right")
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// Private: Create table controls toolbar
|
|
350
|
+
|
|
351
|
+
createTableControls(node, editor, getPos) {
|
|
352
|
+
const controls = document.createElement("div")
|
|
353
|
+
controls.className = "inkpen-table__controls"
|
|
354
|
+
controls.contentEditable = "false"
|
|
355
|
+
|
|
356
|
+
// Alignment buttons
|
|
357
|
+
const alignGroup = document.createElement("div")
|
|
358
|
+
alignGroup.className = "inkpen-table__control-group"
|
|
359
|
+
|
|
360
|
+
Object.entries(ALIGNMENTS).forEach(([align, { label, icon }]) => {
|
|
361
|
+
const btn = document.createElement("button")
|
|
362
|
+
btn.type = "button"
|
|
363
|
+
btn.className = "inkpen-table__control-btn"
|
|
364
|
+
btn.title = `Align ${label}`
|
|
365
|
+
btn.textContent = icon
|
|
366
|
+
|
|
367
|
+
btn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
368
|
+
btn.addEventListener("click", () => {
|
|
369
|
+
editor.chain().focus().setCellAlignment(align).run()
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
alignGroup.appendChild(btn)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
controls.appendChild(alignGroup)
|
|
376
|
+
|
|
377
|
+
// Divider
|
|
378
|
+
const divider1 = document.createElement("span")
|
|
379
|
+
divider1.className = "inkpen-table__control-divider"
|
|
380
|
+
controls.appendChild(divider1)
|
|
381
|
+
|
|
382
|
+
// Variant dropdown
|
|
383
|
+
const variantBtn = document.createElement("button")
|
|
384
|
+
variantBtn.type = "button"
|
|
385
|
+
variantBtn.className = "inkpen-table__control-btn"
|
|
386
|
+
variantBtn.title = "Table Style"
|
|
387
|
+
variantBtn.textContent = "≡"
|
|
388
|
+
|
|
389
|
+
variantBtn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
390
|
+
variantBtn.addEventListener("click", () => {
|
|
391
|
+
this.showVariantDropdown(variantBtn, node, editor, getPos)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
controls.appendChild(variantBtn)
|
|
395
|
+
|
|
396
|
+
// Cell color button
|
|
397
|
+
const colorBtn = document.createElement("button")
|
|
398
|
+
colorBtn.type = "button"
|
|
399
|
+
colorBtn.className = "inkpen-table__control-btn"
|
|
400
|
+
colorBtn.title = "Cell Color"
|
|
401
|
+
colorBtn.textContent = "🎨"
|
|
402
|
+
|
|
403
|
+
colorBtn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
404
|
+
colorBtn.addEventListener("click", () => {
|
|
405
|
+
this.showColorDropdown(colorBtn, editor)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
controls.appendChild(colorBtn)
|
|
409
|
+
|
|
410
|
+
// Divider
|
|
411
|
+
const divider2 = document.createElement("span")
|
|
412
|
+
divider2.className = "inkpen-table__control-divider"
|
|
413
|
+
controls.appendChild(divider2)
|
|
414
|
+
|
|
415
|
+
// Add row button
|
|
416
|
+
const addRowBtn = document.createElement("button")
|
|
417
|
+
addRowBtn.type = "button"
|
|
418
|
+
addRowBtn.className = "inkpen-table__control-btn"
|
|
419
|
+
addRowBtn.title = "Add Row Below"
|
|
420
|
+
addRowBtn.textContent = "+↓"
|
|
421
|
+
|
|
422
|
+
addRowBtn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
423
|
+
addRowBtn.addEventListener("click", () => {
|
|
424
|
+
editor.chain().focus().addRowAfter().run()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
controls.appendChild(addRowBtn)
|
|
428
|
+
|
|
429
|
+
// Add column button
|
|
430
|
+
const addColBtn = document.createElement("button")
|
|
431
|
+
addColBtn.type = "button"
|
|
432
|
+
addColBtn.className = "inkpen-table__control-btn"
|
|
433
|
+
addColBtn.title = "Add Column Right"
|
|
434
|
+
addColBtn.textContent = "+→"
|
|
435
|
+
|
|
436
|
+
addColBtn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
437
|
+
addColBtn.addEventListener("click", () => {
|
|
438
|
+
editor.chain().focus().addColumnAfter().run()
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
controls.appendChild(addColBtn)
|
|
442
|
+
|
|
443
|
+
return controls
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
// Private: Show variant selection dropdown
|
|
447
|
+
|
|
448
|
+
showVariantDropdown(anchor, node, editor, getPos) {
|
|
449
|
+
removeExistingDropdown()
|
|
450
|
+
|
|
451
|
+
const dropdown = document.createElement("div")
|
|
452
|
+
dropdown.className = "inkpen-table__dropdown"
|
|
453
|
+
|
|
454
|
+
Object.entries(TABLE_VARIANTS).forEach(([variant, { label, description }]) => {
|
|
455
|
+
const item = document.createElement("button")
|
|
456
|
+
item.type = "button"
|
|
457
|
+
item.className = "inkpen-table__dropdown-item"
|
|
458
|
+
if (node.attrs.variant === variant) {
|
|
459
|
+
item.classList.add("is-active")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
item.innerHTML = `
|
|
463
|
+
<span class="inkpen-table__dropdown-label">${label}</span>
|
|
464
|
+
<span class="inkpen-table__dropdown-desc">${description}</span>
|
|
465
|
+
`
|
|
466
|
+
|
|
467
|
+
item.addEventListener("mousedown", (e) => e.preventDefault())
|
|
468
|
+
item.addEventListener("click", () => {
|
|
469
|
+
editor.chain().focus().setTableVariant(variant).run()
|
|
470
|
+
dropdown.remove()
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
dropdown.appendChild(item)
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
// Sticky header toggle
|
|
477
|
+
const stickyItem = document.createElement("button")
|
|
478
|
+
stickyItem.type = "button"
|
|
479
|
+
stickyItem.className = "inkpen-table__dropdown-item"
|
|
480
|
+
if (node.attrs.stickyHeader) {
|
|
481
|
+
stickyItem.classList.add("is-active")
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
stickyItem.innerHTML = `
|
|
485
|
+
<span class="inkpen-table__dropdown-label">Sticky Header</span>
|
|
486
|
+
<span class="inkpen-table__dropdown-desc">Header stays visible on scroll</span>
|
|
487
|
+
`
|
|
488
|
+
|
|
489
|
+
stickyItem.addEventListener("mousedown", (e) => e.preventDefault())
|
|
490
|
+
stickyItem.addEventListener("click", () => {
|
|
491
|
+
editor.chain().focus().toggleStickyHeader().run()
|
|
492
|
+
dropdown.remove()
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
dropdown.appendChild(stickyItem)
|
|
496
|
+
|
|
497
|
+
positionDropdown(dropdown, anchor)
|
|
498
|
+
setupDropdownClose(dropdown, anchor)
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
// Private: Show color selection dropdown
|
|
502
|
+
|
|
503
|
+
showColorDropdown(anchor, editor) {
|
|
504
|
+
removeExistingDropdown()
|
|
505
|
+
|
|
506
|
+
const dropdown = document.createElement("div")
|
|
507
|
+
dropdown.className = "inkpen-table__dropdown inkpen-table__dropdown--colors"
|
|
508
|
+
|
|
509
|
+
// No color option
|
|
510
|
+
const clearItem = document.createElement("button")
|
|
511
|
+
clearItem.type = "button"
|
|
512
|
+
clearItem.className = "inkpen-table__color-btn inkpen-table__color-btn--clear"
|
|
513
|
+
clearItem.title = "No Color"
|
|
514
|
+
clearItem.innerHTML = "∅"
|
|
515
|
+
|
|
516
|
+
clearItem.addEventListener("mousedown", (e) => e.preventDefault())
|
|
517
|
+
clearItem.addEventListener("click", () => {
|
|
518
|
+
editor.chain().focus().clearCellBackground().run()
|
|
519
|
+
dropdown.remove()
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
dropdown.appendChild(clearItem)
|
|
523
|
+
|
|
524
|
+
// Color options
|
|
525
|
+
Object.entries(CELL_BACKGROUNDS).forEach(([key, { label }]) => {
|
|
526
|
+
const colorBtn = document.createElement("button")
|
|
527
|
+
colorBtn.type = "button"
|
|
528
|
+
colorBtn.className = `inkpen-table__color-btn inkpen-table__color-btn--${key}`
|
|
529
|
+
colorBtn.title = label
|
|
530
|
+
colorBtn.setAttribute("data-color", key)
|
|
531
|
+
|
|
532
|
+
colorBtn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
533
|
+
colorBtn.addEventListener("click", () => {
|
|
534
|
+
editor.chain().focus().setCellBackground(key).run()
|
|
535
|
+
dropdown.remove()
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
dropdown.appendChild(colorBtn)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
positionDropdown(dropdown, anchor)
|
|
542
|
+
setupDropdownClose(dropdown, anchor)
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// Helper: Find parent table node
|
|
547
|
+
|
|
548
|
+
function findParentTable(selection) {
|
|
549
|
+
const { $from } = selection
|
|
550
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
551
|
+
const node = $from.node(d)
|
|
552
|
+
if (node.type.name === "table") {
|
|
553
|
+
return { node, pos: $from.before(d) }
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return null
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Helper: Get all selected cell positions
|
|
560
|
+
|
|
561
|
+
function getSelectedCellPositions(state) {
|
|
562
|
+
const cells = []
|
|
563
|
+
const { selection, doc } = state
|
|
564
|
+
|
|
565
|
+
// Check for CellSelection (multiple cells selected)
|
|
566
|
+
if (selection.$anchorCell && selection.$headCell) {
|
|
567
|
+
const cellSelection = selection
|
|
568
|
+
const table = cellSelection.$anchorCell.node(-1)
|
|
569
|
+
const map = {}
|
|
570
|
+
|
|
571
|
+
doc.nodesBetween(
|
|
572
|
+
cellSelection.$anchorCell.start(-1),
|
|
573
|
+
cellSelection.$headCell.start(-1) + cellSelection.$headCell.parent.nodeSize,
|
|
574
|
+
(node, pos) => {
|
|
575
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
|
|
576
|
+
if (!map[pos]) {
|
|
577
|
+
map[pos] = true
|
|
578
|
+
cells.push({ node, pos })
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
} else {
|
|
584
|
+
// Single cell - find the cell we're in
|
|
585
|
+
const { $from } = selection
|
|
586
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
587
|
+
const node = $from.node(d)
|
|
588
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
|
|
589
|
+
cells.push({ node, pos: $from.before(d) })
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return cells
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Helper: Remove existing dropdown
|
|
599
|
+
|
|
600
|
+
function removeExistingDropdown() {
|
|
601
|
+
const existing = document.querySelector(".inkpen-table__dropdown")
|
|
602
|
+
if (existing) existing.remove()
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Helper: Position dropdown below anchor
|
|
606
|
+
|
|
607
|
+
function positionDropdown(dropdown, anchor) {
|
|
608
|
+
const rect = anchor.getBoundingClientRect()
|
|
609
|
+
dropdown.style.position = "fixed"
|
|
610
|
+
dropdown.style.left = `${rect.left}px`
|
|
611
|
+
dropdown.style.top = `${rect.bottom + 4}px`
|
|
612
|
+
dropdown.style.zIndex = "10000"
|
|
613
|
+
document.body.appendChild(dropdown)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Helper: Setup dropdown close handlers
|
|
617
|
+
|
|
618
|
+
function setupDropdownClose(dropdown, anchor) {
|
|
619
|
+
const closeHandler = (e) => {
|
|
620
|
+
if (!dropdown.contains(e.target) && !anchor.contains(e.target)) {
|
|
621
|
+
dropdown.remove()
|
|
622
|
+
document.removeEventListener("mousedown", closeHandler)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
setTimeout(() => {
|
|
627
|
+
document.addEventListener("mousedown", closeHandler)
|
|
628
|
+
}, 0)
|
|
629
|
+
|
|
630
|
+
const escHandler = (e) => {
|
|
631
|
+
if (e.key === "Escape") {
|
|
632
|
+
dropdown.remove()
|
|
633
|
+
document.removeEventListener("keydown", escHandler)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
document.addEventListener("keydown", escHandler)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export { CELL_BACKGROUNDS, TABLE_VARIANTS, ALIGNMENTS }
|
|
640
|
+
export default AdvancedTable
|