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,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"
|