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,756 @@
|
|
|
1
|
+
# INKPEN Code Samples - CORRECTED (Stimulus Only, No Vue)
|
|
2
|
+
|
|
3
|
+
## Complete Code Examples - All Stimulus + Vanilla JavaScript
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## D. Custom Blocks (CORRECTED - No Vue)
|
|
8
|
+
|
|
9
|
+
### D1: Custom Block - Hero (Stimulus Implementation)
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# lib/inkpen/extensions/hero.rb
|
|
13
|
+
# frozen_string_literal: true
|
|
14
|
+
|
|
15
|
+
module Inkpen
|
|
16
|
+
module Extensions
|
|
17
|
+
class Hero < Base
|
|
18
|
+
def name
|
|
19
|
+
:hero
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def background_image_url
|
|
23
|
+
options[:background_image_url]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def headline
|
|
27
|
+
options[:headline]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subheadline
|
|
31
|
+
options[:subheadline]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cta_text
|
|
35
|
+
options[:cta_text]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cta_url
|
|
39
|
+
options[:cta_url]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def text_align
|
|
43
|
+
options.fetch(:text_align, "center")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dark_overlay?
|
|
47
|
+
options.fetch(:dark_overlay, false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_config
|
|
51
|
+
{
|
|
52
|
+
backgroundImageUrl: background_image_url,
|
|
53
|
+
headline: headline,
|
|
54
|
+
subheadline: subheadline,
|
|
55
|
+
ctaText: cta_text,
|
|
56
|
+
ctaUrl: cta_url,
|
|
57
|
+
textAlign: text_align,
|
|
58
|
+
darkOverlay: dark_overlay?
|
|
59
|
+
}.compact
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
JavaScript (Pure Stimulus - No Vue):
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// app/javascript/extensions/hero.js
|
|
70
|
+
// Pure Stimulus implementation for Hero block
|
|
71
|
+
|
|
72
|
+
import { Node } from "@tiptap/core"
|
|
73
|
+
|
|
74
|
+
export const HeroBlock = Node.create({
|
|
75
|
+
name: "hero",
|
|
76
|
+
group: "block",
|
|
77
|
+
atom: true,
|
|
78
|
+
draggable: true,
|
|
79
|
+
selectable: true,
|
|
80
|
+
|
|
81
|
+
addAttributes() {
|
|
82
|
+
return {
|
|
83
|
+
backgroundImageUrl: { default: null },
|
|
84
|
+
headline: { default: "Your Headline Here" },
|
|
85
|
+
subheadline: { default: "Add a subheadline" },
|
|
86
|
+
ctaText: { default: "Learn More" },
|
|
87
|
+
ctaUrl: { default: "#" },
|
|
88
|
+
textAlign: { default: "center" },
|
|
89
|
+
darkOverlay: { default: false }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
parseHTML() {
|
|
94
|
+
return [{ tag: "hero-block" }]
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
renderHTML({ HTMLAttributes }) {
|
|
98
|
+
return [
|
|
99
|
+
"div",
|
|
100
|
+
{ class: "hero-block", ...HTMLAttributes },
|
|
101
|
+
[
|
|
102
|
+
"style",
|
|
103
|
+
`
|
|
104
|
+
background-image: url('${HTMLAttributes.backgroundImageUrl || ""}');
|
|
105
|
+
text-align: ${HTMLAttributes.textAlign};
|
|
106
|
+
`
|
|
107
|
+
],
|
|
108
|
+
["div", { class: "hero-overlay" }],
|
|
109
|
+
[
|
|
110
|
+
"div",
|
|
111
|
+
{ class: "hero-content" },
|
|
112
|
+
["h1", { class: "hero-headline" }, HTMLAttributes.headline],
|
|
113
|
+
["p", { class: "hero-subheadline" }, HTMLAttributes.subheadline],
|
|
114
|
+
[
|
|
115
|
+
"a",
|
|
116
|
+
{ href: HTMLAttributes.ctaUrl, class: "hero-cta" },
|
|
117
|
+
HTMLAttributes.ctaText
|
|
118
|
+
]
|
|
119
|
+
]
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
addNodeView() {
|
|
124
|
+
return {
|
|
125
|
+
dom: this.createDOM(),
|
|
126
|
+
contentDOM: null,
|
|
127
|
+
update: (node, decorations, innerDecorations) => {
|
|
128
|
+
this.updateDOM(node)
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
createDOM() {
|
|
135
|
+
const dom = document.createElement("div")
|
|
136
|
+
dom.className = "hero-block"
|
|
137
|
+
dom.setAttribute("data-type", "hero")
|
|
138
|
+
return dom
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
updateDOM(node) {
|
|
142
|
+
// Update hero block with new attributes
|
|
143
|
+
const { backgroundImageUrl, headline, subheadline, ctaText, ctaUrl, textAlign, darkOverlay } = node.attrs
|
|
144
|
+
|
|
145
|
+
const heroContent = document.querySelector(".hero-content")
|
|
146
|
+
if (heroContent) {
|
|
147
|
+
heroContent.innerHTML = `
|
|
148
|
+
<h1 class="hero-headline">${headline}</h1>
|
|
149
|
+
<p class="hero-subheadline">${subheadline}</p>
|
|
150
|
+
<a href="${ctaUrl}" class="hero-cta">${ctaText}</a>
|
|
151
|
+
`
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## C. Integration Pattern (CORRECTED - Stimulus Only)
|
|
160
|
+
|
|
161
|
+
### C3: Stimulus Controller (Pure Stimulus - No Vue Dependencies)
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// app/javascript/controllers/inkpen/editor_controller.js
|
|
165
|
+
// Pure Stimulus - no Vue.js, no frameworks
|
|
166
|
+
|
|
167
|
+
import { Controller } from "@hotwired/stimulus"
|
|
168
|
+
import { Editor } from "@tiptap/core"
|
|
169
|
+
import StarterKit from "@tiptap/starter-kit"
|
|
170
|
+
import ExtensionsLoader from "../utils/extensions_loader"
|
|
171
|
+
|
|
172
|
+
export default class extends Controller {
|
|
173
|
+
static targets = ["editor", "content", "title", "autosaveIndicator"]
|
|
174
|
+
static values = { features: String, autosave: Boolean }
|
|
175
|
+
|
|
176
|
+
connect() {
|
|
177
|
+
console.log("Inkpen Editor Controller Connected")
|
|
178
|
+
this.initEditor()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async initEditor() {
|
|
182
|
+
try {
|
|
183
|
+
const config = await this.fetchExtensionsConfig()
|
|
184
|
+
const extensions = await ExtensionsLoader.load(config)
|
|
185
|
+
|
|
186
|
+
this.editor = new Editor({
|
|
187
|
+
element: this.editorTarget,
|
|
188
|
+
extensions: [
|
|
189
|
+
StarterKit.configure({
|
|
190
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] }
|
|
191
|
+
}),
|
|
192
|
+
...extensions
|
|
193
|
+
],
|
|
194
|
+
content: this.contentTarget.value || "",
|
|
195
|
+
onUpdate: ({ editor }) => {
|
|
196
|
+
this.contentTarget.value = editor.getHTML()
|
|
197
|
+
|
|
198
|
+
if (this.autosaveValue) {
|
|
199
|
+
this.debounce(() => this.autosave(), 1500)
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
onSelectionUpdate: ({ editor }) => {
|
|
203
|
+
this.updateToolbarState(editor)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
console.log("Editor initialized successfully")
|
|
208
|
+
this.attachEditorListeners()
|
|
209
|
+
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("Failed to initialize editor:", error)
|
|
212
|
+
this.showError("Failed to load editor: " + error.message)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async fetchExtensionsConfig() {
|
|
217
|
+
const response = await fetch(`/inkpen/extensions/${this.featuresValue}.json`)
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`Failed to fetch extensions: ${response.status}`)
|
|
220
|
+
}
|
|
221
|
+
return await response.json()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
updateToolbarState(editor) {
|
|
225
|
+
// Update toolbar button states based on editor state
|
|
226
|
+
document.querySelectorAll("[data-command]").forEach(btn => {
|
|
227
|
+
const command = btn.dataset.command
|
|
228
|
+
const isActive = this.isCommandActive(editor, command)
|
|
229
|
+
|
|
230
|
+
if (isActive) {
|
|
231
|
+
btn.classList.add("is-active")
|
|
232
|
+
} else {
|
|
233
|
+
btn.classList.remove("is-active")
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
isCommandActive(editor, command) {
|
|
239
|
+
const [name, attr] = command.split(":")
|
|
240
|
+
|
|
241
|
+
if (name === "bold") return editor.isActive("bold")
|
|
242
|
+
if (name === "italic") return editor.isActive("italic")
|
|
243
|
+
if (name === "underline") return editor.isActive("underline")
|
|
244
|
+
if (name === "strike") return editor.isActive("strike")
|
|
245
|
+
if (name === "heading") {
|
|
246
|
+
const level = parseInt(attr)
|
|
247
|
+
return editor.isActive("heading", { level })
|
|
248
|
+
}
|
|
249
|
+
if (name === "code") return editor.isActive("code")
|
|
250
|
+
if (name === "codeBlock") return editor.isActive("codeBlock")
|
|
251
|
+
if (name === "blockquote") return editor.isActive("blockquote")
|
|
252
|
+
if (name === "bulletList") return editor.isActive("bulletList")
|
|
253
|
+
if (name === "orderedList") return editor.isActive("orderedList")
|
|
254
|
+
if (name === "taskList") return editor.isActive("taskList")
|
|
255
|
+
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
autosave() {
|
|
260
|
+
if (!this.form) return
|
|
261
|
+
|
|
262
|
+
const formData = new FormData(this.form)
|
|
263
|
+
|
|
264
|
+
fetch(this.form.action, {
|
|
265
|
+
method: "PATCH",
|
|
266
|
+
body: formData,
|
|
267
|
+
headers: {
|
|
268
|
+
"X-CSRF-Token": this.csrfToken,
|
|
269
|
+
"Accept": "application/json"
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
.then(r => {
|
|
273
|
+
if (r.ok) {
|
|
274
|
+
this.showAutosaveIndicator()
|
|
275
|
+
} else {
|
|
276
|
+
console.warn("Autosave returned status:", r.status)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
.catch(e => console.error("Autosave failed:", e))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
showAutosaveIndicator() {
|
|
283
|
+
const indicator = this.autosaveIndicatorTarget
|
|
284
|
+
if (!indicator) return
|
|
285
|
+
|
|
286
|
+
indicator.style.display = "block"
|
|
287
|
+
indicator.textContent = "✓ Saved"
|
|
288
|
+
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
indicator.style.display = "none"
|
|
291
|
+
}, 2000)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
showError(message) {
|
|
295
|
+
const errorDiv = document.createElement("div")
|
|
296
|
+
errorDiv.className = "inkpen-error"
|
|
297
|
+
errorDiv.textContent = message
|
|
298
|
+
this.editorTarget.parentNode.insertBefore(errorDiv, this.editorTarget)
|
|
299
|
+
|
|
300
|
+
setTimeout(() => errorDiv.remove(), 5000)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
attachEditorListeners() {
|
|
304
|
+
// Attach any custom listeners here
|
|
305
|
+
this.editor.on("update", () => {
|
|
306
|
+
console.log("Editor content updated")
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
disconnect() {
|
|
311
|
+
if (this.editor) {
|
|
312
|
+
this.editor.destroy()
|
|
313
|
+
}
|
|
314
|
+
clearTimeout(this.debounceTimer)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
debounce(fn, delay) {
|
|
318
|
+
clearTimeout(this.debounceTimer)
|
|
319
|
+
this.debounceTimer = setTimeout(fn, delay)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get form() {
|
|
323
|
+
return this.element.closest("form")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
get csrfToken() {
|
|
327
|
+
return document.querySelector('meta[name="csrf-token"]')?.content || ""
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### C4: Extensions Loader (Pure Vanilla JavaScript - No Vue)
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// app/javascript/utils/extensions_loader.js
|
|
336
|
+
// Pure vanilla JavaScript - no framework dependencies
|
|
337
|
+
|
|
338
|
+
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight"
|
|
339
|
+
import { lowlight } from "lowlight"
|
|
340
|
+
import { TaskList } from "@tiptap/extension-task-list"
|
|
341
|
+
import { TaskItem } from "@tiptap/extension-task-item"
|
|
342
|
+
import { Table } from "@tiptap/extension-table"
|
|
343
|
+
import { TableRow } from "@tiptap/extension-table-row"
|
|
344
|
+
import { TableHeader } from "@tiptap/extension-table-header"
|
|
345
|
+
import { TableCell } from "@tiptap/extension-table-cell"
|
|
346
|
+
import { Mention } from "@tiptap/extension-mention"
|
|
347
|
+
import { SlashCommand } from "@tiptap/extension-slash-command"
|
|
348
|
+
import tippy from "tippy.js"
|
|
349
|
+
|
|
350
|
+
export default class ExtensionsLoader {
|
|
351
|
+
static async load(config) {
|
|
352
|
+
const extensions = []
|
|
353
|
+
|
|
354
|
+
for (const extConfig of config.extensions) {
|
|
355
|
+
const ext = await this.loadExtension(extConfig)
|
|
356
|
+
if (ext) {
|
|
357
|
+
if (Array.isArray(ext)) {
|
|
358
|
+
extensions.push(...ext)
|
|
359
|
+
} else {
|
|
360
|
+
extensions.push(ext)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return extensions
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
static async loadExtension(config) {
|
|
369
|
+
switch (config.name) {
|
|
370
|
+
case "forced_document":
|
|
371
|
+
return this.configureForcedDocument(config)
|
|
372
|
+
|
|
373
|
+
case "code_block_syntax":
|
|
374
|
+
return this.configureCodeBlock(config)
|
|
375
|
+
|
|
376
|
+
case "task_list":
|
|
377
|
+
return this.configureTaskList(config)
|
|
378
|
+
|
|
379
|
+
case "table":
|
|
380
|
+
return this.configureTable(config)
|
|
381
|
+
|
|
382
|
+
case "mention":
|
|
383
|
+
return this.configureMention(config)
|
|
384
|
+
|
|
385
|
+
case "slash_commands":
|
|
386
|
+
return this.configureSlashCommands(config)
|
|
387
|
+
|
|
388
|
+
default:
|
|
389
|
+
console.warn(`Unknown extension: ${config.name}`)
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
static configureForcedDocument(config) {
|
|
395
|
+
const ForcedDocument = require("@tiptap/extension-forced-document").default
|
|
396
|
+
|
|
397
|
+
return ForcedDocument.configure({
|
|
398
|
+
titleLevel: config.config.titleLevel,
|
|
399
|
+
titlePlaceholder: config.config.titlePlaceholder,
|
|
400
|
+
subtitle: config.config.subtitle,
|
|
401
|
+
subtitleLevel: config.config.subtitleLevel,
|
|
402
|
+
subtitlePlaceholder: config.config.subtitlePlaceholder,
|
|
403
|
+
allowDeletion: config.config.allowDeletion
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
static configureCodeBlock(config) {
|
|
408
|
+
const languages = {}
|
|
409
|
+
|
|
410
|
+
// Dynamically load language modules
|
|
411
|
+
for (const lang of config.config.languages) {
|
|
412
|
+
try {
|
|
413
|
+
languages[lang] = require(`highlight.js/lib/languages/${lang}`).default
|
|
414
|
+
} catch (e) {
|
|
415
|
+
console.warn(`Language not found: ${lang}`)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return CodeBlockLowlight.configure({
|
|
420
|
+
lowlight,
|
|
421
|
+
languages,
|
|
422
|
+
defaultLanguage: config.config.defaultLanguage || "javascript"
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
static configureTaskList(config) {
|
|
427
|
+
return [
|
|
428
|
+
TaskList.configure({
|
|
429
|
+
nested: config.config.nested,
|
|
430
|
+
textToggle: config.config.textToggle,
|
|
431
|
+
keyboardShortcut: config.config.keyboardShortcut
|
|
432
|
+
}),
|
|
433
|
+
TaskItem.configure({
|
|
434
|
+
nested: config.config.nested
|
|
435
|
+
})
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static configureTable(config) {
|
|
440
|
+
return [
|
|
441
|
+
Table.configure({
|
|
442
|
+
resizable: config.config.resizable,
|
|
443
|
+
handleWidth: 4,
|
|
444
|
+
cellMinWidth: config.config.cellMinWidth,
|
|
445
|
+
lastColumnResizable: true,
|
|
446
|
+
allowTableNodeSelection: false
|
|
447
|
+
}),
|
|
448
|
+
TableRow,
|
|
449
|
+
TableHeader,
|
|
450
|
+
TableCell
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
static configureMention(config) {
|
|
455
|
+
return Mention.configure({
|
|
456
|
+
HTMLAttributes: { class: "mention" },
|
|
457
|
+
|
|
458
|
+
suggestion: {
|
|
459
|
+
items: async ({ query }) => {
|
|
460
|
+
if (!query) return []
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const response = await fetch(
|
|
464
|
+
`${config.config.searchUrl}?query=${encodeURIComponent(query)}`
|
|
465
|
+
)
|
|
466
|
+
if (!response.ok) return []
|
|
467
|
+
return await response.json()
|
|
468
|
+
} catch (e) {
|
|
469
|
+
console.error("Mention search failed:", e)
|
|
470
|
+
return []
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
render: () => {
|
|
475
|
+
let mentionList = null
|
|
476
|
+
let popup = null
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
onStart: props => {
|
|
480
|
+
// Create mention list using vanilla JavaScript
|
|
481
|
+
mentionList = this.createMentionList(props)
|
|
482
|
+
|
|
483
|
+
popup = tippy("body", {
|
|
484
|
+
getReferenceClientRect: props.clientRect,
|
|
485
|
+
appendTo: () => document.body,
|
|
486
|
+
content: mentionList.element,
|
|
487
|
+
showOnCreate: true,
|
|
488
|
+
interactive: true,
|
|
489
|
+
trigger: "manual",
|
|
490
|
+
placement: "bottom-start"
|
|
491
|
+
})[0]
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
onUpdate(props) {
|
|
495
|
+
if (mentionList) {
|
|
496
|
+
mentionList.update(props)
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
onKeyDown(props) {
|
|
501
|
+
if (mentionList) {
|
|
502
|
+
return mentionList.onKeyDown(props)
|
|
503
|
+
}
|
|
504
|
+
return false
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
onExit() {
|
|
508
|
+
if (popup) popup.destroy()
|
|
509
|
+
if (mentionList) mentionList.destroy()
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
static createMentionList(props) {
|
|
518
|
+
const element = document.createElement("div")
|
|
519
|
+
element.className = "mention-list"
|
|
520
|
+
|
|
521
|
+
let selectedIndex = 0
|
|
522
|
+
|
|
523
|
+
const update = (newProps) => {
|
|
524
|
+
selectedIndex = 0
|
|
525
|
+
|
|
526
|
+
element.innerHTML = ""
|
|
527
|
+
|
|
528
|
+
if (newProps.items.length === 0) {
|
|
529
|
+
const emptyItem = document.createElement("div")
|
|
530
|
+
emptyItem.className = "mention-item empty"
|
|
531
|
+
emptyItem.textContent = "No users found"
|
|
532
|
+
element.appendChild(emptyItem)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
newProps.items.forEach((item, index) => {
|
|
537
|
+
const itemEl = document.createElement("div")
|
|
538
|
+
itemEl.className = "mention-item"
|
|
539
|
+
if (index === selectedIndex) itemEl.classList.add("is-selected")
|
|
540
|
+
|
|
541
|
+
itemEl.innerHTML = `
|
|
542
|
+
<div class="mention-item-avatar">${item.avatar ? `<img src="${item.avatar}" />` : "?"}</div>
|
|
543
|
+
<div class="mention-item-name">${item.label}</div>
|
|
544
|
+
`
|
|
545
|
+
|
|
546
|
+
itemEl.addEventListener("click", () => {
|
|
547
|
+
newProps.command({ id: item.id, label: item.label })
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
element.appendChild(itemEl)
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const onKeyDown = (event) => {
|
|
555
|
+
if (event.key === "ArrowUp") {
|
|
556
|
+
selectedIndex = (selectedIndex - 1 + element.children.length) % element.children.length
|
|
557
|
+
updateSelection()
|
|
558
|
+
return true
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (event.key === "ArrowDown") {
|
|
562
|
+
selectedIndex = (selectedIndex + 1) % element.children.length
|
|
563
|
+
updateSelection()
|
|
564
|
+
return true
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (event.key === "Enter") {
|
|
568
|
+
const selectedItem = element.children[selectedIndex]
|
|
569
|
+
if (selectedItem) {
|
|
570
|
+
selectedItem.click()
|
|
571
|
+
}
|
|
572
|
+
return true
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return false
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const updateSelection = () => {
|
|
579
|
+
element.querySelectorAll(".mention-item").forEach((item, index) => {
|
|
580
|
+
if (index === selectedIndex) {
|
|
581
|
+
item.classList.add("is-selected")
|
|
582
|
+
item.scrollIntoView({ block: "nearest" })
|
|
583
|
+
} else {
|
|
584
|
+
item.classList.remove("is-selected")
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const destroy = () => {
|
|
590
|
+
element.remove()
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
update(props)
|
|
594
|
+
|
|
595
|
+
return { element, update, onKeyDown, destroy }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
static configureSlashCommands(config) {
|
|
599
|
+
const commands = config.config.commands
|
|
600
|
+
|
|
601
|
+
return SlashCommand.configure({
|
|
602
|
+
suggestion: {
|
|
603
|
+
items: ({ query }) => {
|
|
604
|
+
return commands
|
|
605
|
+
.filter(cmd =>
|
|
606
|
+
cmd.label.toLowerCase().startsWith(query.toLowerCase()) ||
|
|
607
|
+
cmd.description.toLowerCase().includes(query.toLowerCase())
|
|
608
|
+
)
|
|
609
|
+
.slice(0, config.config.maxSuggestions)
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
render: () => {
|
|
613
|
+
let commandList = null
|
|
614
|
+
let popup = null
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
onStart: props => {
|
|
618
|
+
// Create command palette using vanilla JavaScript
|
|
619
|
+
commandList = this.createCommandPalette(props)
|
|
620
|
+
|
|
621
|
+
popup = tippy("body", {
|
|
622
|
+
getReferenceClientRect: props.clientRect,
|
|
623
|
+
appendTo: () => document.body,
|
|
624
|
+
content: commandList.element,
|
|
625
|
+
showOnCreate: true,
|
|
626
|
+
interactive: true,
|
|
627
|
+
trigger: "manual",
|
|
628
|
+
placement: "bottom-start"
|
|
629
|
+
})[0]
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
onUpdate(props) {
|
|
633
|
+
if (commandList) {
|
|
634
|
+
commandList.update(props)
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
onKeyDown(props) {
|
|
639
|
+
if (commandList) {
|
|
640
|
+
return commandList.onKeyDown(props)
|
|
641
|
+
}
|
|
642
|
+
return false
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
onExit() {
|
|
646
|
+
if (popup) popup.destroy()
|
|
647
|
+
if (commandList) commandList.destroy()
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static createCommandPalette(props) {
|
|
656
|
+
const element = document.createElement("div")
|
|
657
|
+
element.className = "command-palette"
|
|
658
|
+
|
|
659
|
+
let selectedIndex = 0
|
|
660
|
+
let items = []
|
|
661
|
+
|
|
662
|
+
const update = (newProps) => {
|
|
663
|
+
selectedIndex = 0
|
|
664
|
+
items = newProps.items
|
|
665
|
+
|
|
666
|
+
element.innerHTML = ""
|
|
667
|
+
|
|
668
|
+
if (items.length === 0) {
|
|
669
|
+
const emptyItem = document.createElement("div")
|
|
670
|
+
emptyItem.className = "command-item empty"
|
|
671
|
+
emptyItem.textContent = "No commands found"
|
|
672
|
+
element.appendChild(emptyItem)
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
items.forEach((cmd, index) => {
|
|
677
|
+
const itemEl = document.createElement("div")
|
|
678
|
+
itemEl.className = "command-item"
|
|
679
|
+
if (index === selectedIndex) itemEl.classList.add("is-selected")
|
|
680
|
+
|
|
681
|
+
itemEl.innerHTML = `
|
|
682
|
+
<div class="command-icon">${cmd.icon ? `📝` : "•"}</div>
|
|
683
|
+
<div class="command-info">
|
|
684
|
+
<div class="command-name">${cmd.label}</div>
|
|
685
|
+
<div class="command-desc">${cmd.description}</div>
|
|
686
|
+
</div>
|
|
687
|
+
${cmd.shortcut ? `<div class="command-shortcut">${cmd.shortcut}</div>` : ""}
|
|
688
|
+
`
|
|
689
|
+
|
|
690
|
+
itemEl.addEventListener("click", () => {
|
|
691
|
+
newProps.command({ name: cmd.name })
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
element.appendChild(itemEl)
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const onKeyDown = (event) => {
|
|
699
|
+
if (event.key === "ArrowUp") {
|
|
700
|
+
selectedIndex = (selectedIndex - 1 + element.children.length) % element.children.length
|
|
701
|
+
updateSelection()
|
|
702
|
+
return true
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (event.key === "ArrowDown") {
|
|
706
|
+
selectedIndex = (selectedIndex + 1) % element.children.length
|
|
707
|
+
updateSelection()
|
|
708
|
+
return true
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (event.key === "Enter") {
|
|
712
|
+
const selectedItem = element.children[selectedIndex]
|
|
713
|
+
if (selectedItem && !selectedItem.classList.contains("empty")) {
|
|
714
|
+
selectedItem.click()
|
|
715
|
+
}
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return false
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const updateSelection = () => {
|
|
723
|
+
element.querySelectorAll(".command-item").forEach((item, index) => {
|
|
724
|
+
if (index === selectedIndex) {
|
|
725
|
+
item.classList.add("is-selected")
|
|
726
|
+
item.scrollIntoView({ block: "nearest" })
|
|
727
|
+
} else {
|
|
728
|
+
item.classList.remove("is-selected")
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const destroy = () => {
|
|
734
|
+
element.remove()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
update(props)
|
|
738
|
+
|
|
739
|
+
return { element, update, onKeyDown, destroy }
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## Key Changes Made:
|
|
747
|
+
|
|
748
|
+
✅ **Removed all Vue.js references**
|
|
749
|
+
✅ **Removed VueNodeViewRenderer**
|
|
750
|
+
✅ **Removed Vue component files**
|
|
751
|
+
✅ **Using pure vanilla JavaScript for dropdowns**
|
|
752
|
+
✅ **Using Stimulus for state management**
|
|
753
|
+
✅ **Custom list components using DOM API**
|
|
754
|
+
✅ **All interactive elements built with vanilla JS**
|
|
755
|
+
|
|
756
|
+
You were 100% correct - this is **pure Stimulus + vanilla JavaScript throughout**. No Vue, no component frameworks. Everything uses vanilla DOM manipulation.
|