@joinezco/markdown-editor 0.0.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.
Files changed (168) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +9 -0
  3. package/TEST_README.md +359 -0
  4. package/dist/editor/commands/index.d.ts +2 -0
  5. package/dist/editor/commands/index.js +96 -0
  6. package/dist/editor/extensions/codeblock.d.ts +30 -0
  7. package/dist/editor/extensions/codeblock.js +390 -0
  8. package/dist/editor/extensions/filesystem.d.ts +8 -0
  9. package/dist/editor/extensions/filesystem.js +54 -0
  10. package/dist/editor/extensions/link.d.ts +1 -0
  11. package/dist/editor/extensions/link.js +24 -0
  12. package/dist/editor/extensions/slash-commands.d.ts +17 -0
  13. package/dist/editor/extensions/slash-commands.js +324 -0
  14. package/dist/editor/extensions/taskitem.d.ts +8 -0
  15. package/dist/editor/extensions/taskitem.js +67 -0
  16. package/dist/editor/index.d.ts +19 -0
  17. package/dist/editor/index.js +68 -0
  18. package/dist/editor/styles.d.ts +2 -0
  19. package/dist/editor/styles.js +153 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +2 -0
  22. package/index.html +16 -0
  23. package/package.json +73 -0
  24. package/public/fonts/Source_Serif_4/OFL.txt +91 -0
  25. package/public/fonts/Source_Serif_4/README.txt +128 -0
  26. package/public/fonts/Source_Serif_4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf +0 -0
  27. package/public/fonts/Source_Serif_4/SourceSerif4-VariableFont_opsz,wght.ttf +0 -0
  28. package/public/fonts/Source_Serif_4/static/SourceSerif4-Black.ttf +0 -0
  29. package/public/fonts/Source_Serif_4/static/SourceSerif4-BlackItalic.ttf +0 -0
  30. package/public/fonts/Source_Serif_4/static/SourceSerif4-Bold.ttf +0 -0
  31. package/public/fonts/Source_Serif_4/static/SourceSerif4-BoldItalic.ttf +0 -0
  32. package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBold.ttf +0 -0
  33. package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBoldItalic.ttf +0 -0
  34. package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLight.ttf +0 -0
  35. package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLightItalic.ttf +0 -0
  36. package/public/fonts/Source_Serif_4/static/SourceSerif4-Italic.ttf +0 -0
  37. package/public/fonts/Source_Serif_4/static/SourceSerif4-Light.ttf +0 -0
  38. package/public/fonts/Source_Serif_4/static/SourceSerif4-LightItalic.ttf +0 -0
  39. package/public/fonts/Source_Serif_4/static/SourceSerif4-Medium.ttf +0 -0
  40. package/public/fonts/Source_Serif_4/static/SourceSerif4-MediumItalic.ttf +0 -0
  41. package/public/fonts/Source_Serif_4/static/SourceSerif4-Regular.ttf +0 -0
  42. package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBold.ttf +0 -0
  43. package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBoldItalic.ttf +0 -0
  44. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Black.ttf +0 -0
  45. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BlackItalic.ttf +0 -0
  46. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Bold.ttf +0 -0
  47. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BoldItalic.ttf +0 -0
  48. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBold.ttf +0 -0
  49. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBoldItalic.ttf +0 -0
  50. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLight.ttf +0 -0
  51. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLightItalic.ttf +0 -0
  52. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Italic.ttf +0 -0
  53. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Light.ttf +0 -0
  54. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-LightItalic.ttf +0 -0
  55. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Medium.ttf +0 -0
  56. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-MediumItalic.ttf +0 -0
  57. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Regular.ttf +0 -0
  58. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBold.ttf +0 -0
  59. package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBoldItalic.ttf +0 -0
  60. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Black.ttf +0 -0
  61. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BlackItalic.ttf +0 -0
  62. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Bold.ttf +0 -0
  63. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BoldItalic.ttf +0 -0
  64. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBold.ttf +0 -0
  65. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBoldItalic.ttf +0 -0
  66. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLight.ttf +0 -0
  67. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLightItalic.ttf +0 -0
  68. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Italic.ttf +0 -0
  69. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Light.ttf +0 -0
  70. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-LightItalic.ttf +0 -0
  71. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Medium.ttf +0 -0
  72. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-MediumItalic.ttf +0 -0
  73. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Regular.ttf +0 -0
  74. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBold.ttf +0 -0
  75. package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBoldItalic.ttf +0 -0
  76. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Black.ttf +0 -0
  77. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BlackItalic.ttf +0 -0
  78. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Bold.ttf +0 -0
  79. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BoldItalic.ttf +0 -0
  80. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBold.ttf +0 -0
  81. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBoldItalic.ttf +0 -0
  82. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLight.ttf +0 -0
  83. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLightItalic.ttf +0 -0
  84. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Italic.ttf +0 -0
  85. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Light.ttf +0 -0
  86. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-LightItalic.ttf +0 -0
  87. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Medium.ttf +0 -0
  88. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-MediumItalic.ttf +0 -0
  89. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Regular.ttf +0 -0
  90. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBold.ttf +0 -0
  91. package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBoldItalic.ttf +0 -0
  92. package/public/snapshot.bin +0 -0
  93. package/src/App.css +236 -0
  94. package/src/App.tsx +73 -0
  95. package/src/index.css +63 -0
  96. package/src/lib/editor/commands/index.ts +110 -0
  97. package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-checked-task-item-when--x--is-typed-1.png +0 -0
  98. package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-task-item-when-----is-typed-1.png +0 -0
  99. package/src/lib/editor/extensions/bullet-to-task.test.ts +38 -0
  100. package/src/lib/editor/extensions/codeblock.ts +447 -0
  101. package/src/lib/editor/extensions/filesystem.ts +68 -0
  102. package/src/lib/editor/extensions/link.ts +31 -0
  103. package/src/lib/editor/extensions/slash-commands.ts +383 -0
  104. package/src/lib/editor/extensions/taskitem.ts +86 -0
  105. package/src/lib/editor/index.ts +82 -0
  106. package/src/lib/editor/styles.ts +166 -0
  107. package/src/lib/index.ts +3 -0
  108. package/src/lib/types/css.d.ts +4 -0
  109. package/src/main.tsx +10 -0
  110. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
  111. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
  112. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
  113. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
  114. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
  115. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
  116. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
  117. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
  118. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
  119. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
  120. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
  121. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
  122. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
  123. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
  124. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
  125. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
  126. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
  127. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
  128. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
  129. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
  130. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
  131. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
  132. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
  133. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
  134. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
  135. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
  136. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
  137. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
  138. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
  139. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
  140. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
  141. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
  142. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
  143. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
  144. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
  145. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
  146. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
  147. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
  148. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
  149. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
  150. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
  151. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
  152. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
  153. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
  154. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
  155. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
  156. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
  157. package/src/test/editor.test.ts +346 -0
  158. package/src/test/example.ts +92 -0
  159. package/src/test/extensions.test.ts +333 -0
  160. package/src/test/setup.ts +55 -0
  161. package/src/test/utils.ts +212 -0
  162. package/src/vite-env.d.ts +1 -0
  163. package/tsconfig.app.json +29 -0
  164. package/tsconfig.json +7 -0
  165. package/tsconfig.lib.json +17 -0
  166. package/tsconfig.node.json +24 -0
  167. package/vite.config.ts +89 -0
  168. package/vitest.config.ts +65 -0
