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,593 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state"
3
+
4
+ /**
5
+ * File Attachment Extension for TipTap
6
+ *
7
+ * Upload and display file attachments with:
8
+ * - Drag & drop file upload
9
+ * - File type icons (PDF, Word, Excel, ZIP, etc.)
10
+ * - File size display
11
+ * - Download button
12
+ * - Upload progress indicator
13
+ * - Configurable upload endpoint
14
+ *
15
+ * @since 0.5.0
16
+ */
17
+
18
+ const FILE_ATTACHMENT_KEY = new PluginKey("fileAttachment")
19
+
20
+ // File type configurations with icons and colors
21
+ const FILE_TYPES = {
22
+ // Documents
23
+ pdf: { icon: "📄", label: "PDF", color: "#dc2626" },
24
+ doc: { icon: "📝", label: "Word", color: "#2563eb" },
25
+ docx: { icon: "📝", label: "Word", color: "#2563eb" },
26
+ txt: { icon: "📃", label: "Text", color: "#6b7280" },
27
+ rtf: { icon: "📃", label: "RTF", color: "#6b7280" },
28
+ odt: { icon: "📝", label: "ODT", color: "#2563eb" },
29
+
30
+ // Spreadsheets
31
+ xls: { icon: "📊", label: "Excel", color: "#16a34a" },
32
+ xlsx: { icon: "📊", label: "Excel", color: "#16a34a" },
33
+ csv: { icon: "📊", label: "CSV", color: "#16a34a" },
34
+ ods: { icon: "📊", label: "ODS", color: "#16a34a" },
35
+
36
+ // Presentations
37
+ ppt: { icon: "📽", label: "PowerPoint", color: "#ea580c" },
38
+ pptx: { icon: "📽", label: "PowerPoint", color: "#ea580c" },
39
+ odp: { icon: "📽", label: "ODP", color: "#ea580c" },
40
+
41
+ // Archives
42
+ zip: { icon: "📦", label: "ZIP", color: "#854d0e" },
43
+ rar: { icon: "📦", label: "RAR", color: "#854d0e" },
44
+ "7z": { icon: "📦", label: "7Z", color: "#854d0e" },
45
+ tar: { icon: "📦", label: "TAR", color: "#854d0e" },
46
+ gz: { icon: "📦", label: "GZ", color: "#854d0e" },
47
+
48
+ // Audio
49
+ mp3: { icon: "🎵", label: "MP3", color: "#7c3aed" },
50
+ wav: { icon: "🎵", label: "WAV", color: "#7c3aed" },
51
+ ogg: { icon: "🎵", label: "OGG", color: "#7c3aed" },
52
+ flac: { icon: "🎵", label: "FLAC", color: "#7c3aed" },
53
+ m4a: { icon: "🎵", label: "M4A", color: "#7c3aed" },
54
+
55
+ // Video
56
+ mp4: { icon: "🎬", label: "MP4", color: "#db2777" },
57
+ mov: { icon: "🎬", label: "MOV", color: "#db2777" },
58
+ avi: { icon: "🎬", label: "AVI", color: "#db2777" },
59
+ mkv: { icon: "🎬", label: "MKV", color: "#db2777" },
60
+ webm: { icon: "🎬", label: "WebM", color: "#db2777" },
61
+
62
+ // Images (fallback, usually handled by image extension)
63
+ png: { icon: "🖼", label: "PNG", color: "#0891b2" },
64
+ jpg: { icon: "🖼", label: "JPG", color: "#0891b2" },
65
+ jpeg: { icon: "🖼", label: "JPEG", color: "#0891b2" },
66
+ gif: { icon: "🖼", label: "GIF", color: "#0891b2" },
67
+ svg: { icon: "🖼", label: "SVG", color: "#0891b2" },
68
+ webp: { icon: "🖼", label: "WebP", color: "#0891b2" },
69
+
70
+ // Code
71
+ js: { icon: "⚡", label: "JavaScript", color: "#eab308" },
72
+ ts: { icon: "⚡", label: "TypeScript", color: "#3b82f6" },
73
+ py: { icon: "🐍", label: "Python", color: "#22c55e" },
74
+ rb: { icon: "💎", label: "Ruby", color: "#dc2626" },
75
+ json: { icon: "{}", label: "JSON", color: "#6b7280" },
76
+ xml: { icon: "<>", label: "XML", color: "#6b7280" },
77
+ html: { icon: "🌐", label: "HTML", color: "#ea580c" },
78
+ css: { icon: "🎨", label: "CSS", color: "#3b82f6" },
79
+
80
+ // Default
81
+ default: { icon: "📎", label: "File", color: "#6b7280" }
82
+ }
83
+
84
+ // Format file size for display
85
+ function formatFileSize(bytes) {
86
+ if (bytes === 0) return "0 B"
87
+ const k = 1024
88
+ const sizes = ["B", "KB", "MB", "GB"]
89
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
90
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
91
+ }
92
+
93
+ // Get file extension from filename
94
+ function getFileExtension(filename) {
95
+ return filename?.split(".").pop()?.toLowerCase() || ""
96
+ }
97
+
98
+ // Get file type config
99
+ function getFileType(filename) {
100
+ const ext = getFileExtension(filename)
101
+ return FILE_TYPES[ext] || FILE_TYPES.default
102
+ }
103
+
104
+ export const FileAttachment = Node.create({
105
+ name: "fileAttachment",
106
+
107
+ group: "block",
108
+
109
+ atom: true,
110
+
111
+ draggable: true,
112
+
113
+ addOptions() {
114
+ return {
115
+ // Upload configuration
116
+ uploadUrl: null, // If null, uses base64 data URLs
117
+ uploadHeaders: {},
118
+ uploadFieldName: "file",
119
+ // File restrictions
120
+ allowedTypes: null, // null = all, or ["application/pdf", "image/*"]
121
+ maxSize: 10 * 1024 * 1024, // 10MB default
122
+ // Callbacks
123
+ onUploadStart: null,
124
+ onUploadProgress: null,
125
+ onUploadComplete: null,
126
+ onUploadError: null,
127
+ // Custom upload handler (overrides default)
128
+ uploadHandler: null
129
+ }
130
+ },
131
+
132
+ addAttributes() {
133
+ return {
134
+ url: {
135
+ default: null
136
+ },
137
+ filename: {
138
+ default: null
139
+ },
140
+ filesize: {
141
+ default: null
142
+ },
143
+ filetype: {
144
+ default: null
145
+ },
146
+ uploadProgress: {
147
+ default: null // null = complete, 0-100 = uploading
148
+ },
149
+ uploadError: {
150
+ default: null
151
+ }
152
+ }
153
+ },
154
+
155
+ parseHTML() {
156
+ return [
157
+ {
158
+ tag: 'div[data-type="file-attachment"]'
159
+ }
160
+ ]
161
+ },
162
+
163
+ renderHTML({ HTMLAttributes, node }) {
164
+ const fileType = getFileType(node.attrs.filename)
165
+
166
+ return [
167
+ "div",
168
+ mergeAttributes(HTMLAttributes, {
169
+ "data-type": "file-attachment",
170
+ class: "inkpen-file"
171
+ }),
172
+ [
173
+ "a",
174
+ {
175
+ href: node.attrs.url,
176
+ download: node.attrs.filename,
177
+ target: "_blank",
178
+ rel: "noopener noreferrer",
179
+ class: "inkpen-file__link"
180
+ },
181
+ [
182
+ "span",
183
+ { class: "inkpen-file__icon", style: `color: ${fileType.color}` },
184
+ fileType.icon
185
+ ],
186
+ [
187
+ "span",
188
+ { class: "inkpen-file__info" },
189
+ [
190
+ "span",
191
+ { class: "inkpen-file__name" },
192
+ node.attrs.filename || "Unknown file"
193
+ ],
194
+ [
195
+ "span",
196
+ { class: "inkpen-file__meta" },
197
+ `${fileType.label} · ${formatFileSize(node.attrs.filesize || 0)}`
198
+ ]
199
+ ],
200
+ [
201
+ "span",
202
+ { class: "inkpen-file__download" },
203
+ "↓"
204
+ ]
205
+ ]
206
+ ]
207
+ },
208
+
209
+ addNodeView() {
210
+ return ({ node, editor, getPos }) => {
211
+ const extension = this
212
+
213
+ // Create container
214
+ const container = document.createElement("div")
215
+ container.className = "inkpen-file"
216
+ container.setAttribute("data-type", "file-attachment")
217
+
218
+ // Build the file card
219
+ const buildCard = () => {
220
+ const fileType = getFileType(node.attrs.filename)
221
+ const isUploading = node.attrs.uploadProgress !== null && node.attrs.uploadProgress < 100
222
+ const hasError = node.attrs.uploadError !== null
223
+
224
+ container.innerHTML = ""
225
+
226
+ if (hasError) {
227
+ // Error state
228
+ container.innerHTML = `
229
+ <div class="inkpen-file__error">
230
+ <span class="inkpen-file__icon">⚠️</span>
231
+ <span class="inkpen-file__info">
232
+ <span class="inkpen-file__name">${node.attrs.filename || "Upload failed"}</span>
233
+ <span class="inkpen-file__meta inkpen-file__meta--error">${node.attrs.uploadError}</span>
234
+ </span>
235
+ <button type="button" class="inkpen-file__retry" title="Retry upload">↻</button>
236
+ <button type="button" class="inkpen-file__remove" title="Remove">×</button>
237
+ </div>
238
+ `
239
+
240
+ container.querySelector(".inkpen-file__retry")?.addEventListener("click", () => {
241
+ // Would need to store the original file to retry
242
+ // For now, just remove
243
+ })
244
+
245
+ container.querySelector(".inkpen-file__remove")?.addEventListener("click", () => {
246
+ if (typeof getPos === "function") {
247
+ editor.chain().focus().deleteRange({
248
+ from: getPos(),
249
+ to: getPos() + node.nodeSize
250
+ }).run()
251
+ }
252
+ })
253
+ } else if (isUploading) {
254
+ // Uploading state
255
+ container.innerHTML = `
256
+ <div class="inkpen-file__uploading">
257
+ <span class="inkpen-file__icon" style="color: ${fileType.color}">${fileType.icon}</span>
258
+ <span class="inkpen-file__info">
259
+ <span class="inkpen-file__name">${node.attrs.filename}</span>
260
+ <span class="inkpen-file__meta">Uploading... ${Math.round(node.attrs.uploadProgress)}%</span>
261
+ </span>
262
+ <div class="inkpen-file__progress">
263
+ <div class="inkpen-file__progress-bar" style="width: ${node.attrs.uploadProgress}%"></div>
264
+ </div>
265
+ </div>
266
+ `
267
+ } else {
268
+ // Complete state
269
+ const link = document.createElement("a")
270
+ link.href = node.attrs.url || "#"
271
+ link.download = node.attrs.filename
272
+ link.target = "_blank"
273
+ link.rel = "noopener noreferrer"
274
+ link.className = "inkpen-file__link"
275
+
276
+ link.innerHTML = `
277
+ <span class="inkpen-file__icon" style="color: ${fileType.color}">${fileType.icon}</span>
278
+ <span class="inkpen-file__info">
279
+ <span class="inkpen-file__name">${node.attrs.filename || "Unknown file"}</span>
280
+ <span class="inkpen-file__meta">${fileType.label} · ${formatFileSize(node.attrs.filesize || 0)}</span>
281
+ </span>
282
+ <span class="inkpen-file__download">
283
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
284
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
285
+ <polyline points="7 10 12 15 17 10"></polyline>
286
+ <line x1="12" y1="15" x2="12" y2="3"></line>
287
+ </svg>
288
+ </span>
289
+ `
290
+
291
+ container.appendChild(link)
292
+ }
293
+ }
294
+
295
+ buildCard()
296
+
297
+ return {
298
+ dom: container,
299
+ update: (updatedNode) => {
300
+ if (updatedNode.type.name !== "fileAttachment") return false
301
+
302
+ node = updatedNode
303
+ buildCard()
304
+ return true
305
+ },
306
+ selectNode: () => {
307
+ container.classList.add("is-selected")
308
+ },
309
+ deselectNode: () => {
310
+ container.classList.remove("is-selected")
311
+ }
312
+ }
313
+ }
314
+ },
315
+
316
+ addCommands() {
317
+ return {
318
+ // Insert a file attachment with URL
319
+ insertFileAttachment: (attrs) => ({ commands }) => {
320
+ return commands.insertContent({
321
+ type: this.name,
322
+ attrs
323
+ })
324
+ },
325
+
326
+ // Upload a file
327
+ uploadFile: (file) => ({ editor, commands }) => {
328
+ const extension = this
329
+
330
+ // Validate file type
331
+ if (extension.options.allowedTypes) {
332
+ const allowed = extension.options.allowedTypes.some(type => {
333
+ if (type.endsWith("/*")) {
334
+ return file.type.startsWith(type.replace("/*", ""))
335
+ }
336
+ return file.type === type
337
+ })
338
+
339
+ if (!allowed) {
340
+ extension.options.onUploadError?.({
341
+ file,
342
+ error: `File type ${file.type} not allowed`
343
+ })
344
+ return false
345
+ }
346
+ }
347
+
348
+ // Validate file size
349
+ if (extension.options.maxSize && file.size > extension.options.maxSize) {
350
+ extension.options.onUploadError?.({
351
+ file,
352
+ error: `File too large. Max size: ${formatFileSize(extension.options.maxSize)}`
353
+ })
354
+ return false
355
+ }
356
+
357
+ // Insert placeholder node
358
+ const placeholderAttrs = {
359
+ filename: file.name,
360
+ filesize: file.size,
361
+ filetype: file.type,
362
+ uploadProgress: 0
363
+ }
364
+
365
+ commands.insertContent({
366
+ type: this.name,
367
+ attrs: placeholderAttrs
368
+ })
369
+
370
+ // Get the position of the inserted node
371
+ const { state } = editor
372
+ const pos = state.selection.from - 1
373
+
374
+ // Custom upload handler
375
+ if (extension.options.uploadHandler) {
376
+ extension.options.uploadHandler({
377
+ file,
378
+ onProgress: (progress) => {
379
+ updateNodeAttr(editor, pos, { uploadProgress: progress })
380
+ },
381
+ onComplete: (url) => {
382
+ updateNodeAttr(editor, pos, {
383
+ url,
384
+ uploadProgress: null
385
+ })
386
+ extension.options.onUploadComplete?.({ file, url })
387
+ },
388
+ onError: (error) => {
389
+ updateNodeAttr(editor, pos, {
390
+ uploadProgress: null,
391
+ uploadError: error
392
+ })
393
+ extension.options.onUploadError?.({ file, error })
394
+ }
395
+ })
396
+ return true
397
+ }
398
+
399
+ // Default: upload to URL or use base64
400
+ if (extension.options.uploadUrl) {
401
+ uploadToServer(file, extension.options, {
402
+ onProgress: (progress) => {
403
+ updateNodeAttr(editor, pos, { uploadProgress: progress })
404
+ },
405
+ onComplete: (url) => {
406
+ updateNodeAttr(editor, pos, {
407
+ url,
408
+ uploadProgress: null
409
+ })
410
+ extension.options.onUploadComplete?.({ file, url })
411
+ },
412
+ onError: (error) => {
413
+ updateNodeAttr(editor, pos, {
414
+ uploadProgress: null,
415
+ uploadError: error
416
+ })
417
+ extension.options.onUploadError?.({ file, error })
418
+ }
419
+ })
420
+ } else {
421
+ // Use base64 data URL
422
+ const reader = new FileReader()
423
+ reader.onprogress = (e) => {
424
+ if (e.lengthComputable) {
425
+ const progress = (e.loaded / e.total) * 100
426
+ updateNodeAttr(editor, pos, { uploadProgress: progress })
427
+ }
428
+ }
429
+ reader.onload = (e) => {
430
+ const url = e.target?.result
431
+ updateNodeAttr(editor, pos, {
432
+ url,
433
+ uploadProgress: null
434
+ })
435
+ extension.options.onUploadComplete?.({ file, url })
436
+ }
437
+ reader.onerror = () => {
438
+ updateNodeAttr(editor, pos, {
439
+ uploadProgress: null,
440
+ uploadError: "Failed to read file"
441
+ })
442
+ extension.options.onUploadError?.({ file, error: "Failed to read file" })
443
+ }
444
+ reader.readAsDataURL(file)
445
+ }
446
+
447
+ extension.options.onUploadStart?.({ file })
448
+ return true
449
+ }
450
+ }
451
+ },
452
+
453
+ addKeyboardShortcuts() {
454
+ return {
455
+ Backspace: ({ editor }) => {
456
+ const { selection } = editor.state
457
+ const node = selection.node
458
+
459
+ if (node?.type.name === this.name) {
460
+ return editor.commands.deleteSelection()
461
+ }
462
+ return false
463
+ },
464
+ Delete: ({ editor }) => {
465
+ const { selection } = editor.state
466
+ const node = selection.node
467
+
468
+ if (node?.type.name === this.name) {
469
+ return editor.commands.deleteSelection()
470
+ }
471
+ return false
472
+ }
473
+ }
474
+ },
475
+
476
+ addProseMirrorPlugins() {
477
+ const extension = this
478
+
479
+ return [
480
+ new Plugin({
481
+ key: FILE_ATTACHMENT_KEY,
482
+ props: {
483
+ handleDrop(view, event) {
484
+ const files = event.dataTransfer?.files
485
+ if (!files?.length) return false
486
+
487
+ // Filter out images (let image extension handle those)
488
+ const nonImageFiles = Array.from(files).filter(
489
+ file => !file.type.startsWith("image/")
490
+ )
491
+
492
+ if (!nonImageFiles.length) return false
493
+
494
+ event.preventDefault()
495
+
496
+ const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
497
+ if (!pos) return false
498
+
499
+ // Focus editor and set selection
500
+ view.focus()
501
+
502
+ nonImageFiles.forEach(file => {
503
+ extension.editor.commands.uploadFile(file)
504
+ })
505
+
506
+ return true
507
+ },
508
+
509
+ handlePaste(view, event) {
510
+ const files = event.clipboardData?.files
511
+ if (!files?.length) return false
512
+
513
+ // Filter out images
514
+ const nonImageFiles = Array.from(files).filter(
515
+ file => !file.type.startsWith("image/")
516
+ )
517
+
518
+ if (!nonImageFiles.length) return false
519
+
520
+ event.preventDefault()
521
+
522
+ nonImageFiles.forEach(file => {
523
+ extension.editor.commands.uploadFile(file)
524
+ })
525
+
526
+ return true
527
+ }
528
+ }
529
+ })
530
+ ]
531
+ }
532
+ })
533
+
534
+ // Helper: Update node attribute at position
535
+ function updateNodeAttr(editor, pos, attrs) {
536
+ const { state } = editor
537
+ const node = state.doc.nodeAt(pos)
538
+
539
+ if (node && node.type.name === "fileAttachment") {
540
+ editor.view.dispatch(
541
+ state.tr.setNodeMarkup(pos, null, { ...node.attrs, ...attrs })
542
+ )
543
+ }
544
+ }
545
+
546
+ // Helper: Upload file to server
547
+ function uploadToServer(file, options, callbacks) {
548
+ const xhr = new XMLHttpRequest()
549
+
550
+ xhr.upload.addEventListener("progress", (e) => {
551
+ if (e.lengthComputable) {
552
+ const progress = (e.loaded / e.total) * 100
553
+ callbacks.onProgress(progress)
554
+ }
555
+ })
556
+
557
+ xhr.addEventListener("load", () => {
558
+ if (xhr.status >= 200 && xhr.status < 300) {
559
+ try {
560
+ const response = JSON.parse(xhr.responseText)
561
+ const url = response.url || response.file_url || response.path
562
+ if (url) {
563
+ callbacks.onComplete(url)
564
+ } else {
565
+ callbacks.onError("Invalid server response")
566
+ }
567
+ } catch (e) {
568
+ callbacks.onError("Failed to parse server response")
569
+ }
570
+ } else {
571
+ callbacks.onError(`Upload failed: ${xhr.status}`)
572
+ }
573
+ })
574
+
575
+ xhr.addEventListener("error", () => {
576
+ callbacks.onError("Network error during upload")
577
+ })
578
+
579
+ xhr.open("POST", options.uploadUrl)
580
+
581
+ // Set headers
582
+ Object.entries(options.uploadHeaders || {}).forEach(([key, value]) => {
583
+ xhr.setRequestHeader(key, value)
584
+ })
585
+
586
+ // Create form data
587
+ const formData = new FormData()
588
+ formData.append(options.uploadFieldName || "file", file)
589
+
590
+ xhr.send(formData)
591
+ }
592
+
593
+ export default FileAttachment
@@ -0,0 +1,58 @@
1
+ /**
2
+ * InkpenTable Extension
3
+ *
4
+ * Unified enhanced table extension with Notion/Airtable-style features.
5
+ * Replaces both basic table and advanced_table extensions.
6
+ *
7
+ * @since 0.8.0
8
+ *
9
+ * @example
10
+ * import { InkpenTable, InkpenTableCell, InkpenTableHeader, InkpenTableRow } from "inkpen/extensions/inkpen_table"
11
+ *
12
+ * const editor = new Editor({
13
+ * extensions: [
14
+ * InkpenTable.configure({
15
+ * showHandles: true,
16
+ * showAddButtons: true,
17
+ * showCaption: true
18
+ * }),
19
+ * InkpenTableRow,
20
+ * InkpenTableCell,
21
+ * InkpenTableHeader
22
+ * ]
23
+ * })
24
+ */
25
+
26
+ export { InkpenTable, InkpenTableRow } from "inkpen/extensions/inkpen_table/inkpen_table"
27
+ export { InkpenTableCell } from "inkpen/extensions/inkpen_table/inkpen_table_cell"
28
+ export { InkpenTableHeader } from "inkpen/extensions/inkpen_table/inkpen_table_header"
29
+ export { TableMenu } from "inkpen/extensions/inkpen_table/table_menu"
30
+
31
+ // Re-export constants for customization
32
+ export {
33
+ TEXT_COLORS,
34
+ BACKGROUND_COLORS,
35
+ TABLE_VARIANTS,
36
+ ROW_MENU_ITEMS,
37
+ COLUMN_MENU_ITEMS,
38
+ ALIGNMENT_OPTIONS,
39
+ DEFAULT_CONFIG,
40
+ CSS_CLASSES
41
+ } from "inkpen/extensions/inkpen_table/table_constants"
42
+
43
+ // Re-export helpers for advanced usage
44
+ export {
45
+ createElement,
46
+ nextFrame,
47
+ waitFrames,
48
+ positionBelow,
49
+ positionRight,
50
+ getTableDimensions,
51
+ getRowIndex,
52
+ getColumnIndex,
53
+ findTableWrapper,
54
+ findTableElement,
55
+ stopEvent,
56
+ onClickOutside,
57
+ onEscapeKey
58
+ } from "inkpen/extensions/inkpen_table/table_helpers"