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,667 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inkpen Sticky Toolbar Controller
|
|
5
|
+
*
|
|
6
|
+
* Fixed-position toolbar for block/media/widget insertion.
|
|
7
|
+
* Supports horizontal (bottom) and vertical (left/right) layouts.
|
|
8
|
+
*
|
|
9
|
+
* Follows Fizzy patterns:
|
|
10
|
+
* - Single responsibility (toolbar positioning + commands)
|
|
11
|
+
* - Declarative data-* configuration
|
|
12
|
+
* - Event-driven communication with editor
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* <div data-controller="inkpen--editor inkpen--sticky-toolbar"
|
|
16
|
+
* data-inkpen--sticky-toolbar-position-value="bottom"
|
|
17
|
+
* data-inkpen--sticky-toolbar-buttons-value='["table","code_block","image"]'>
|
|
18
|
+
* ...
|
|
19
|
+
* </div>
|
|
20
|
+
*/
|
|
21
|
+
export default class extends Controller {
|
|
22
|
+
static targets = ["button", "widgetModal", "widgetList"]
|
|
23
|
+
|
|
24
|
+
static values = {
|
|
25
|
+
position: { type: String, default: "bottom" },
|
|
26
|
+
buttons: { type: Array, default: [] },
|
|
27
|
+
widgetTypes: { type: Array, default: [] },
|
|
28
|
+
vertical: { type: Boolean, default: false },
|
|
29
|
+
showExport: { type: Boolean, default: false },
|
|
30
|
+
exportFormats: { type: Array, default: ["markdown", "html", "pdf"] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#exportMenuOpen = false
|
|
34
|
+
#boundCloseExportMenu = null
|
|
35
|
+
|
|
36
|
+
connect() {
|
|
37
|
+
this.editorController = null
|
|
38
|
+
this.findEditorController()
|
|
39
|
+
|
|
40
|
+
// Prevent duplicate toolbars on reconnect
|
|
41
|
+
if (!this.toolbarElement) {
|
|
42
|
+
this.createToolbarElement()
|
|
43
|
+
this.buildToolbar()
|
|
44
|
+
this.applyLayout()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Bind document click to close export menu
|
|
48
|
+
this.#boundCloseExportMenu = this.#closeExportMenuOnClickOutside.bind(this)
|
|
49
|
+
document.addEventListener("click", this.#boundCloseExportMenu)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
disconnect() {
|
|
53
|
+
this.closeWidgetModal()
|
|
54
|
+
this.#closeExportMenu()
|
|
55
|
+
this.removeToolbarElement()
|
|
56
|
+
this.removeModalElement()
|
|
57
|
+
document.removeEventListener("click", this.#boundCloseExportMenu)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Toolbar Element Management ---
|
|
61
|
+
|
|
62
|
+
createToolbarElement() {
|
|
63
|
+
// Create the sticky toolbar container as a sibling to the editor
|
|
64
|
+
this.toolbarElement = document.createElement("div")
|
|
65
|
+
this.toolbarElement.className = "inkpen-sticky-toolbar"
|
|
66
|
+
|
|
67
|
+
// Insert after the editor element
|
|
68
|
+
this.element.parentNode.insertBefore(this.toolbarElement, this.element.nextSibling)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
removeToolbarElement() {
|
|
72
|
+
if (this.toolbarElement && this.toolbarElement.parentNode) {
|
|
73
|
+
this.toolbarElement.parentNode.removeChild(this.toolbarElement)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create modal element and append to body (avoids transform containing block issues)
|
|
78
|
+
createModalElement() {
|
|
79
|
+
// Prevent duplicate modals
|
|
80
|
+
if (this.modalElement) return
|
|
81
|
+
|
|
82
|
+
const wrapper = document.createElement("div")
|
|
83
|
+
wrapper.innerHTML = this.renderWidgetModal()
|
|
84
|
+
this.modalElement = wrapper.firstElementChild
|
|
85
|
+
document.body.appendChild(this.modalElement)
|
|
86
|
+
|
|
87
|
+
// Bind modal event handlers
|
|
88
|
+
this.bindModalEvents()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removeModalElement() {
|
|
92
|
+
if (this.modalElement && this.modalElement.parentNode) {
|
|
93
|
+
this.modalElement.parentNode.removeChild(this.modalElement)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
bindModalEvents() {
|
|
98
|
+
if (!this.modalElement) return
|
|
99
|
+
|
|
100
|
+
const backdrop = this.modalElement.querySelector(".inkpen-widget-modal__backdrop")
|
|
101
|
+
if (backdrop) {
|
|
102
|
+
backdrop.addEventListener("click", () => this.closeWidgetModal())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const closeBtn = this.modalElement.querySelector(".inkpen-widget-modal__close")
|
|
106
|
+
if (closeBtn) {
|
|
107
|
+
closeBtn.addEventListener("click", () => this.closeWidgetModal())
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.modalElement.querySelectorAll("[data-widget-type]").forEach(btn => {
|
|
111
|
+
btn.addEventListener("click", (e) => this.insertWidget(e))
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Layout Management ---
|
|
116
|
+
|
|
117
|
+
applyLayout() {
|
|
118
|
+
if (!this.toolbarElement) return
|
|
119
|
+
|
|
120
|
+
this.toolbarElement.classList.toggle("inkpen-sticky-toolbar--vertical", this.verticalValue)
|
|
121
|
+
this.toolbarElement.classList.toggle("inkpen-sticky-toolbar--horizontal", !this.verticalValue)
|
|
122
|
+
this.toolbarElement.dataset.position = this.positionValue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
positionValueChanged() {
|
|
126
|
+
this.verticalValue = ["left", "right"].includes(this.positionValue)
|
|
127
|
+
this.applyLayout()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Toolbar Building ---
|
|
131
|
+
|
|
132
|
+
buildToolbar() {
|
|
133
|
+
if (!this.toolbarElement) return
|
|
134
|
+
|
|
135
|
+
const buttons = this.buttonsValue.length > 0 ? this.buttonsValue : this.defaultButtons()
|
|
136
|
+
|
|
137
|
+
// Build toolbar buttons only (modal is appended to body separately)
|
|
138
|
+
this.toolbarElement.innerHTML = `
|
|
139
|
+
<div class="inkpen-sticky-toolbar__buttons">
|
|
140
|
+
${buttons.map(btn => this.renderButton(btn)).join("")}
|
|
141
|
+
${this.showExportValue ? this.#renderExportButton() : ""}
|
|
142
|
+
</div>
|
|
143
|
+
`
|
|
144
|
+
|
|
145
|
+
// Bind toolbar button handlers
|
|
146
|
+
this.bindToolbarEvents()
|
|
147
|
+
|
|
148
|
+
// Create modal as child of body to avoid transform containing block issues
|
|
149
|
+
this.createModalElement()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
bindToolbarEvents() {
|
|
153
|
+
if (!this.toolbarElement) return
|
|
154
|
+
|
|
155
|
+
// Button click handlers with mousedown prevention to preserve editor selection
|
|
156
|
+
this.toolbarElement.querySelectorAll("[data-command]").forEach(btn => {
|
|
157
|
+
btn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
158
|
+
btn.addEventListener("click", (e) => this.executeCommand(e))
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Export menu toggle
|
|
162
|
+
const exportToggle = this.toolbarElement.querySelector("[data-export-toggle]")
|
|
163
|
+
if (exportToggle) {
|
|
164
|
+
exportToggle.addEventListener("mousedown", (e) => e.preventDefault())
|
|
165
|
+
exportToggle.addEventListener("click", (e) => {
|
|
166
|
+
e.stopPropagation()
|
|
167
|
+
this.#toggleExportMenu()
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Export menu item handlers
|
|
172
|
+
this.toolbarElement.querySelectorAll("[data-export-format]").forEach(btn => {
|
|
173
|
+
btn.addEventListener("mousedown", (e) => e.preventDefault())
|
|
174
|
+
btn.addEventListener("click", (e) => this.#handleExport(e))
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
defaultButtons() {
|
|
179
|
+
return ["table", "code_block", "divider", "image", "youtube", "divider", "widget"]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
renderButton(btn) {
|
|
183
|
+
if (btn === "divider") {
|
|
184
|
+
return '<span class="inkpen-sticky-toolbar__divider"></span>'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const config = this.buttonConfig(btn)
|
|
188
|
+
return `
|
|
189
|
+
<button type="button"
|
|
190
|
+
class="inkpen-sticky-toolbar__btn"
|
|
191
|
+
data-command="${btn}"
|
|
192
|
+
title="${config.title}">
|
|
193
|
+
${config.icon}
|
|
194
|
+
</button>
|
|
195
|
+
`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
buttonConfig(name) {
|
|
199
|
+
const configs = {
|
|
200
|
+
table: {
|
|
201
|
+
title: "Insert Table",
|
|
202
|
+
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"/></svg>'
|
|
203
|
+
},
|
|
204
|
+
code_block: {
|
|
205
|
+
title: "Code Block",
|
|
206
|
+
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>'
|
|
207
|
+
},
|
|
208
|
+
blockquote: {
|
|
209
|
+
title: "Quote Block",
|
|
210
|
+
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>'
|
|
211
|
+
},
|
|
212
|
+
horizontal_rule: {
|
|
213
|
+
title: "Divider Line",
|
|
214
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/></svg>'
|
|
215
|
+
},
|
|
216
|
+
task_list: {
|
|
217
|
+
title: "Task List",
|
|
218
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3.5 8 2 2 3-3"/><line x1="13" y1="8" x2="21" y2="8"/><rect x="3" y="14" width="6" height="6" rx="1"/><line x1="13" y1="17" x2="21" y2="17"/></svg>'
|
|
219
|
+
},
|
|
220
|
+
image: {
|
|
221
|
+
title: "Insert Image",
|
|
222
|
+
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"/></svg>'
|
|
223
|
+
},
|
|
224
|
+
youtube: {
|
|
225
|
+
title: "YouTube Video",
|
|
226
|
+
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>'
|
|
227
|
+
},
|
|
228
|
+
embed: {
|
|
229
|
+
title: "Embed Content",
|
|
230
|
+
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>'
|
|
231
|
+
},
|
|
232
|
+
widget: {
|
|
233
|
+
title: "Insert Widget",
|
|
234
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>'
|
|
235
|
+
},
|
|
236
|
+
section: {
|
|
237
|
+
title: "Insert Section",
|
|
238
|
+
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"/></svg>'
|
|
239
|
+
},
|
|
240
|
+
preformatted: {
|
|
241
|
+
title: "Preformatted Text (ASCII)",
|
|
242
|
+
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"/><text x="6" y="10" font-size="5" font-family="monospace" fill="currentColor">┌──┐</text><text x="6" y="15" font-size="5" font-family="monospace" fill="currentColor">│ │</text><text x="6" y="20" font-size="5" font-family="monospace" fill="currentColor">└──┘</text></svg>'
|
|
243
|
+
},
|
|
244
|
+
export: {
|
|
245
|
+
title: "Export",
|
|
246
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'
|
|
247
|
+
},
|
|
248
|
+
callout: {
|
|
249
|
+
title: "Callout Block",
|
|
250
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>'
|
|
251
|
+
},
|
|
252
|
+
toggle: {
|
|
253
|
+
title: "Toggle Block",
|
|
254
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/><line x1="4" y1="12" x2="15" y2="12"/><line x1="4" y1="6" x2="11" y2="6"/><line x1="4" y1="18" x2="11" y2="18"/></svg>'
|
|
255
|
+
},
|
|
256
|
+
columns: {
|
|
257
|
+
title: "Multi-Column Layout",
|
|
258
|
+
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="12" y1="3" x2="12" y2="21"/></svg>'
|
|
259
|
+
},
|
|
260
|
+
table_of_contents: {
|
|
261
|
+
title: "Table of Contents",
|
|
262
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="14" y2="12"/><line x1="4" y1="18" x2="18" 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>'
|
|
263
|
+
},
|
|
264
|
+
file_attachment: {
|
|
265
|
+
title: "Attach File",
|
|
266
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57a4 4 0 1 1 5.66 5.66l-8.58 8.58a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>'
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return configs[name] || { title: name, icon: name }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#exportFormatConfig(format) {
|
|
274
|
+
const configs = {
|
|
275
|
+
markdown: {
|
|
276
|
+
label: "Markdown",
|
|
277
|
+
description: "Export as .md file",
|
|
278
|
+
icon: "M↓",
|
|
279
|
+
shortcut: "⌘⌥M"
|
|
280
|
+
},
|
|
281
|
+
html: {
|
|
282
|
+
label: "HTML",
|
|
283
|
+
description: "Export as .html file",
|
|
284
|
+
icon: "<>",
|
|
285
|
+
shortcut: "⌘⌥H"
|
|
286
|
+
},
|
|
287
|
+
pdf: {
|
|
288
|
+
label: "PDF",
|
|
289
|
+
description: "Export as .pdf file",
|
|
290
|
+
icon: "📄",
|
|
291
|
+
shortcut: "⌘⌥P"
|
|
292
|
+
},
|
|
293
|
+
copy_markdown: {
|
|
294
|
+
label: "Copy as Markdown",
|
|
295
|
+
description: "Copy content to clipboard",
|
|
296
|
+
icon: "📋",
|
|
297
|
+
shortcut: "⌘⌥⇧M"
|
|
298
|
+
},
|
|
299
|
+
copy_html: {
|
|
300
|
+
label: "Copy as HTML",
|
|
301
|
+
description: "Copy HTML to clipboard",
|
|
302
|
+
icon: "📋",
|
|
303
|
+
shortcut: "⌘⌥⇧H"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return configs[format] || { label: format, description: "", icon: "", shortcut: "" }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Widget Modal ---
|
|
311
|
+
|
|
312
|
+
renderWidgetModal() {
|
|
313
|
+
const widgets = this.widgetTypesValue.length > 0 ? this.widgetTypesValue : ["form", "gallery", "poll"]
|
|
314
|
+
|
|
315
|
+
return `
|
|
316
|
+
<div class="inkpen-widget-modal hidden" role="dialog" aria-modal="true">
|
|
317
|
+
<div class="inkpen-widget-modal__backdrop"></div>
|
|
318
|
+
<div class="inkpen-widget-modal__content">
|
|
319
|
+
<div class="inkpen-widget-modal__header">
|
|
320
|
+
<h3>Insert Widget</h3>
|
|
321
|
+
<button type="button" class="inkpen-widget-modal__close">
|
|
322
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
323
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
324
|
+
</svg>
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="inkpen-widget-modal__list">
|
|
328
|
+
${widgets.map(w => this.renderWidgetOption(w)).join("")}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
renderWidgetOption(type) {
|
|
336
|
+
const config = this.widgetConfig(type)
|
|
337
|
+
return `
|
|
338
|
+
<button type="button" class="inkpen-widget-modal__option" data-widget-type="${type}">
|
|
339
|
+
<span class="inkpen-widget-modal__option-icon">${config.icon}</span>
|
|
340
|
+
<div class="inkpen-widget-modal__option-text">
|
|
341
|
+
<span class="inkpen-widget-modal__option-label">${config.label}</span>
|
|
342
|
+
<span class="inkpen-widget-modal__option-desc">${config.description}</span>
|
|
343
|
+
</div>
|
|
344
|
+
</button>
|
|
345
|
+
`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
widgetConfig(type) {
|
|
349
|
+
const configs = {
|
|
350
|
+
form: {
|
|
351
|
+
label: "Form",
|
|
352
|
+
description: "Embed a contact or signup form",
|
|
353
|
+
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="7" y1="8" x2="17" y2="8"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="7" y1="16" x2="12" y2="16"/></svg>'
|
|
354
|
+
},
|
|
355
|
+
gallery: {
|
|
356
|
+
label: "Gallery",
|
|
357
|
+
description: "Display an image gallery",
|
|
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"/><rect x="7" y="7" width="3" height="3"/><rect x="14" y="7" width="3" height="3"/><rect x="7" y="14" width="3" height="3"/><rect x="14" y="14" width="3" height="3"/></svg>'
|
|
359
|
+
},
|
|
360
|
+
poll: {
|
|
361
|
+
label: "Poll",
|
|
362
|
+
description: "Create an interactive poll",
|
|
363
|
+
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"/><rect x="7" y="8" width="10" height="2"/><rect x="7" y="14" width="6" height="2"/></svg>'
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return configs[type] || { label: type, description: "", icon: "" }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
openWidgetModal() {
|
|
371
|
+
if (this.modalElement) {
|
|
372
|
+
this.modalElement.classList.remove("hidden")
|
|
373
|
+
document.body.style.overflow = "hidden"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
closeWidgetModal() {
|
|
378
|
+
if (this.modalElement) {
|
|
379
|
+
this.modalElement.classList.add("hidden")
|
|
380
|
+
document.body.style.overflow = ""
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
insertWidget(event) {
|
|
385
|
+
const type = event.currentTarget.dataset.widgetType
|
|
386
|
+
this.closeWidgetModal()
|
|
387
|
+
|
|
388
|
+
// Dispatch event for host app to handle widget insertion
|
|
389
|
+
this.element.dispatchEvent(
|
|
390
|
+
new CustomEvent("inkpen:insert-widget", {
|
|
391
|
+
bubbles: true,
|
|
392
|
+
detail: { type, controller: this }
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// --- Command Execution ---
|
|
398
|
+
|
|
399
|
+
findEditorController() {
|
|
400
|
+
// The sticky toolbar controller is on the same element as the editor controller
|
|
401
|
+
this.editorController = this.application.getControllerForElementAndIdentifier(
|
|
402
|
+
this.element,
|
|
403
|
+
"inkpen--editor"
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
executeCommand(event) {
|
|
408
|
+
const command = event.currentTarget.dataset.command
|
|
409
|
+
if (!this.editorController) {
|
|
410
|
+
this.findEditorController()
|
|
411
|
+
}
|
|
412
|
+
if (!this.editorController) return
|
|
413
|
+
|
|
414
|
+
switch (command) {
|
|
415
|
+
case "table":
|
|
416
|
+
this.editorController.insertTable(3, 3, true)
|
|
417
|
+
break
|
|
418
|
+
case "code_block":
|
|
419
|
+
this.editorController.toggleCodeBlock()
|
|
420
|
+
break
|
|
421
|
+
case "blockquote":
|
|
422
|
+
this.editorController.toggleBlockquote()
|
|
423
|
+
break
|
|
424
|
+
case "horizontal_rule":
|
|
425
|
+
this.editorController.insertHorizontalRule()
|
|
426
|
+
break
|
|
427
|
+
case "task_list":
|
|
428
|
+
this.editorController.toggleTaskList()
|
|
429
|
+
break
|
|
430
|
+
case "image":
|
|
431
|
+
this.promptForImage()
|
|
432
|
+
break
|
|
433
|
+
case "youtube":
|
|
434
|
+
this.promptForYoutubeUrl()
|
|
435
|
+
break
|
|
436
|
+
case "embed":
|
|
437
|
+
this.promptForEmbed()
|
|
438
|
+
break
|
|
439
|
+
case "widget":
|
|
440
|
+
this.openWidgetModal()
|
|
441
|
+
break
|
|
442
|
+
case "section":
|
|
443
|
+
this.editorController.insertSection()
|
|
444
|
+
break
|
|
445
|
+
case "preformatted":
|
|
446
|
+
this.editorController.insertPreformatted()
|
|
447
|
+
break
|
|
448
|
+
case "callout":
|
|
449
|
+
this.editorController.insertCallout()
|
|
450
|
+
break
|
|
451
|
+
case "toggle":
|
|
452
|
+
this.editorController.insertToggle()
|
|
453
|
+
break
|
|
454
|
+
case "columns":
|
|
455
|
+
this.editorController.insertColumns()
|
|
456
|
+
break
|
|
457
|
+
case "table_of_contents":
|
|
458
|
+
this.editorController.insertTableOfContents()
|
|
459
|
+
break
|
|
460
|
+
case "file_attachment":
|
|
461
|
+
this.promptForFileAttachment()
|
|
462
|
+
break
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
promptForFileAttachment() {
|
|
467
|
+
// Dispatch event for host app to handle file attachment
|
|
468
|
+
this.element.dispatchEvent(
|
|
469
|
+
new CustomEvent("inkpen:request-file", {
|
|
470
|
+
bubbles: true,
|
|
471
|
+
detail: { controller: this }
|
|
472
|
+
})
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
promptForImage() {
|
|
477
|
+
// Dispatch event for host app to handle image upload
|
|
478
|
+
this.element.dispatchEvent(
|
|
479
|
+
new CustomEvent("inkpen:request-image", {
|
|
480
|
+
bubbles: true,
|
|
481
|
+
detail: { controller: this }
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
promptForYoutubeUrl() {
|
|
487
|
+
const url = prompt("Enter YouTube URL:", "https://www.youtube.com/watch?v=")
|
|
488
|
+
if (url && url !== "https://www.youtube.com/watch?v=") {
|
|
489
|
+
this.editorController.insertYoutubeVideo(url)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
promptForEmbed() {
|
|
494
|
+
// Dispatch event for host app to handle embed
|
|
495
|
+
this.element.dispatchEvent(
|
|
496
|
+
new CustomEvent("inkpen:request-embed", {
|
|
497
|
+
bubbles: true,
|
|
498
|
+
detail: { controller: this }
|
|
499
|
+
})
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// --- Export Menu ---
|
|
504
|
+
|
|
505
|
+
#renderExportButton() {
|
|
506
|
+
const config = this.buttonConfig("export")
|
|
507
|
+
const formats = this.exportFormatsValue
|
|
508
|
+
|
|
509
|
+
return `
|
|
510
|
+
<span class="inkpen-sticky-toolbar__divider"></span>
|
|
511
|
+
<div class="inkpen-export-dropdown">
|
|
512
|
+
<button type="button"
|
|
513
|
+
class="inkpen-sticky-toolbar__btn inkpen-export-dropdown__toggle"
|
|
514
|
+
data-export-toggle
|
|
515
|
+
title="${config.title}">
|
|
516
|
+
${config.icon}
|
|
517
|
+
<span class="inkpen-export-dropdown__caret">▾</span>
|
|
518
|
+
</button>
|
|
519
|
+
<div class="inkpen-export-dropdown__menu">
|
|
520
|
+
<div class="inkpen-export-dropdown__header">Download</div>
|
|
521
|
+
${formats.map(format => this.#renderExportMenuItem(format)).join("")}
|
|
522
|
+
<div class="inkpen-export-dropdown__divider"></div>
|
|
523
|
+
<div class="inkpen-export-dropdown__header">Copy</div>
|
|
524
|
+
${this.#renderExportMenuItem("copy_markdown")}
|
|
525
|
+
${this.#renderExportMenuItem("copy_html")}
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
`
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#renderExportMenuItem(format) {
|
|
532
|
+
const config = this.#exportFormatConfig(format)
|
|
533
|
+
return `
|
|
534
|
+
<button type="button"
|
|
535
|
+
class="inkpen-export-dropdown__item"
|
|
536
|
+
data-export-format="${format}">
|
|
537
|
+
<span class="inkpen-export-dropdown__icon">${config.icon}</span>
|
|
538
|
+
<span class="inkpen-export-dropdown__label">${config.label}</span>
|
|
539
|
+
${config.shortcut ? `<span class="inkpen-export-dropdown__shortcut">${config.shortcut}</span>` : ""}
|
|
540
|
+
</button>
|
|
541
|
+
`
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#toggleExportMenu() {
|
|
545
|
+
this.#exportMenuOpen = !this.#exportMenuOpen
|
|
546
|
+
const menu = this.toolbarElement?.querySelector(".inkpen-export-dropdown__menu")
|
|
547
|
+
const toggle = this.toolbarElement?.querySelector("[data-export-toggle]")
|
|
548
|
+
|
|
549
|
+
if (menu) {
|
|
550
|
+
menu.classList.toggle("is-open", this.#exportMenuOpen)
|
|
551
|
+
}
|
|
552
|
+
if (toggle) {
|
|
553
|
+
toggle.classList.toggle("is-active", this.#exportMenuOpen)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#closeExportMenu() {
|
|
558
|
+
this.#exportMenuOpen = false
|
|
559
|
+
const menu = this.toolbarElement?.querySelector(".inkpen-export-dropdown__menu")
|
|
560
|
+
const toggle = this.toolbarElement?.querySelector("[data-export-toggle]")
|
|
561
|
+
|
|
562
|
+
if (menu) {
|
|
563
|
+
menu.classList.remove("is-open")
|
|
564
|
+
}
|
|
565
|
+
if (toggle) {
|
|
566
|
+
toggle.classList.remove("is-active")
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#closeExportMenuOnClickOutside(event) {
|
|
571
|
+
if (!this.#exportMenuOpen) return
|
|
572
|
+
|
|
573
|
+
const dropdown = this.toolbarElement?.querySelector(".inkpen-export-dropdown")
|
|
574
|
+
if (dropdown && !dropdown.contains(event.target)) {
|
|
575
|
+
this.#closeExportMenu()
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async #handleExport(event) {
|
|
580
|
+
const format = event.currentTarget.dataset.exportFormat
|
|
581
|
+
this.#closeExportMenu()
|
|
582
|
+
|
|
583
|
+
if (!this.editorController) {
|
|
584
|
+
this.findEditorController()
|
|
585
|
+
}
|
|
586
|
+
if (!this.editorController) return
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
switch (format) {
|
|
590
|
+
case "markdown":
|
|
591
|
+
this.editorController.downloadAsMarkdown()
|
|
592
|
+
this.#showExportSuccess("Downloaded as Markdown")
|
|
593
|
+
break
|
|
594
|
+
case "html":
|
|
595
|
+
this.editorController.downloadAsHTML()
|
|
596
|
+
this.#showExportSuccess("Downloaded as HTML")
|
|
597
|
+
break
|
|
598
|
+
case "pdf":
|
|
599
|
+
await this.editorController.downloadAsPDF()
|
|
600
|
+
this.#showExportSuccess("Downloaded as PDF")
|
|
601
|
+
break
|
|
602
|
+
case "copy_markdown":
|
|
603
|
+
const mdSuccess = await this.editorController.copyAsMarkdown()
|
|
604
|
+
if (mdSuccess) {
|
|
605
|
+
this.#showExportSuccess("Copied as Markdown")
|
|
606
|
+
}
|
|
607
|
+
break
|
|
608
|
+
case "copy_html":
|
|
609
|
+
const htmlSuccess = await this.editorController.copyAsHTML()
|
|
610
|
+
if (htmlSuccess) {
|
|
611
|
+
this.#showExportSuccess("Copied as HTML")
|
|
612
|
+
}
|
|
613
|
+
break
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error("Export failed:", error)
|
|
617
|
+
this.#showExportError("Export failed")
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#showExportSuccess(message) {
|
|
622
|
+
this.element.dispatchEvent(
|
|
623
|
+
new CustomEvent("inkpen:export-success", {
|
|
624
|
+
bubbles: true,
|
|
625
|
+
detail: { message, controller: this }
|
|
626
|
+
})
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#showExportError(message) {
|
|
631
|
+
this.element.dispatchEvent(
|
|
632
|
+
new CustomEvent("inkpen:export-error", {
|
|
633
|
+
bubbles: true,
|
|
634
|
+
detail: { message, controller: this }
|
|
635
|
+
})
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// --- Public API ---
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Insert an image at the current cursor position.
|
|
643
|
+
* Call this from host app after uploading an image.
|
|
644
|
+
* @param {string} src - The image URL
|
|
645
|
+
* @param {string} alt - Alt text for the image
|
|
646
|
+
*/
|
|
647
|
+
insertImage(src, alt = "") {
|
|
648
|
+
if (this.editorController?.editor) {
|
|
649
|
+
this.editorController.editor.chain().focus().setImage({ src, alt }).run()
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Insert a widget placeholder at the current cursor position.
|
|
655
|
+
* @param {string} type - Widget type (form, gallery, poll)
|
|
656
|
+
* @param {object} data - Widget configuration data
|
|
657
|
+
*/
|
|
658
|
+
insertWidgetBlock(type, data = {}) {
|
|
659
|
+
// Dispatch event with widget data for host app to insert custom node
|
|
660
|
+
this.element.dispatchEvent(
|
|
661
|
+
new CustomEvent("inkpen:widget-inserted", {
|
|
662
|
+
bubbles: true,
|
|
663
|
+
detail: { type, data, controller: this }
|
|
664
|
+
})
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
}
|