@@ -0,0 +1,383 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import { PluginKey } from '@tiptap/pm/state'
3
+ import { Plugin } from '@tiptap/pm/state'
4
+ import tippy, { Instance as TippyInstance } from 'tippy.js'
5
+
6
+ export interface SlashCommand {
7
+ title: string
8
+ description: string
9
+ icon?: string
10
+ command: ({ editor, range }: { editor: any; range: any }) => void
11
+ }
12
+
13
+ export interface SlashCommandsOptions {
14
+ commands: SlashCommand[]
15
+ char: string
16
+ allowSpaces: boolean
17
+ startOfLine: boolean
18
+ }
19
+
20
+ export const SlashCommands = Extension.create<SlashCommandsOptions>({
21
+ name: 'slashCommands',
22
+
23
+ addOptions() {
24
+ return {
25
+ commands: [],
26
+ char: '/',
27
+ allowSpaces: false,
28
+ startOfLine: false,
29
+ }
30
+ },
31
+
32
+ addProseMirrorPlugins() {
33
+ let slashView: SlashCommandsView | null = null
34
+
35
+ return [
36
+ new Plugin({
37
+ key: new PluginKey('slashCommands'),
38
+ view: () => {
39
+ slashView = new SlashCommandsView({
40
+ editor: this.editor,
41
+ commands: this.options.commands,
42
+ char: this.options.char,
43
+ allowSpaces: this.options.allowSpaces,
44
+ startOfLine: this.options.startOfLine,
45
+ })
46
+ return slashView
47
+ },
48
+ props: {
49
+ handleKeyDown: (_, event) => {
50
+ if (slashView) {
51
+ // Track when "/" is typed to distinguish from cursor movement
52
+ if (event.key === '/') {
53
+ slashView.lastInputWasSlash = true
54
+ } else {
55
+ slashView.lastInputWasSlash = false
56
+ }
57
+
58
+ if (slashView.dropdown) {
59
+ return slashView.handleKeyDown(event)
60
+ }
61
+ }
62
+ return false
63
+ }
64
+ }
65
+ }),
66
+ ]
67
+ },
68
+ })
69
+
70
+ class SlashCommandsView {
71
+ public editor: any
72
+ public commands: SlashCommand[]
73
+ public char: string
74
+ public allowSpaces: boolean
75
+ public startOfLine: boolean
76
+ public dropdown: HTMLElement | null = null
77
+ public popup: TippyInstance | null = null
78
+ public range: any = null
79
+ public query: string = ''
80
+ public selectedIndex: number = 0
81
+ public lastInputWasSlash: boolean = false
82
+ private outsideClickHandler: ((event: MouseEvent) => void) | null = null
83
+
84
+ constructor({
85
+ editor,
86
+ commands,
87
+ char,
88
+ allowSpaces,
89
+ startOfLine,
90
+ }: {
91
+ editor: any
92
+ commands: SlashCommand[]
93
+ char: string
94
+ allowSpaces: boolean
95
+ startOfLine: boolean
96
+ }) {
97
+ this.editor = editor
98
+ this.commands = commands
99
+ this.char = char
100
+ this.allowSpaces = allowSpaces
101
+ this.startOfLine = startOfLine
102
+
103
+ this.editor.on('selectionUpdate', this.selectionUpdate.bind(this))
104
+ this.editor.on('update', this.selectionUpdate.bind(this))
105
+ }
106
+
107
+ selectionUpdate() {
108
+ const { selection } = this.editor.state
109
+ const { $from } = selection
110
+
111
+ // Get text before cursor in current node
112
+ const currentNode = $from.parent
113
+ const currentNodeText = currentNode.textContent
114
+ const posInNode = $from.parentOffset
115
+ const textBeforeCursor = currentNodeText.slice(0, posInNode)
116
+
117
+ // Look for slash command pattern
118
+ const match = textBeforeCursor.match(new RegExp(`${this.char}([^\\s${this.char}]*)$`))
119
+
120
+ if (match && this.lastInputWasSlash) {
121
+ const query = match[1]
122
+ const from = $from.pos - match[0].length
123
+ const to = $from.pos
124
+
125
+ this.range = { from, to }
126
+ this.query = query
127
+ this.selectedIndex = 0
128
+ this.showSuggestions()
129
+ } else {
130
+ this.hideSuggestions()
131
+ // Reset the flag when we're not in a slash command context
132
+ if (!match) {
133
+ this.lastInputWasSlash = false
134
+ }
135
+ }
136
+ }
137
+
138
+ createDropdown(): HTMLElement {
139
+ const dropdown = document.createElement('div')
140
+ dropdown.className = 'slash-commands-dropdown'
141
+
142
+ const filteredCommands = this.getFilteredCommands()
143
+
144
+ if (filteredCommands.length === 0) {
145
+ const noResults = document.createElement('div')
146
+ noResults.className = 'slash-command-item'
147
+ noResults.innerHTML = `
148
+ <div class="slash-command-content">
149
+ <div class="slash-command-text">
150
+ <div class="slash-command-title">No results</div>
151
+ <div class="slash-command-description">No commands found for "${this.query}"</div>
152
+ </div>
153
+ </div>
154
+ `
155
+ dropdown.appendChild(noResults)
156
+ } else {
157
+ filteredCommands.forEach((command, index) => {
158
+ const item = document.createElement('button')
159
+ item.className = `slash-command-item ${index === this.selectedIndex ? 'selected' : ''}`
160
+ item.innerHTML = `
161
+ <div class="slash-command-content">
162
+ ${command.icon ? `<span class="slash-command-icon">${command.icon}</span>` : ''}
163
+ <div class="slash-command-text">
164
+ <div class="slash-command-title">${command.title}</div>
165
+ <div class="slash-command-description">${command.description}</div>
166
+ </div>
167
+ </div>
168
+ `
169
+
170
+ item.addEventListener('click', () => this.selectCommand(command))
171
+ dropdown.appendChild(item)
172
+ })
173
+ }
174
+
175
+ // Add keyboard event handling
176
+ dropdown.addEventListener('keydown', this.handleKeyDown.bind(this))
177
+
178
+ return dropdown
179
+ }
180
+
181
+ handleKeyDown(event: KeyboardEvent): boolean {
182
+ const filteredCommands = this.getFilteredCommands()
183
+
184
+ switch (event.key) {
185
+ case 'ArrowUp':
186
+ event.preventDefault()
187
+ if (this.selectedIndex === 0) {
188
+ // Exit dropdown when at the start and pressing up
189
+ this.hideSuggestions()
190
+ return false // Let editor handle the event
191
+ } else {
192
+ this.selectedIndex = this.selectedIndex - 1
193
+ this.updateSelection()
194
+ }
195
+ return true
196
+ case 'ArrowDown':
197
+ event.preventDefault()
198
+ if (this.selectedIndex === filteredCommands.length - 1) {
199
+ // Cycle to start when at the end and pressing down
200
+ this.selectedIndex = 0
201
+ } else {
202
+ this.selectedIndex = this.selectedIndex + 1
203
+ }
204
+ this.updateSelection()
205
+ return true
206
+ case 'Enter':
207
+ event.preventDefault()
208
+ const selectedCommand = filteredCommands[this.selectedIndex]
209
+ if (selectedCommand) {
210
+ this.selectCommand(selectedCommand)
211
+ }
212
+ return true
213
+ case 'Escape':
214
+ event.preventDefault()
215
+ this.hideSuggestions()
216
+ return true
217
+ default:
218
+ return false
219
+ }
220
+ }
221
+
222
+ updateSelection() {
223
+ if (!this.dropdown) return
224
+
225
+ const items = this.dropdown.querySelectorAll('.slash-command-item')
226
+ items.forEach((item, index) => {
227
+ if (index === this.selectedIndex) {
228
+ item.classList.add('selected')
229
+ } else {
230
+ item.classList.remove('selected')
231
+ }
232
+ })
233
+ }
234
+
235
+ showSuggestions() {
236
+ // Always recreate the dropdown to ensure fresh content and event listeners
237
+ this.dropdown = this.createDropdown()
238
+
239
+ if (this.popup) {
240
+ // Update existing popup content
241
+ this.popup.setContent(this.dropdown)
242
+ } else {
243
+ // Create new popup
244
+ const instances = tippy(document.body, {
245
+ getReferenceClientRect: () => {
246
+ if (!this.range) {
247
+ // Return a default rect if range is null
248
+ return new DOMRect(0, 0, 0, 0)
249
+ }
250
+ const { view } = this.editor
251
+ const { from } = this.range
252
+ const start = view.coordsAtPos(from)
253
+ const end = view.coordsAtPos(this.range.to)
254
+
255
+ return {
256
+ top: start.top,
257
+ bottom: end.bottom,
258
+ left: start.left,
259
+ right: end.right,
260
+ width: end.right - start.left,
261
+ height: end.bottom - start.top,
262
+ x: start.left,
263
+ y: start.top,
264
+ toJSON: () => ({
265
+ top: start.top,
266
+ bottom: end.bottom,
267
+ left: start.left,
268
+ right: end.right,
269
+ width: end.right - start.left,
270
+ height: end.bottom - start.top,
271
+ x: start.left,
272
+ y: start.top,
273
+ })
274
+ } as DOMRect
275
+ },
276
+ appendTo: () => document.body,
277
+ content: this.dropdown,
278
+ showOnCreate: true,
279
+ interactive: true,
280
+ trigger: 'manual',
281
+ placement: 'bottom-start',
282
+ theme: 'slash-commands',
283
+ maxWidth: 'none',
284
+ onShow: () => {
285
+ // Add outside click handler when dropdown is shown
286
+ this.addOutsideClickHandler()
287
+ },
288
+ onHide: () => {
289
+ // Remove outside click handler when dropdown is hidden
290
+ this.removeOutsideClickHandler()
291
+ }
292
+ }) as any
293
+ this.popup = instances[0]
294
+ }
295
+ }
296
+
297
+ hideSuggestions() {
298
+ this.removeOutsideClickHandler()
299
+
300
+ if (this.popup) {
301
+ this.popup.destroy()
302
+ this.popup = null
303
+ }
304
+
305
+ // Force cleanup of any remaining tippy instances
306
+ const existingTippyInstances = document.querySelectorAll('[data-tippy-root]')
307
+ existingTippyInstances.forEach(instance => {
308
+ instance.remove()
309
+ })
310
+
311
+ // Also remove any dropdown elements that might be lingering
312
+ const existingDropdowns = document.querySelectorAll('.slash-commands-dropdown')
313
+ existingDropdowns.forEach(dropdown => {
314
+ console.log('Force removing dropdown element')
315
+ dropdown.remove()
316
+ })
317
+
318
+ this.dropdown = null
319
+ this.range = null
320
+ this.query = ''
321
+ this.selectedIndex = 0
322
+ this.lastInputWasSlash = false
323
+ }
324
+
325
+ addOutsideClickHandler() {
326
+ if (this.outsideClickHandler) {
327
+ this.removeOutsideClickHandler()
328
+ }
329
+
330
+ this.outsideClickHandler = (event: MouseEvent) => {
331
+ const target = event.target as Element
332
+ // Check if click is outside the dropdown and editor
333
+ if (this.dropdown && !this.dropdown.contains(target) &&
334
+ !this.editor.view.dom.contains(target)) {
335
+ this.hideSuggestions()
336
+ }
337
+ }
338
+
339
+ // Add listener with a slight delay to avoid immediate closure
340
+ setTimeout(() => {
341
+ document.addEventListener('click', this.outsideClickHandler!, true)
342
+ }, 100)
343
+ }
344
+
345
+ removeOutsideClickHandler() {
346
+ if (this.outsideClickHandler) {
347
+ document.removeEventListener('click', this.outsideClickHandler, true)
348
+ this.outsideClickHandler = null
349
+ }
350
+ }
351
+
352
+ getFilteredCommands(): SlashCommand[] {
353
+ if (!this.query) {
354
+ return this.commands
355
+ }
356
+
357
+ return this.commands.filter(command =>
358
+ command.title.toLowerCase().includes(this.query.toLowerCase()) ||
359
+ command.description.toLowerCase().includes(this.query.toLowerCase())
360
+ )
361
+ }
362
+
363
+ selectCommand(command: SlashCommand) {
364
+ if (this.range) {
365
+ // Store the range before hiding suggestions, as hideSuggestions() might clear it
366
+ const range = this.range
367
+ console.log('Executing command, hiding suggestions first')
368
+ // Hide suggestions first to ensure dropdown closes
369
+ this.hideSuggestions()
370
+ console.log('Suggestions hidden, executing command')
371
+ // Then execute the command with the stored range
372
+ command.command({ editor: this.editor, range })
373
+ console.log('Command execution completed')
374
+ }
375
+ }
376
+
377
+ destroy() {
378
+ this.removeOutsideClickHandler()
379
+ this.hideSuggestions()
380
+ this.editor.off('selectionUpdate', this.selectionUpdate)
381
+ this.editor.off('update', this.selectionUpdate)
382
+ }
383
+ }
@@ -0,0 +1,86 @@
1
+ import { TaskItem } from '@tiptap/extension-task-item';
2
+ import { wrappingInputRule } from '@tiptap/core';
3
+
4
+ /**
5
+ * Extended TaskItem extension with configurable input regex rules.
6
+ */
7
+ export interface ExtendedTaskItemOptions {
8
+ nested: boolean;
9
+ inputRegex?: RegExp;
10
+ }
11
+
12
+ export const ExtendedTaskItem = TaskItem.extend<ExtendedTaskItemOptions>({
13
+ addOptions() {
14
+ return {
15
+ ...this.parent?.(),
16
+ nested: true,
17
+ inputRegex: /^\s*[-+*]\s\[( |x|X)\](?:\s)?$/, // Match `- [ ]` or `- [x]`, with optional space
18
+ };
19
+ },
20
+
21
+ addInputRules() {
22
+ const { inputRegex } = this.options;
23
+
24
+ return [
25
+ wrappingInputRule({
26
+ find: inputRegex!,
27
+ type: this.type,
28
+ getAttributes: (match) => {
29
+ return {
30
+ checked: match[1] === 'x' || match[1] === 'X',
31
+ };
32
+ },
33
+ })
34
+ ];
35
+ },
36
+
37
+ addNodeView() {
38
+ return ({ node, getPos, editor }) => {
39
+ const listItem = document.createElement('li');
40
+ listItem.setAttribute('data-type', 'taskItem');
41
+ listItem.setAttribute('data-checked', node.attrs.checked);
42
+
43
+ const checkboxWrapper = document.createElement('label');
44
+ checkboxWrapper.contentEditable = 'false';
45
+
46
+ const checkbox = document.createElement('input');
47
+ checkbox.type = 'checkbox';
48
+ checkbox.checked = node.attrs.checked;
49
+
50
+ // Add click handler to the checkbox
51
+ checkbox.addEventListener('change', () => {
52
+ const pos = getPos();
53
+ if (typeof pos === 'number') {
54
+ const { tr } = editor.state;
55
+ tr.setNodeMarkup(pos, undefined, {
56
+ ...node.attrs,
57
+ checked: checkbox.checked
58
+ });
59
+ editor.view.dispatch(tr);
60
+ }
61
+ });
62
+
63
+ const content = document.createElement('div');
64
+ content.style.display = 'inline';
65
+
66
+ checkboxWrapper.appendChild(checkbox);
67
+ listItem.appendChild(checkboxWrapper);
68
+ listItem.appendChild(content);
69
+
70
+ return {
71
+ dom: listItem,
72
+ contentDOM: content,
73
+ update: (updatedNode) => {
74
+ if (updatedNode.type !== this.type) {
75
+ return false;
76
+ }
77
+
78
+ // Update checkbox state when node is updated
79
+ checkbox.checked = updatedNode.attrs.checked;
80
+ listItem.setAttribute('data-checked', updatedNode.attrs.checked);
81
+ return true;
82
+ }
83
+ };
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,82 @@
1
+ import { Editor, EditorOptions, Extension } from '@tiptap/core';
2
+ import StarterKit from '@tiptap/starter-kit';
3
+ import TaskList from '@tiptap/extension-task-list';
4
+ import { TableKit } from '@tiptap/extension-table'
5
+ import { Markdown, MarkdownStorage } from 'tiptap-markdown';
6
+ import { ExtendedCodeblock } from './extensions/codeblock';
7
+ import { ExtendedTaskItem } from './extensions/taskitem';
8
+ import { FileSystem, FileSystemOptions } from './extensions/filesystem';
9
+ import { styleModule } from './styles';
10
+
11
+ import { ExtendedLink } from './extensions/link';
12
+ import { SlashCommands } from './extensions/slash-commands';
13
+ import { defaultSlashCommands } from './commands';
14
+ import { StyleModule } from 'style-mod';
15
+
16
+ export type MarkdownEditorOptions = Partial<EditorOptions> & {
17
+ extensions?: Extension[];
18
+ fs?: FileSystemOptions;
19
+ }
20
+
21
+ export type MarkdownEditor = Editor & {
22
+ storage: {
23
+ markdown: MarkdownStorage;
24
+ } & Record<string, any>;
25
+ }
26
+
27
+ /**
28
+ * Create a Markdown-ready Tiptap Editor with default extensions and options.
29
+ *
30
+ * @param options Optional overrides for the Tiptap Editor options.
31
+ * @returns An instance of Tiptap Editor.
32
+ */
33
+ export function createEditor(options: MarkdownEditorOptions = {}): MarkdownEditor {
34
+ const editor = new Editor({
35
+ extensions: [
36
+ FileSystem.configure(options.fs || {}),
37
+ ExtendedLink.configure({}),
38
+ StarterKit.configure({
39
+ codeBlock: false,
40
+ // bulletList: false, // As Markdown handles bullet lists and allows us to configure the marker to prevent task item conflicts
41
+ }),
42
+ Markdown.configure({
43
+ html: false,
44
+ tightLists: true,
45
+ tightListClass: 'tight',
46
+ bulletListMarker: '*',
47
+ linkify: true,
48
+ breaks: true,
49
+ transformPastedText: true,
50
+ transformCopiedText: true,
51
+ }),
52
+ ExtendedCodeblock,
53
+ TaskList,
54
+ ExtendedTaskItem.configure({
55
+ nested: true,
56
+ }),
57
+ TableKit.configure({
58
+ table: { resizable: true, allowTableNodeSelection: true },
59
+ }),
60
+ SlashCommands.configure({
61
+ commands: defaultSlashCommands,
62
+ }),
63
+ ...(options.extensions || []),
64
+ ],
65
+ editorProps: {
66
+ attributes: options.editorProps?.attributes || {},
67
+ ...(options.editorProps || {}),
68
+ },
69
+ content: options.content || '',
70
+ onUpdate: options.onUpdate || (() => { }),
71
+ autofocus: options.autofocus,
72
+ editable: options.editable,
73
+ injectCSS: options.injectCSS,
74
+ ...options,
75
+ });
76
+ editor.view.dom.classList.add('ezco-mde');
77
+
78
+ if (typeof document !== 'undefined') {
79
+ StyleModule.mount(document, styleModule);
80
+ }
81
+ return editor as MarkdownEditor;
82
+ }