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,693 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inkpen Toolbar Controller
|
|
5
|
+
*
|
|
6
|
+
* Context-aware floating toolbar that adapts its buttons based on the current selection.
|
|
7
|
+
* Shows different actions when selecting text vs. inside a table vs. on an image.
|
|
8
|
+
*
|
|
9
|
+
* Visibility and positioning are handled by TipTap's BubbleMenu extension.
|
|
10
|
+
*
|
|
11
|
+
* Important: BubbleMenu uses Tippy.js which moves the toolbar element to the
|
|
12
|
+
* document body. This triggers Stimulus to disconnect and reconnect the
|
|
13
|
+
* controller. We store the editor element ID on the DOM element itself
|
|
14
|
+
* so we can find it after reconnection.
|
|
15
|
+
*
|
|
16
|
+
* Context Types:
|
|
17
|
+
* - text: Regular text selection (formatting options)
|
|
18
|
+
* - table: Inside a table cell (table operations)
|
|
19
|
+
* - image: Image selected (alignment, delete)
|
|
20
|
+
* - code: Inside code block (language selector)
|
|
21
|
+
*/
|
|
22
|
+
export default class extends Controller {
|
|
23
|
+
static targets = ["button", "contextMenu"]
|
|
24
|
+
|
|
25
|
+
static values = {
|
|
26
|
+
buttons: { type: Array, default: [] },
|
|
27
|
+
contextAware: { type: Boolean, default: true }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
connect() {
|
|
31
|
+
this.editorController = null
|
|
32
|
+
this.currentContext = "text"
|
|
33
|
+
this.modeChangeHandler = null
|
|
34
|
+
|
|
35
|
+
// Store editor element ID on the DOM element before BubbleMenu moves us to body
|
|
36
|
+
// Using a data attribute persists across Stimulus disconnect/reconnect cycles
|
|
37
|
+
if (!this.element.dataset.inkpenEditorId) {
|
|
38
|
+
const editorElement = this.element.closest("[data-controller*='inkpen--editor']")
|
|
39
|
+
if (editorElement) {
|
|
40
|
+
// Generate a unique ID if the editor doesn't have one
|
|
41
|
+
if (!editorElement.id) {
|
|
42
|
+
editorElement.id = `inkpen-editor-${Date.now()}`
|
|
43
|
+
}
|
|
44
|
+
this.element.dataset.inkpenEditorId = editorElement.id
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find parent editor controller (with retry for timing)
|
|
49
|
+
this.findEditorController()
|
|
50
|
+
if (!this.editorController) {
|
|
51
|
+
this.retryFindEditor()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build initial toolbar buttons
|
|
55
|
+
this.buildToolbar()
|
|
56
|
+
|
|
57
|
+
// Keep toolbar active states in sync when markdown mode changes via shortcuts.
|
|
58
|
+
const editorElement = this.getEditorElement()
|
|
59
|
+
if (editorElement) {
|
|
60
|
+
this.modeChangeHandler = () => this.updateActiveStates()
|
|
61
|
+
editorElement.addEventListener("inkpen:mode-change", this.modeChangeHandler)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Listen for selection changes to update context
|
|
65
|
+
if (this.contextAwareValue) {
|
|
66
|
+
this.setupContextDetection()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
disconnect() {
|
|
71
|
+
if (this.retryTimer) {
|
|
72
|
+
clearTimeout(this.retryTimer)
|
|
73
|
+
}
|
|
74
|
+
if (this.selectionObserver) {
|
|
75
|
+
this.selectionObserver = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const editorElement = this.getEditorElement()
|
|
79
|
+
if (editorElement && this.modeChangeHandler) {
|
|
80
|
+
editorElement.removeEventListener("inkpen:mode-change", this.modeChangeHandler)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Context Detection ---
|
|
85
|
+
|
|
86
|
+
setupContextDetection() {
|
|
87
|
+
// Watch for editor selection events
|
|
88
|
+
const editorElement = this.getEditorElement()
|
|
89
|
+
if (editorElement) {
|
|
90
|
+
editorElement.addEventListener("inkpen:selection-change", this.handleSelectionChange.bind(this))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
handleSelectionChange(event) {
|
|
95
|
+
const newContext = this.detectContext()
|
|
96
|
+
if (newContext !== this.currentContext) {
|
|
97
|
+
this.currentContext = newContext
|
|
98
|
+
this.buildToolbar()
|
|
99
|
+
}
|
|
100
|
+
this.updateActiveStates()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
detectContext() {
|
|
104
|
+
if (!this.editorController?.editor) return "text"
|
|
105
|
+
|
|
106
|
+
const editor = this.editorController.editor
|
|
107
|
+
const { selection } = editor.state
|
|
108
|
+
const { $from } = selection
|
|
109
|
+
|
|
110
|
+
// Check if inside a table cell
|
|
111
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
112
|
+
const node = $from.node(depth)
|
|
113
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
|
|
114
|
+
return "table"
|
|
115
|
+
}
|
|
116
|
+
if (node.type.name === "codeBlock") {
|
|
117
|
+
return "code"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if an image is selected
|
|
122
|
+
if (selection.node?.type?.name === "image") {
|
|
123
|
+
return "image"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return "text"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getEditorElement() {
|
|
130
|
+
const editorId = this.element.dataset.inkpenEditorId
|
|
131
|
+
if (editorId) {
|
|
132
|
+
return document.getElementById(editorId)
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
retryFindEditor() {
|
|
138
|
+
let attempts = 0
|
|
139
|
+
const maxAttempts = 10
|
|
140
|
+
|
|
141
|
+
const tryFind = () => {
|
|
142
|
+
attempts++
|
|
143
|
+
this.findEditorController()
|
|
144
|
+
|
|
145
|
+
if (!this.editorController && attempts < maxAttempts) {
|
|
146
|
+
this.retryTimer = setTimeout(tryFind, 100)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.retryTimer = setTimeout(tryFind, 50)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
findEditorController() {
|
|
154
|
+
let editorElement = null
|
|
155
|
+
|
|
156
|
+
// First, try using the stored editor ID (needed after BubbleMenu moves us to body)
|
|
157
|
+
const editorId = this.element.dataset.inkpenEditorId
|
|
158
|
+
if (editorId) {
|
|
159
|
+
editorElement = document.getElementById(editorId)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fallback: try closest (works if we haven't been moved yet)
|
|
163
|
+
if (!editorElement) {
|
|
164
|
+
editorElement = this.element.closest("[data-controller*='inkpen--editor']")
|
|
165
|
+
if (editorElement) {
|
|
166
|
+
if (!editorElement.id) {
|
|
167
|
+
editorElement.id = `inkpen-editor-${Date.now()}`
|
|
168
|
+
}
|
|
169
|
+
this.element.dataset.inkpenEditorId = editorElement.id
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (editorElement) {
|
|
174
|
+
this.editorController = this.application.getControllerForElementAndIdentifier(
|
|
175
|
+
editorElement,
|
|
176
|
+
"inkpen--editor"
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
buildToolbar() {
|
|
182
|
+
const buttons = this.getButtonsForContext()
|
|
183
|
+
|
|
184
|
+
this.element.innerHTML = buttons.map(btn => {
|
|
185
|
+
if (btn === "divider") {
|
|
186
|
+
return '<span class="inkpen-toolbar__divider"></span>'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const config = this.buttonConfig(btn)
|
|
190
|
+
// Use mousedown to prevent focus loss + click to execute command
|
|
191
|
+
return `
|
|
192
|
+
<button type="button"
|
|
193
|
+
class="inkpen-toolbar__button"
|
|
194
|
+
data-action="mousedown->inkpen--toolbar#preventFocusLoss click->inkpen--toolbar#executeCommand"
|
|
195
|
+
data-command="${btn}"
|
|
196
|
+
title="${config.title}">
|
|
197
|
+
${config.icon}
|
|
198
|
+
</button>
|
|
199
|
+
`
|
|
200
|
+
}).join("")
|
|
201
|
+
|
|
202
|
+
// Update data attribute for CSS styling
|
|
203
|
+
this.element.dataset.context = this.currentContext
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get buttons based on current context
|
|
208
|
+
*/
|
|
209
|
+
getButtonsForContext() {
|
|
210
|
+
// If buttons are explicitly set, use those
|
|
211
|
+
if (this.buttonsValue.length > 0) {
|
|
212
|
+
return this.buttonsValue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Otherwise, return context-appropriate buttons
|
|
216
|
+
switch (this.currentContext) {
|
|
217
|
+
case "table":
|
|
218
|
+
return this.tableButtons()
|
|
219
|
+
case "code":
|
|
220
|
+
return this.codeButtons()
|
|
221
|
+
case "image":
|
|
222
|
+
return this.imageButtons()
|
|
223
|
+
default:
|
|
224
|
+
return this.defaultButtons()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Prevent focus from moving to the button when clicked.
|
|
230
|
+
* This preserves the editor's text selection so formatting commands work correctly.
|
|
231
|
+
*/
|
|
232
|
+
preventFocusLoss(event) {
|
|
233
|
+
event.preventDefault()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Button Sets by Context ---
|
|
237
|
+
|
|
238
|
+
defaultButtons() {
|
|
239
|
+
return this.appendMarkdownToggle([
|
|
240
|
+
"bold", "italic", "underline", "strike",
|
|
241
|
+
"divider", "code", "highlight", "link",
|
|
242
|
+
"divider", "heading", "callout"
|
|
243
|
+
])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
tableButtons() {
|
|
247
|
+
return this.appendMarkdownToggle([
|
|
248
|
+
"bold", "italic", "divider",
|
|
249
|
+
"addRowBefore", "addRowAfter", "deleteRow", "divider",
|
|
250
|
+
"addColumnBefore", "addColumnAfter", "deleteColumn", "divider",
|
|
251
|
+
"mergeCells", "splitCell", "deleteTable"
|
|
252
|
+
])
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
codeButtons() {
|
|
256
|
+
return this.appendMarkdownToggle(["languageSelector"])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
imageButtons() {
|
|
260
|
+
return this.appendMarkdownToggle(["alignLeft", "alignCenter", "alignRight", "divider", "deleteImage"])
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
markdownToggleAvailable() {
|
|
264
|
+
return this.editorController?.toolbarValue === "fixed" &&
|
|
265
|
+
this.editorController?.markdownEnabledValue &&
|
|
266
|
+
this.editorController?.markdownToolbarButtonValue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
appendMarkdownToggle(buttons) {
|
|
270
|
+
if (!this.markdownToggleAvailable()) return buttons
|
|
271
|
+
if (buttons.includes("markdownMode")) return buttons
|
|
272
|
+
|
|
273
|
+
return [...buttons, "divider", "markdownMode"]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
buttonConfig(name) {
|
|
277
|
+
const configs = {
|
|
278
|
+
bold: {
|
|
279
|
+
title: "Bold (Cmd+B)",
|
|
280
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>'
|
|
281
|
+
},
|
|
282
|
+
italic: {
|
|
283
|
+
title: "Italic (Cmd+I)",
|
|
284
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>'
|
|
285
|
+
},
|
|
286
|
+
underline: {
|
|
287
|
+
title: "Underline (Cmd+U)",
|
|
288
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4v6a6 6 0 0 0 6 6 6 6 0 0 0 6-6V4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>'
|
|
289
|
+
},
|
|
290
|
+
strike: {
|
|
291
|
+
title: "Strikethrough",
|
|
292
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" y1="12" x2="20" y2="12"/></svg>'
|
|
293
|
+
},
|
|
294
|
+
code: {
|
|
295
|
+
title: "Inline Code",
|
|
296
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>'
|
|
297
|
+
},
|
|
298
|
+
highlight: {
|
|
299
|
+
title: "Highlight",
|
|
300
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/></svg>'
|
|
301
|
+
},
|
|
302
|
+
link: {
|
|
303
|
+
title: "Link (Cmd+K)",
|
|
304
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>'
|
|
305
|
+
},
|
|
306
|
+
heading: {
|
|
307
|
+
title: "Heading",
|
|
308
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16"/><path d="M4 6v12"/><path d="M20 6v12"/></svg>'
|
|
309
|
+
},
|
|
310
|
+
bulletList: {
|
|
311
|
+
title: "Bullet List",
|
|
312
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></svg>'
|
|
313
|
+
},
|
|
314
|
+
orderedList: {
|
|
315
|
+
title: "Numbered List",
|
|
316
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="4" y="7" font-size="6" fill="currentColor">1</text><text x="4" y="13" font-size="6" fill="currentColor">2</text><text x="4" y="19" font-size="6" fill="currentColor">3</text></svg>'
|
|
317
|
+
},
|
|
318
|
+
blockquote: {
|
|
319
|
+
title: "Quote",
|
|
320
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z"/></svg>'
|
|
321
|
+
},
|
|
322
|
+
codeBlock: {
|
|
323
|
+
title: "Code Block",
|
|
324
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>'
|
|
325
|
+
},
|
|
326
|
+
youtube: {
|
|
327
|
+
title: "YouTube Video",
|
|
328
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"/><path d="m10 15 5-3-5-3z"/></svg>'
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// --- Table Operations ---
|
|
332
|
+
addRowBefore: {
|
|
333
|
+
title: "Add Row Above",
|
|
334
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="8" width="18" height="13" rx="2"/><path d="M12 2v4"/><path d="M9 4h6"/></svg>'
|
|
335
|
+
},
|
|
336
|
+
addRowAfter: {
|
|
337
|
+
title: "Add Row Below",
|
|
338
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="13" rx="2"/><path d="M12 18v4"/><path d="M9 20h6"/></svg>'
|
|
339
|
+
},
|
|
340
|
+
deleteRow: {
|
|
341
|
+
title: "Delete Row",
|
|
342
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="6" width="18" height="12" rx="2"/><path d="M9 12h6"/></svg>'
|
|
343
|
+
},
|
|
344
|
+
addColumnBefore: {
|
|
345
|
+
title: "Add Column Left",
|
|
346
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="3" width="13" height="18" rx="2"/><path d="M2 12h4"/><path d="M4 9v6"/></svg>'
|
|
347
|
+
},
|
|
348
|
+
addColumnAfter: {
|
|
349
|
+
title: "Add Column Right",
|
|
350
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="13" height="18" rx="2"/><path d="M18 12h4"/><path d="M20 9v6"/></svg>'
|
|
351
|
+
},
|
|
352
|
+
deleteColumn: {
|
|
353
|
+
title: "Delete Column",
|
|
354
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="3" width="12" height="18" rx="2"/><path d="M9 12h6"/></svg>'
|
|
355
|
+
},
|
|
356
|
+
mergeCells: {
|
|
357
|
+
title: "Merge Cells",
|
|
358
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M9 12h6"/></svg>'
|
|
359
|
+
},
|
|
360
|
+
splitCell: {
|
|
361
|
+
title: "Split Cell",
|
|
362
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/><path d="M3 12h7"/><path d="M14 12h7"/></svg>'
|
|
363
|
+
},
|
|
364
|
+
deleteTable: {
|
|
365
|
+
title: "Delete Table",
|
|
366
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="4" y1="4" x2="20" y2="20" stroke-width="2.5"/></svg>'
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
// --- Advanced HTML Features ---
|
|
370
|
+
callout: {
|
|
371
|
+
title: "Callout Box",
|
|
372
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="10" r="1" fill="currentColor"/><path d="M12 14v3"/></svg>'
|
|
373
|
+
},
|
|
374
|
+
htmlBlock: {
|
|
375
|
+
title: "HTML Block",
|
|
376
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m16 18 2-2v-3"/><path d="m8 18-2-2v-3"/><path d="M12 2v6"/><path d="m16 6-2-2v-3"/><path d="m8 6 2-2V2"/><rect x="4" y="10" width="16" height="10" rx="2"/></svg>'
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// --- Image Operations ---
|
|
380
|
+
alignLeft: {
|
|
381
|
+
title: "Align Left",
|
|
382
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'
|
|
383
|
+
},
|
|
384
|
+
alignCenter: {
|
|
385
|
+
title: "Align Center",
|
|
386
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'
|
|
387
|
+
},
|
|
388
|
+
alignRight: {
|
|
389
|
+
title: "Align Right",
|
|
390
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'
|
|
391
|
+
},
|
|
392
|
+
deleteImage: {
|
|
393
|
+
title: "Delete Image",
|
|
394
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/><line x1="5" y1="5" x2="19" y2="19" stroke-width="2.5"/></svg>'
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// --- Code Block ---
|
|
398
|
+
languageSelector: {
|
|
399
|
+
title: "Select Language",
|
|
400
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/><text x="9" y="17" font-size="8" fill="currentColor">JS</text></svg>'
|
|
401
|
+
},
|
|
402
|
+
markdownMode: {
|
|
403
|
+
title: "Toggle Markdown",
|
|
404
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19V5h2l4 6 4-6h2v14h-2V9l-4 6-4-6v10z"/><path d="M20 8v8"/><path d="M18 10l2-2 2 2"/><path d="m18 14 2 2 2-2"/></svg>'
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return configs[name] || { title: name, icon: name }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
executeCommand(event) {
|
|
412
|
+
const command = event.currentTarget.dataset.command
|
|
413
|
+
if (!this.editorController) {
|
|
414
|
+
this.findEditorController()
|
|
415
|
+
}
|
|
416
|
+
if (!this.editorController) return
|
|
417
|
+
|
|
418
|
+
switch (command) {
|
|
419
|
+
// --- Text Formatting ---
|
|
420
|
+
case "bold":
|
|
421
|
+
this.editorController.toggleBold()
|
|
422
|
+
break
|
|
423
|
+
case "italic":
|
|
424
|
+
this.editorController.toggleItalic()
|
|
425
|
+
break
|
|
426
|
+
case "underline":
|
|
427
|
+
this.editorController.toggleUnderline()
|
|
428
|
+
break
|
|
429
|
+
case "strike":
|
|
430
|
+
this.editorController.toggleStrike()
|
|
431
|
+
break
|
|
432
|
+
case "highlight":
|
|
433
|
+
this.editorController.toggleHighlight()
|
|
434
|
+
break
|
|
435
|
+
case "link":
|
|
436
|
+
this.promptForLink()
|
|
437
|
+
break
|
|
438
|
+
case "heading":
|
|
439
|
+
this.editorController.toggleHeading(2)
|
|
440
|
+
break
|
|
441
|
+
case "bulletList":
|
|
442
|
+
this.editorController.toggleBulletList()
|
|
443
|
+
break
|
|
444
|
+
case "orderedList":
|
|
445
|
+
this.editorController.toggleOrderedList()
|
|
446
|
+
break
|
|
447
|
+
case "blockquote":
|
|
448
|
+
this.editorController.toggleBlockquote()
|
|
449
|
+
break
|
|
450
|
+
case "codeBlock":
|
|
451
|
+
this.editorController.toggleCodeBlock()
|
|
452
|
+
break
|
|
453
|
+
case "code":
|
|
454
|
+
this.editorController.toggleCode()
|
|
455
|
+
break
|
|
456
|
+
case "youtube":
|
|
457
|
+
this.promptForYoutubeUrl()
|
|
458
|
+
break
|
|
459
|
+
|
|
460
|
+
// --- Table Operations ---
|
|
461
|
+
case "addRowBefore":
|
|
462
|
+
this.editorController.addTableRowBefore()
|
|
463
|
+
break
|
|
464
|
+
case "addRowAfter":
|
|
465
|
+
this.editorController.addTableRowAfter()
|
|
466
|
+
break
|
|
467
|
+
case "deleteRow":
|
|
468
|
+
this.editorController.deleteTableRow()
|
|
469
|
+
break
|
|
470
|
+
case "addColumnBefore":
|
|
471
|
+
this.editorController.addTableColumnBefore()
|
|
472
|
+
break
|
|
473
|
+
case "addColumnAfter":
|
|
474
|
+
this.editorController.addTableColumnAfter()
|
|
475
|
+
break
|
|
476
|
+
case "deleteColumn":
|
|
477
|
+
this.editorController.deleteTableColumn()
|
|
478
|
+
break
|
|
479
|
+
case "mergeCells":
|
|
480
|
+
this.editorController.mergeCells()
|
|
481
|
+
break
|
|
482
|
+
case "splitCell":
|
|
483
|
+
this.editorController.splitCell()
|
|
484
|
+
break
|
|
485
|
+
case "deleteTable":
|
|
486
|
+
if (confirm("Delete this table?")) {
|
|
487
|
+
this.editorController.deleteTable()
|
|
488
|
+
}
|
|
489
|
+
break
|
|
490
|
+
|
|
491
|
+
// --- Advanced HTML Features ---
|
|
492
|
+
case "callout":
|
|
493
|
+
this.insertCallout()
|
|
494
|
+
break
|
|
495
|
+
case "htmlBlock":
|
|
496
|
+
this.insertHtmlBlock()
|
|
497
|
+
break
|
|
498
|
+
|
|
499
|
+
// --- Image Operations ---
|
|
500
|
+
case "alignLeft":
|
|
501
|
+
this.setImageAlignment("left")
|
|
502
|
+
break
|
|
503
|
+
case "alignCenter":
|
|
504
|
+
this.setImageAlignment("center")
|
|
505
|
+
break
|
|
506
|
+
case "alignRight":
|
|
507
|
+
this.setImageAlignment("right")
|
|
508
|
+
break
|
|
509
|
+
case "deleteImage":
|
|
510
|
+
this.deleteSelectedNode()
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
// --- Code Block ---
|
|
514
|
+
case "languageSelector":
|
|
515
|
+
this.showLanguageSelector()
|
|
516
|
+
break
|
|
517
|
+
case "markdownMode":
|
|
518
|
+
this.editorController.toggleMarkdownMode()
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.updateActiveStates()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
promptForYoutubeUrl() {
|
|
526
|
+
const url = prompt("Enter YouTube URL:", "https://www.youtube.com/watch?v=")
|
|
527
|
+
if (url && url !== "https://www.youtube.com/watch?v=") {
|
|
528
|
+
this.editorController.insertYoutubeVideo(url)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
promptForLink() {
|
|
533
|
+
const url = prompt("Enter URL:", "https://")
|
|
534
|
+
if (url && url !== "https://") {
|
|
535
|
+
this.editorController.setLink(url)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
updateActiveStates() {
|
|
540
|
+
if (!this.editorController) return
|
|
541
|
+
|
|
542
|
+
this.element.querySelectorAll(".inkpen-toolbar__button").forEach(btn => {
|
|
543
|
+
const command = btn.dataset.command
|
|
544
|
+
const isActive = this.isCommandActive(command)
|
|
545
|
+
btn.classList.toggle("is-active", isActive)
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
isCommandActive(command) {
|
|
550
|
+
if (command === "markdownMode") {
|
|
551
|
+
return this.editorController?.markdownEnabledValue &&
|
|
552
|
+
this.editorController?.markdownModeValue !== "wysiwyg"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return this.editorController.isActive(this.commandToNodeName(command))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
commandToNodeName(command) {
|
|
559
|
+
const mapping = {
|
|
560
|
+
bold: "bold",
|
|
561
|
+
italic: "italic",
|
|
562
|
+
strike: "strike",
|
|
563
|
+
underline: "underline",
|
|
564
|
+
link: "link",
|
|
565
|
+
heading: "heading",
|
|
566
|
+
bulletList: "bulletList",
|
|
567
|
+
orderedList: "orderedList",
|
|
568
|
+
blockquote: "blockquote",
|
|
569
|
+
codeBlock: "codeBlock",
|
|
570
|
+
code: "code",
|
|
571
|
+
highlight: "highlight"
|
|
572
|
+
}
|
|
573
|
+
return mapping[command] || command
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --- Advanced HTML Features ---
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Insert a callout/alert box
|
|
580
|
+
* Types: info, warning, tip, note
|
|
581
|
+
*/
|
|
582
|
+
insertCallout() {
|
|
583
|
+
const type = this.promptCalloutType()
|
|
584
|
+
if (!type) return
|
|
585
|
+
|
|
586
|
+
const calloutHtml = `
|
|
587
|
+
<div class="inkpen-callout inkpen-callout--${type}" data-callout-type="${type}">
|
|
588
|
+
<div class="inkpen-callout__icon">${this.getCalloutIcon(type)}</div>
|
|
589
|
+
<div class="inkpen-callout__content"><p>Type your ${type} message here...</p></div>
|
|
590
|
+
</div>
|
|
591
|
+
`
|
|
592
|
+
this.editorController?.editor?.chain().focus().insertContent(calloutHtml).run()
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
promptCalloutType() {
|
|
596
|
+
const type = prompt("Callout type (info, warning, tip, note):", "info")
|
|
597
|
+
if (!type) return null
|
|
598
|
+
|
|
599
|
+
const validTypes = ["info", "warning", "tip", "note"]
|
|
600
|
+
const normalizedType = type.toLowerCase().trim()
|
|
601
|
+
|
|
602
|
+
if (!validTypes.includes(normalizedType)) {
|
|
603
|
+
alert("Invalid callout type. Please use: info, warning, tip, or note.")
|
|
604
|
+
return null
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return normalizedType
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
getCalloutIcon(type) {
|
|
611
|
+
const icons = {
|
|
612
|
+
info: "ℹ️",
|
|
613
|
+
warning: "⚠️",
|
|
614
|
+
tip: "💡",
|
|
615
|
+
note: "📝"
|
|
616
|
+
}
|
|
617
|
+
return icons[type] || "ℹ️"
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Insert a raw HTML block
|
|
622
|
+
*/
|
|
623
|
+
insertHtmlBlock() {
|
|
624
|
+
const html = prompt("Enter HTML code:", "<div></div>")
|
|
625
|
+
if (!html) return
|
|
626
|
+
|
|
627
|
+
// Wrap in a container with editing disabled for the raw HTML
|
|
628
|
+
const wrappedHtml = `<div class="inkpen-html-block" contenteditable="false">${html}</div>`
|
|
629
|
+
this.editorController?.editor?.chain().focus().insertContent(wrappedHtml).run()
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// --- Image Operations ---
|
|
633
|
+
|
|
634
|
+
setImageAlignment(alignment) {
|
|
635
|
+
const editor = this.editorController?.editor
|
|
636
|
+
if (!editor) return
|
|
637
|
+
|
|
638
|
+
// Get the selected image node
|
|
639
|
+
const { selection } = editor.state
|
|
640
|
+
if (selection.node?.type?.name !== "image") return
|
|
641
|
+
|
|
642
|
+
// Update the image with alignment attribute
|
|
643
|
+
editor.chain().focus().updateAttributes("image", {
|
|
644
|
+
style: `display: block; margin: ${alignment === "center" ? "0 auto" : alignment === "right" ? "0 0 0 auto" : "0"};`
|
|
645
|
+
}).run()
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
deleteSelectedNode() {
|
|
649
|
+
this.editorController?.editor?.chain().focus().deleteSelection().run()
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// --- Code Block Language ---
|
|
653
|
+
|
|
654
|
+
showLanguageSelector() {
|
|
655
|
+
const languages = [
|
|
656
|
+
"javascript", "typescript", "python", "ruby", "go",
|
|
657
|
+
"rust", "java", "c", "cpp", "csharp",
|
|
658
|
+
"html", "css", "json", "yaml", "sql",
|
|
659
|
+
"bash", "shell", "markdown", "plaintext"
|
|
660
|
+
]
|
|
661
|
+
|
|
662
|
+
const current = this.getCurrentCodeLanguage() || "plaintext"
|
|
663
|
+
const selected = prompt(
|
|
664
|
+
`Select language (current: ${current}):\n\n${languages.join(", ")}`,
|
|
665
|
+
current
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if (selected && languages.includes(selected.toLowerCase())) {
|
|
669
|
+
this.setCodeLanguage(selected.toLowerCase())
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
getCurrentCodeLanguage() {
|
|
674
|
+
const editor = this.editorController?.editor
|
|
675
|
+
if (!editor) return null
|
|
676
|
+
|
|
677
|
+
const { selection } = editor.state
|
|
678
|
+
const { $from } = selection
|
|
679
|
+
|
|
680
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
681
|
+
const node = $from.node(depth)
|
|
682
|
+
if (node.type.name === "codeBlock") {
|
|
683
|
+
return node.attrs.language || null
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return null
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
setCodeLanguage(language) {
|
|
691
|
+
this.editorController?.editor?.chain().focus().updateAttributes("codeBlock", { language }).run()
|
|
692
|
+
}
|
|
693
|
+
}
|