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,439 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core"
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
3
|
+
import Suggestion from "@tiptap/suggestion"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Slash Commands Extension for TipTap
|
|
7
|
+
*
|
|
8
|
+
* Notion-style "/" command palette for rapid block insertion.
|
|
9
|
+
* Type "/" to open the command menu, then type to filter.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Fuzzy search/filter as you type
|
|
13
|
+
* - Keyboard navigation (arrows, enter, escape)
|
|
14
|
+
* - Grouped commands (Basic, Lists, Blocks, Media, Advanced)
|
|
15
|
+
* - Customizable command list
|
|
16
|
+
* - Icons and descriptions
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* editor.commands.insertContent("/") // Opens menu
|
|
20
|
+
*
|
|
21
|
+
* @since 0.3.0
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Default command definitions with icons and metadata
|
|
25
|
+
const DEFAULT_COMMANDS = [
|
|
26
|
+
// Basic
|
|
27
|
+
{ id: "paragraph", title: "Text", description: "Plain text block", icon: "¶", keywords: ["text", "p"], group: "Basic" },
|
|
28
|
+
{ id: "heading1", title: "Heading 1", description: "Large heading", icon: "H1", keywords: ["h1", "title", "large"], group: "Basic" },
|
|
29
|
+
{ id: "heading2", title: "Heading 2", description: "Medium heading", icon: "H2", keywords: ["h2", "subtitle"], group: "Basic" },
|
|
30
|
+
{ id: "heading3", title: "Heading 3", description: "Small heading", icon: "H3", keywords: ["h3"], group: "Basic" },
|
|
31
|
+
|
|
32
|
+
// Lists
|
|
33
|
+
{ id: "bulletList", title: "Bullet List", description: "Unordered list", icon: "•", keywords: ["ul", "unordered", "bullets"], group: "Lists" },
|
|
34
|
+
{ id: "orderedList", title: "Numbered List", description: "Ordered list", icon: "1.", keywords: ["ol", "numbered", "numbers"], group: "Lists" },
|
|
35
|
+
{ id: "taskList", title: "Task List", description: "Checklist with checkboxes", icon: "☐", keywords: ["todo", "checkbox", "checklist"], group: "Lists" },
|
|
36
|
+
|
|
37
|
+
// Blocks
|
|
38
|
+
{ id: "blockquote", title: "Quote", description: "Quote block", icon: "❝", keywords: ["quote", "citation", "pullquote"], group: "Blocks" },
|
|
39
|
+
{ id: "codeBlock", title: "Code Block", description: "Code with syntax highlighting", icon: "</>", keywords: ["code", "pre", "syntax"], group: "Blocks" },
|
|
40
|
+
{ id: "preformatted", title: "Plain Text", description: "Preformatted text for ASCII art", icon: "⎔", keywords: ["ascii", "pre", "monospace", "plain"], group: "Blocks" },
|
|
41
|
+
{ id: "divider", title: "Divider", description: "Horizontal line", icon: "—", keywords: ["hr", "line", "separator"], group: "Blocks" },
|
|
42
|
+
|
|
43
|
+
// Media
|
|
44
|
+
{ id: "image", title: "Image", description: "Upload or embed an image", icon: "🖼", keywords: ["img", "picture", "photo"], group: "Media" },
|
|
45
|
+
{ id: "file", title: "File", description: "Upload a file attachment", icon: "📎", keywords: ["upload", "attachment", "document", "pdf"], group: "Media" },
|
|
46
|
+
{ id: "youtube", title: "YouTube", description: "Embed a YouTube video", icon: "▶", keywords: ["video", "embed", "youtube"], group: "Media" },
|
|
47
|
+
{ id: "table", title: "Table", description: "Insert a table", icon: "⊞", keywords: ["grid", "data", "rows", "columns"], group: "Media" },
|
|
48
|
+
|
|
49
|
+
// Advanced
|
|
50
|
+
{ id: "section", title: "Section", description: "Page section with width control", icon: "▢", keywords: ["layout", "container", "wrapper"], group: "Advanced" },
|
|
51
|
+
{ id: "documentSection", title: "Document Section", description: "Collapsible section with title", icon: "📑", keywords: ["section", "group", "collapse", "outline", "heading"], group: "Advanced" },
|
|
52
|
+
{ id: "toggle", title: "Toggle", description: "Collapsible content block", icon: "▸", keywords: ["collapse", "expand", "accordion", "details"], group: "Advanced" },
|
|
53
|
+
{ id: "columns2", title: "2 Columns", description: "Side-by-side columns", icon: "▥", keywords: ["layout", "grid", "split", "two"], group: "Advanced" },
|
|
54
|
+
{ id: "columns3", title: "3 Columns", description: "Three column layout", icon: "▦", keywords: ["layout", "grid", "three"], group: "Advanced" },
|
|
55
|
+
{ id: "calloutInfo", title: "Info Callout", description: "Informational note", icon: "ℹ️", keywords: ["callout", "note", "info", "alert"], group: "Advanced" },
|
|
56
|
+
{ id: "calloutWarning", title: "Warning Callout", description: "Warning or caution", icon: "⚠️", keywords: ["callout", "warning", "caution", "alert"], group: "Advanced" },
|
|
57
|
+
{ id: "calloutTip", title: "Tip Callout", description: "Helpful tip", icon: "💡", keywords: ["callout", "tip", "hint", "idea"], group: "Advanced" },
|
|
58
|
+
|
|
59
|
+
// Data (v0.6.0)
|
|
60
|
+
{ id: "footnote", title: "Footnote", description: "Add a footnote reference", icon: "¹", keywords: ["footnote", "reference", "citation", "note", "academic"], group: "Data" },
|
|
61
|
+
{ id: "toc", title: "Table of Contents", description: "Auto-generated navigation", icon: "📑", keywords: ["toc", "contents", "navigation", "index", "outline"], group: "Data" },
|
|
62
|
+
{ id: "database", title: "Database", description: "Inline database with views", icon: "🗃️", keywords: ["database", "notion", "table", "kanban", "board"], group: "Data" },
|
|
63
|
+
{ id: "databaseBoard", title: "Kanban Board", description: "Database with board view", icon: "▣", keywords: ["kanban", "board", "trello", "tasks"], group: "Data" },
|
|
64
|
+
{ id: "databaseGallery", title: "Gallery", description: "Database with gallery view", icon: "⊟", keywords: ["gallery", "cards", "grid"], group: "Data" },
|
|
65
|
+
|
|
66
|
+
// Embeds
|
|
67
|
+
{ id: "embed", title: "Embed", description: "Embed from URL", icon: "🔗", keywords: ["embed", "url", "link", "website"], group: "Media" },
|
|
68
|
+
{ id: "embedTwitter", title: "Twitter/X", description: "Embed a tweet", icon: "𝕏", keywords: ["twitter", "x", "tweet", "social"], group: "Media" },
|
|
69
|
+
{ id: "embedInstagram", title: "Instagram", description: "Embed Instagram post", icon: "📷", keywords: ["instagram", "ig", "social", "photo"], group: "Media" },
|
|
70
|
+
{ id: "embedFigma", title: "Figma", description: "Embed Figma design", icon: "◈", keywords: ["figma", "design", "prototype"], group: "Media" },
|
|
71
|
+
{ id: "embedLoom", title: "Loom", description: "Embed Loom video", icon: "🎥", keywords: ["loom", "video", "recording"], group: "Media" },
|
|
72
|
+
{ id: "embedCodePen", title: "CodePen", description: "Embed CodePen", icon: "⌨", keywords: ["codepen", "code", "demo"], group: "Media" },
|
|
73
|
+
{ id: "embedSpotify", title: "Spotify", description: "Embed Spotify track", icon: "🎵", keywords: ["spotify", "music", "audio"], group: "Media" },
|
|
74
|
+
|
|
75
|
+
// Export (v0.7.0) - requires export_commands extension
|
|
76
|
+
{ id: "exportMarkdown", title: "Export Markdown", description: "Download as .md file", icon: "↓", keywords: ["export", "download", "markdown", "md"], group: "Export", requiresCommand: "downloadMarkdown" },
|
|
77
|
+
{ id: "exportHTML", title: "Export HTML", description: "Download as .html file", icon: "↓", keywords: ["export", "download", "html"], group: "Export", requiresCommand: "downloadHTML" },
|
|
78
|
+
{ id: "exportPDF", title: "Export PDF", description: "Download as .pdf file", icon: "↓", keywords: ["export", "download", "pdf", "print"], group: "Export", requiresCommand: "downloadPDF" },
|
|
79
|
+
{ id: "copyMarkdown", title: "Copy as Markdown", description: "Copy content to clipboard", icon: "📋", keywords: ["copy", "clipboard", "markdown"], group: "Export", requiresCommand: "copyMarkdown" },
|
|
80
|
+
{ id: "copyHTML", title: "Copy as HTML", description: "Copy HTML to clipboard", icon: "📋", keywords: ["copy", "clipboard", "html"], group: "Export", requiresCommand: "copyHTML" }
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
export const SlashCommands = Extension.create({
|
|
84
|
+
name: "slashCommands",
|
|
85
|
+
|
|
86
|
+
addOptions() {
|
|
87
|
+
return {
|
|
88
|
+
commands: DEFAULT_COMMANDS,
|
|
89
|
+
groups: ["Basic", "Lists", "Blocks", "Media", "Data", "Advanced", "Export"],
|
|
90
|
+
maxSuggestions: 10,
|
|
91
|
+
char: "/",
|
|
92
|
+
startOfLine: false,
|
|
93
|
+
allowSpaces: false,
|
|
94
|
+
decorationTag: "span",
|
|
95
|
+
decorationClass: "inkpen-slash-decoration",
|
|
96
|
+
suggestionClass: "inkpen-slash-menu",
|
|
97
|
+
itemClass: "inkpen-slash-menu__item",
|
|
98
|
+
activeClass: "is-selected",
|
|
99
|
+
groupClass: "inkpen-slash-menu__group",
|
|
100
|
+
// Callbacks
|
|
101
|
+
onOpen: null,
|
|
102
|
+
onClose: null,
|
|
103
|
+
onSelect: null
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
addProseMirrorPlugins() {
|
|
108
|
+
const options = this.options
|
|
109
|
+
const editor = this.editor
|
|
110
|
+
|
|
111
|
+
// Normalize commands: support both {id, title} and {name, label} formats
|
|
112
|
+
const normalizeCommand = (cmd) => ({
|
|
113
|
+
...cmd,
|
|
114
|
+
id: cmd.id || cmd.name,
|
|
115
|
+
title: cmd.title || cmd.label
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Filter commands based on query and availability
|
|
119
|
+
const filterCommands = (query) => {
|
|
120
|
+
const commands = options.commands.map(normalizeCommand)
|
|
121
|
+
const maxSuggestions = options.maxSuggestions
|
|
122
|
+
|
|
123
|
+
const availableCommands = commands.filter(cmd => {
|
|
124
|
+
if (cmd.requiresCommand) {
|
|
125
|
+
return editor.commands[cmd.requiresCommand] !== undefined
|
|
126
|
+
}
|
|
127
|
+
return true
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!query) {
|
|
131
|
+
return availableCommands.slice(0, maxSuggestions)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const q = query.toLowerCase()
|
|
135
|
+
|
|
136
|
+
return availableCommands
|
|
137
|
+
.filter(cmd => {
|
|
138
|
+
const titleMatch = cmd.title?.toLowerCase().includes(q)
|
|
139
|
+
const keywordMatch = cmd.keywords?.some(k => k.toLowerCase().includes(q))
|
|
140
|
+
const descMatch = cmd.description?.toLowerCase().includes(q)
|
|
141
|
+
return titleMatch || keywordMatch || descMatch
|
|
142
|
+
})
|
|
143
|
+
.slice(0, maxSuggestions)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Group items by their group property
|
|
147
|
+
const groupItems = (items) => {
|
|
148
|
+
const groups = options.groups
|
|
149
|
+
const grouped = {}
|
|
150
|
+
|
|
151
|
+
groups.forEach(group => { grouped[group] = [] })
|
|
152
|
+
|
|
153
|
+
items.forEach(item => {
|
|
154
|
+
const group = item.group || "Other"
|
|
155
|
+
if (!grouped[group]) grouped[group] = []
|
|
156
|
+
grouped[group].push(item)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
Object.keys(grouped).forEach(key => {
|
|
160
|
+
if (grouped[key].length === 0) delete grouped[key]
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return grouped
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Execute a command by ID
|
|
167
|
+
const executeCommand = (commandId) => {
|
|
168
|
+
const chain = editor.chain().focus()
|
|
169
|
+
|
|
170
|
+
switch (commandId) {
|
|
171
|
+
case "paragraph": chain.setParagraph().run(); break
|
|
172
|
+
case "heading1": chain.toggleHeading({ level: 1 }).run(); break
|
|
173
|
+
case "heading2": chain.toggleHeading({ level: 2 }).run(); break
|
|
174
|
+
case "heading3": chain.toggleHeading({ level: 3 }).run(); break
|
|
175
|
+
case "bulletList": chain.toggleBulletList().run(); break
|
|
176
|
+
case "orderedList": chain.toggleOrderedList().run(); break
|
|
177
|
+
case "taskList": chain.toggleTaskList().run(); break
|
|
178
|
+
case "blockquote": chain.toggleBlockquote().run(); break
|
|
179
|
+
case "codeBlock": chain.toggleCodeBlock().run(); break
|
|
180
|
+
case "preformatted":
|
|
181
|
+
if (editor.commands.togglePreformatted) chain.togglePreformatted().run()
|
|
182
|
+
break
|
|
183
|
+
case "divider": case "horizontalRule":
|
|
184
|
+
chain.setHorizontalRule().run(); break
|
|
185
|
+
case "image": {
|
|
186
|
+
const url = prompt("Enter image URL:")
|
|
187
|
+
if (url) {
|
|
188
|
+
if (editor.commands.setEnhancedImage) chain.setEnhancedImage({ src: url }).run()
|
|
189
|
+
else chain.setImage({ src: url }).run()
|
|
190
|
+
}
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
case "file":
|
|
194
|
+
if (editor.commands.uploadFile) {
|
|
195
|
+
const input = document.createElement("input")
|
|
196
|
+
input.type = "file"; input.accept = "*/*"
|
|
197
|
+
input.onchange = (e) => { if (e.target.files?.[0]) editor.commands.uploadFile(e.target.files[0]) }
|
|
198
|
+
input.click()
|
|
199
|
+
}
|
|
200
|
+
break
|
|
201
|
+
case "youtube": {
|
|
202
|
+
const url = prompt("Enter YouTube URL:")
|
|
203
|
+
if (url) chain.setYoutubeVideo({ src: url }).run()
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
case "table": chain.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); break
|
|
207
|
+
case "section":
|
|
208
|
+
if (editor.commands.insertSection) chain.insertSection().run()
|
|
209
|
+
break
|
|
210
|
+
case "documentSection":
|
|
211
|
+
if (editor.commands.insertDocumentSection) chain.insertDocumentSection().run()
|
|
212
|
+
break
|
|
213
|
+
case "toggle":
|
|
214
|
+
if (editor.commands.insertToggle) chain.insertToggle().run()
|
|
215
|
+
break
|
|
216
|
+
case "columns2":
|
|
217
|
+
if (editor.commands.insertColumns) chain.insertColumns({ count: 2 }).run()
|
|
218
|
+
break
|
|
219
|
+
case "columns3":
|
|
220
|
+
if (editor.commands.insertColumns) chain.insertColumns({ count: 3 }).run()
|
|
221
|
+
break
|
|
222
|
+
case "calloutInfo":
|
|
223
|
+
if (editor.commands.insertCallout) chain.insertCallout({ type: "info" }).run()
|
|
224
|
+
break
|
|
225
|
+
case "calloutWarning":
|
|
226
|
+
if (editor.commands.insertCallout) chain.insertCallout({ type: "warning" }).run()
|
|
227
|
+
break
|
|
228
|
+
case "calloutTip":
|
|
229
|
+
if (editor.commands.insertCallout) chain.insertCallout({ type: "tip" }).run()
|
|
230
|
+
break
|
|
231
|
+
case "embed": case "embedTwitter": case "embedInstagram":
|
|
232
|
+
case "embedFigma": case "embedLoom": case "embedCodePen": case "embedSpotify": {
|
|
233
|
+
if (editor.commands.insertEmbed) {
|
|
234
|
+
const url = prompt("Enter URL to embed:")
|
|
235
|
+
if (url) editor.commands.insertEmbed(url)
|
|
236
|
+
}
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
case "footnote":
|
|
240
|
+
if (editor.commands.insertFootnoteReference) chain.insertFootnoteReference().run()
|
|
241
|
+
break
|
|
242
|
+
case "toc":
|
|
243
|
+
if (editor.commands.insertTableOfContents) chain.insertTableOfContents().run()
|
|
244
|
+
break
|
|
245
|
+
case "database":
|
|
246
|
+
if (editor.commands.insertDatabase) chain.insertDatabase({ view: "table" }).run()
|
|
247
|
+
break
|
|
248
|
+
case "databaseBoard":
|
|
249
|
+
if (editor.commands.insertDatabase) chain.insertDatabase({ view: "board" }).run()
|
|
250
|
+
break
|
|
251
|
+
case "databaseGallery":
|
|
252
|
+
if (editor.commands.insertDatabase) chain.insertDatabase({ view: "gallery" }).run()
|
|
253
|
+
break
|
|
254
|
+
case "exportMarkdown": editor.commands.downloadMarkdown?.(); break
|
|
255
|
+
case "exportHTML": editor.commands.downloadHTML?.(); break
|
|
256
|
+
case "exportPDF": editor.commands.downloadPDF?.(); break
|
|
257
|
+
case "copyMarkdown": editor.commands.copyMarkdown?.(); break
|
|
258
|
+
case "copyHTML": editor.commands.copyHTML?.(); break
|
|
259
|
+
default: {
|
|
260
|
+
// Dispatch custom event for app-level handling (e.g., embed commands)
|
|
261
|
+
const event = new CustomEvent("inkpen:slash-command", {
|
|
262
|
+
detail: { commandId },
|
|
263
|
+
bubbles: true,
|
|
264
|
+
cancelable: true
|
|
265
|
+
})
|
|
266
|
+
editor.view.dom.dispatchEvent(event)
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return [
|
|
273
|
+
Suggestion({
|
|
274
|
+
editor: this.editor,
|
|
275
|
+
char: options.char,
|
|
276
|
+
startOfLine: options.startOfLine,
|
|
277
|
+
allowSpaces: options.allowSpaces,
|
|
278
|
+
decorationTag: options.decorationTag,
|
|
279
|
+
decorationClass: options.decorationClass,
|
|
280
|
+
|
|
281
|
+
command: ({ editor, range, props }) => {
|
|
282
|
+
editor.chain().focus().deleteRange(range).run()
|
|
283
|
+
executeCommand(props.id)
|
|
284
|
+
options.onSelect?.(props)
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
items: ({ query }) => {
|
|
288
|
+
return filterCommands(query)
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
render: () => {
|
|
292
|
+
let popup = null
|
|
293
|
+
let selectedIndex = 0
|
|
294
|
+
let items = []
|
|
295
|
+
let command = null
|
|
296
|
+
|
|
297
|
+
const createPopup = () => {
|
|
298
|
+
const el = document.createElement("div")
|
|
299
|
+
el.className = options.suggestionClass
|
|
300
|
+
el.setAttribute("role", "listbox")
|
|
301
|
+
el.setAttribute("aria-label", "Slash commands")
|
|
302
|
+
document.body.appendChild(el)
|
|
303
|
+
return el
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const updatePopup = (filteredItems, newCommand) => {
|
|
307
|
+
command = newCommand
|
|
308
|
+
items = filteredItems
|
|
309
|
+
selectedIndex = 0
|
|
310
|
+
|
|
311
|
+
if (!popup) return
|
|
312
|
+
if (items.length === 0) {
|
|
313
|
+
popup.innerHTML = `<div class="${options.suggestionClass}__empty">No results</div>`
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const grouped = groupItems(items)
|
|
318
|
+
|
|
319
|
+
popup.innerHTML = Object.entries(grouped).map(([group, groupItems]) => `
|
|
320
|
+
<div class="${options.groupClass}">
|
|
321
|
+
<div class="${options.groupClass}-title">${group}</div>
|
|
322
|
+
${groupItems.map((item, i) => {
|
|
323
|
+
const globalIndex = items.indexOf(item)
|
|
324
|
+
return `
|
|
325
|
+
<button type="button"
|
|
326
|
+
class="${options.itemClass} ${globalIndex === selectedIndex ? options.activeClass : ""}"
|
|
327
|
+
role="option"
|
|
328
|
+
aria-selected="${globalIndex === selectedIndex}"
|
|
329
|
+
data-index="${globalIndex}">
|
|
330
|
+
<span class="${options.itemClass}-icon">${item.icon || ""}</span>
|
|
331
|
+
<span class="${options.itemClass}-content">
|
|
332
|
+
<span class="${options.itemClass}-title">${item.title || ""}</span>
|
|
333
|
+
<span class="${options.itemClass}-description">${item.description || ""}</span>
|
|
334
|
+
</span>
|
|
335
|
+
</button>
|
|
336
|
+
`
|
|
337
|
+
}).join("")}
|
|
338
|
+
</div>
|
|
339
|
+
`).join("")
|
|
340
|
+
|
|
341
|
+
popup.querySelectorAll(`.${options.itemClass}`).forEach(button => {
|
|
342
|
+
button.addEventListener("click", () => {
|
|
343
|
+
const index = parseInt(button.dataset.index)
|
|
344
|
+
command(items[index])
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const positionPopup = (clientRect) => {
|
|
350
|
+
if (!popup || !clientRect) return
|
|
351
|
+
|
|
352
|
+
const rect = clientRect()
|
|
353
|
+
if (!rect) return
|
|
354
|
+
|
|
355
|
+
popup.style.position = "fixed"
|
|
356
|
+
popup.style.left = `${rect.left}px`
|
|
357
|
+
popup.style.top = `${rect.bottom + 8}px`
|
|
358
|
+
popup.style.zIndex = "10000"
|
|
359
|
+
|
|
360
|
+
const popupRect = popup.getBoundingClientRect()
|
|
361
|
+
const viewportHeight = window.innerHeight
|
|
362
|
+
const viewportWidth = window.innerWidth
|
|
363
|
+
|
|
364
|
+
if (popupRect.bottom > viewportHeight) {
|
|
365
|
+
popup.style.top = `${rect.top - popupRect.height - 8}px`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (popupRect.right > viewportWidth) {
|
|
369
|
+
popup.style.left = `${viewportWidth - popupRect.width - 16}px`
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const updateSelection = (newIndex) => {
|
|
374
|
+
selectedIndex = newIndex
|
|
375
|
+
|
|
376
|
+
popup?.querySelectorAll(`.${options.itemClass}`).forEach((button, i) => {
|
|
377
|
+
const isSelected = i === selectedIndex
|
|
378
|
+
button.classList.toggle(options.activeClass, isSelected)
|
|
379
|
+
button.setAttribute("aria-selected", isSelected.toString())
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
onStart: (props) => {
|
|
385
|
+
popup = createPopup()
|
|
386
|
+
updatePopup(props.items, props.command)
|
|
387
|
+
positionPopup(props.clientRect)
|
|
388
|
+
options.onOpen?.()
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
onUpdate: (props) => {
|
|
392
|
+
updatePopup(props.items, props.command)
|
|
393
|
+
positionPopup(props.clientRect)
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
onKeyDown: (props) => {
|
|
397
|
+
const { event } = props
|
|
398
|
+
|
|
399
|
+
if (event.key === "ArrowDown") {
|
|
400
|
+
event.preventDefault()
|
|
401
|
+
updateSelection((selectedIndex + 1) % items.length)
|
|
402
|
+
return true
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (event.key === "ArrowUp") {
|
|
406
|
+
event.preventDefault()
|
|
407
|
+
updateSelection((selectedIndex - 1 + items.length) % items.length)
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (event.key === "Enter") {
|
|
412
|
+
event.preventDefault()
|
|
413
|
+
if (items[selectedIndex] && command) {
|
|
414
|
+
command(items[selectedIndex])
|
|
415
|
+
}
|
|
416
|
+
return true
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (event.key === "Escape") {
|
|
420
|
+
event.preventDefault()
|
|
421
|
+
return true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return false
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
onExit: () => {
|
|
428
|
+
popup?.remove()
|
|
429
|
+
popup = null
|
|
430
|
+
options.onClose?.()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
export default SlashCommands
|