@pilotiq/tiptap 3.10.5 → 3.10.6
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.
- package/CHANGELOG.md +745 -0
- package/boost/guidelines.md +268 -0
- package/boost/skills/pilotiq-tiptap-blocks/SKILL.md +48 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/custom-blocks.md +90 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/slash-menu-and-mentions.md +101 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/toolbar-and-extensibility.md +161 -0
- package/package.json +4 -3
- package/src/Block.ts +0 -75
- package/src/MentionProvider.ts +0 -153
- package/src/PlainTextEditor.dom.test.ts +0 -111
- package/src/PlainTextEditor.test.ts +0 -158
- package/src/PlainTextEditor.ts +0 -229
- package/src/RichTextField.test.ts +0 -447
- package/src/RichTextField.ts +0 -508
- package/src/extensions/AiInlineDiffExtension.ts +0 -286
- package/src/extensions/AiSuggestionExtension.test.ts +0 -141
- package/src/extensions/AiSuggestionExtension.ts +0 -522
- package/src/extensions/BlockNodeExtension.ts +0 -134
- package/src/extensions/DragHandleExtension.ts +0 -184
- package/src/extensions/GridExtension.test.ts +0 -31
- package/src/extensions/GridExtension.ts +0 -138
- package/src/extensions/MentionExtension.ts +0 -248
- package/src/extensions/MergeTagExtension.ts +0 -75
- package/src/extensions/SlashCommandExtension.test.ts +0 -147
- package/src/extensions/SlashCommandExtension.ts +0 -332
- package/src/extensions/TextSizeMarks.ts +0 -73
- package/src/index.ts +0 -62
- package/src/markdownExtension.ts +0 -19
- package/src/markdownStorage.ts +0 -49
- package/src/plugin.test.ts +0 -19
- package/src/plugin.ts +0 -26
- package/src/react/AiSuggestionBanner.tsx +0 -185
- package/src/react/BlockNodeView.tsx +0 -99
- package/src/react/BlockSidePanel.dom.test.tsx +0 -38
- package/src/react/BlockSidePanel.test.ts +0 -412
- package/src/react/BlockSidePanel.tsx +0 -451
- package/src/react/CollabTextRenderer.tsx +0 -228
- package/src/react/FloatingToolbar.tsx +0 -304
- package/src/react/MarkdownEditor.tsx +0 -603
- package/src/react/MentionMenu.tsx +0 -120
- package/src/react/Palette.tsx +0 -86
- package/src/react/SlashMenu.tsx +0 -129
- package/src/react/TableFloatingToolbar.tsx +0 -154
- package/src/react/TiptapEditor.dom.test.tsx +0 -112
- package/src/react/TiptapEditor.tsx +0 -777
- package/src/react/Toolbar.tsx +0 -438
- package/src/react/toolbarButtons.tsx +0 -579
- package/src/react/useAiInlineDiff.ts +0 -342
- package/src/react/useAiSuggestionBridge.ts +0 -223
- package/src/register.test.ts +0 -14
- package/src/register.ts +0 -42
- package/src/render.test.ts +0 -745
- package/src/render.ts +0 -480
- package/src/surgicalOps.ts +0 -205
- package/src/test/setup.ts +0 -64
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { buildSlashItems } from './SlashCommandExtension.js'
|
|
4
|
-
|
|
5
|
-
describe('SlashCommandExtension built-ins', () => {
|
|
6
|
-
it('always exposes the Table entry', () => {
|
|
7
|
-
const items = buildSlashItems([], [], '', {
|
|
8
|
-
hasUpload: false,
|
|
9
|
-
onInsertImage: () => {},
|
|
10
|
-
})
|
|
11
|
-
const table = items.find((i) => i.key === 'table')
|
|
12
|
-
assert.ok(table, 'expected a "table" slash entry')
|
|
13
|
-
assert.equal(table!.label, 'Table')
|
|
14
|
-
assert.equal(table!.group, 'Insert')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('omits the Image entry when no upload adapter is wired', () => {
|
|
18
|
-
const items = buildSlashItems([], [], '', {
|
|
19
|
-
hasUpload: false,
|
|
20
|
-
onInsertImage: () => {},
|
|
21
|
-
})
|
|
22
|
-
const image = items.find((i) => i.key === 'image')
|
|
23
|
-
assert.equal(image, undefined)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('surfaces the Image entry when hasUpload is true', () => {
|
|
27
|
-
const items = buildSlashItems([], [], '', {
|
|
28
|
-
hasUpload: true,
|
|
29
|
-
onInsertImage: () => {},
|
|
30
|
-
})
|
|
31
|
-
const image = items.find((i) => i.key === 'image')
|
|
32
|
-
assert.ok(image, 'expected an "image" slash entry when hasUpload')
|
|
33
|
-
assert.equal(image!.label, 'Image')
|
|
34
|
-
assert.equal(image!.group, 'Insert')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('Image command fires the onInsertImage callback', () => {
|
|
38
|
-
let calls = 0
|
|
39
|
-
const items = buildSlashItems([], [], '', {
|
|
40
|
-
hasUpload: true,
|
|
41
|
-
onInsertImage: () => { calls += 1 },
|
|
42
|
-
})
|
|
43
|
-
const image = items.find((i) => i.key === 'image')
|
|
44
|
-
// Stand-in editor — the slash command chains through `.deleteRange(...)`
|
|
45
|
-
// before firing the callback. We don't assert on the chain output;
|
|
46
|
-
// confirming `calls === 1` proves the wire is connected.
|
|
47
|
-
const stubChain = {
|
|
48
|
-
focus: () => stubChain,
|
|
49
|
-
deleteRange: () => stubChain,
|
|
50
|
-
run: () => true,
|
|
51
|
-
}
|
|
52
|
-
image!.command({
|
|
53
|
-
editor: { chain: () => stubChain } as never,
|
|
54
|
-
range: { from: 0, to: 0 } as never,
|
|
55
|
-
})
|
|
56
|
-
assert.equal(calls, 1)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('respects the search query against label / searchKey / group', () => {
|
|
60
|
-
const items = buildSlashItems([], [], 'tab', {
|
|
61
|
-
hasUpload: true,
|
|
62
|
-
onInsertImage: () => {},
|
|
63
|
-
})
|
|
64
|
-
assert.ok(items.some((i) => i.key === 'table'),
|
|
65
|
-
'expected `table` to match the "tab" query via label/searchKey')
|
|
66
|
-
assert.equal(items.some((i) => i.key === 'paragraph'), false,
|
|
67
|
-
'expected unrelated entries to drop out of the filtered list')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('exposes lead + small entries under the Style group', () => {
|
|
71
|
-
const items = buildSlashItems([], [], '', {
|
|
72
|
-
hasUpload: false,
|
|
73
|
-
onInsertImage: () => {},
|
|
74
|
-
})
|
|
75
|
-
const lead = items.find((i) => i.key === 'lead')
|
|
76
|
-
const small = items.find((i) => i.key === 'small')
|
|
77
|
-
assert.ok(lead, 'expected a "lead" slash entry')
|
|
78
|
-
assert.ok(small, 'expected a "small" slash entry')
|
|
79
|
-
assert.equal(lead!.group, 'Style')
|
|
80
|
-
assert.equal(small!.group, 'Style')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('always exposes the Details entry under the Insert group', () => {
|
|
84
|
-
const items = buildSlashItems([], [], '', {
|
|
85
|
-
hasUpload: false,
|
|
86
|
-
onInsertImage: () => {},
|
|
87
|
-
})
|
|
88
|
-
const details = items.find((i) => i.key === 'details')
|
|
89
|
-
assert.ok(details, 'expected a "details" slash entry')
|
|
90
|
-
assert.equal(details!.label, 'Collapsible block')
|
|
91
|
-
assert.equal(details!.group, 'Insert')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('Details search matches "collapsible" / "summary" / "toggle"', () => {
|
|
95
|
-
for (const q of ['collapsible', 'summary', 'toggle']) {
|
|
96
|
-
const items = buildSlashItems([], [], q, {
|
|
97
|
-
hasUpload: false,
|
|
98
|
-
onInsertImage: () => {},
|
|
99
|
-
})
|
|
100
|
-
assert.ok(
|
|
101
|
-
items.some((i) => i.key === 'details'),
|
|
102
|
-
`expected the Details entry to match search query "${q}"`,
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('exposes Two-column and Three-column grid entries under Insert', () => {
|
|
108
|
-
const items = buildSlashItems([], [], '', {
|
|
109
|
-
hasUpload: false,
|
|
110
|
-
onInsertImage: () => {},
|
|
111
|
-
})
|
|
112
|
-
const two = items.find((i) => i.key === 'grid-2')
|
|
113
|
-
const three = items.find((i) => i.key === 'grid-3')
|
|
114
|
-
assert.ok(two, 'expected a "grid-2" slash entry')
|
|
115
|
-
assert.ok(three, 'expected a "grid-3" slash entry')
|
|
116
|
-
assert.equal(two!.label, 'Two-column grid')
|
|
117
|
-
assert.equal(two!.group, 'Insert')
|
|
118
|
-
assert.equal(three!.label, 'Three-column grid')
|
|
119
|
-
assert.equal(three!.group, 'Insert')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('Grid search matches "columns" / "split" / "layout"', () => {
|
|
123
|
-
for (const q of ['columns', 'split', 'layout']) {
|
|
124
|
-
const items = buildSlashItems([], [], q, {
|
|
125
|
-
hasUpload: false,
|
|
126
|
-
onInsertImage: () => {},
|
|
127
|
-
})
|
|
128
|
-
assert.ok(
|
|
129
|
-
items.some((i) => i.key === 'grid-2'),
|
|
130
|
-
`expected the Two-column grid entry to match search query "${q}"`,
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('Three-column grid distinguishes by "three" / "3"', () => {
|
|
136
|
-
for (const q of ['three', '3']) {
|
|
137
|
-
const items = buildSlashItems([], [], q, {
|
|
138
|
-
hasUpload: false,
|
|
139
|
-
onInsertImage: () => {},
|
|
140
|
-
})
|
|
141
|
-
assert.ok(
|
|
142
|
-
items.some((i) => i.key === 'grid-3'),
|
|
143
|
-
`expected the Three-column grid entry to match search query "${q}"`,
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
})
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { Extension, type Editor, type Range } from '@tiptap/core'
|
|
2
|
-
import Suggestion, { type SuggestionOptions } from '@tiptap/suggestion'
|
|
3
|
-
import { PluginKey } from '@tiptap/pm/state'
|
|
4
|
-
|
|
5
|
-
const SLASH_PLUGIN_KEY = new PluginKey('pilotiqSlashSuggestion')
|
|
6
|
-
import type { BlockMeta } from '../Block.js'
|
|
7
|
-
|
|
8
|
-
export interface SlashItem {
|
|
9
|
-
/** Stable id used to dedupe + as React key. */
|
|
10
|
-
key: string
|
|
11
|
-
label: string
|
|
12
|
-
icon: string | undefined
|
|
13
|
-
group?: string
|
|
14
|
-
/** Free-text searched against label + group. */
|
|
15
|
-
searchKey: string
|
|
16
|
-
/** Run when the user picks this item. `range` is the slash + query slice. */
|
|
17
|
-
command: (args: { editor: Editor; range: Range }) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* State the React side of the editor needs to render the slash menu. Set to
|
|
22
|
-
* `null` when the menu should be unmounted.
|
|
23
|
-
*/
|
|
24
|
-
export interface SlashState {
|
|
25
|
-
items: SlashItem[]
|
|
26
|
-
/** Pick item — Suggestion will replace the slash range and run the command. */
|
|
27
|
-
command: (item: SlashItem) => void
|
|
28
|
-
/**
|
|
29
|
-
* Cursor / range coords in viewport space. Re-call this every layout tick
|
|
30
|
-
* (the popover positioner does) so scroll/resize tracking comes for free.
|
|
31
|
-
*/
|
|
32
|
-
clientRect: () => DOMRect | null
|
|
33
|
-
query: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SlashCommandOptions {
|
|
37
|
-
/** Custom blocks contributed by RichTextField.blocks([...]). */
|
|
38
|
-
blocks: BlockMeta[]
|
|
39
|
-
/** Merge-tag identifiers contributed by RichTextField.mergeTags([...]). */
|
|
40
|
-
mergeTags: string[]
|
|
41
|
-
/**
|
|
42
|
-
* Called whenever the menu should mount, update, or unmount. TiptapEditor
|
|
43
|
-
* holds the React state and passes a setter here.
|
|
44
|
-
*/
|
|
45
|
-
onStateChange: (state: SlashState | null) => void
|
|
46
|
-
/**
|
|
47
|
-
* `true` when the panel has wired an `UploadAdapter` (mirrors the
|
|
48
|
-
* toolbar's `attachFiles` gating). Drives whether the "Image" entry
|
|
49
|
-
* appears in the menu — it would have nowhere to upload otherwise.
|
|
50
|
-
*/
|
|
51
|
-
hasUpload: boolean
|
|
52
|
-
/**
|
|
53
|
-
* Called when the user picks the "Image" slash entry. The slash range
|
|
54
|
-
* is already deleted before this fires; the callback just opens the
|
|
55
|
-
* shared attach-files dialog (whose UI lives in `Toolbar`).
|
|
56
|
-
*/
|
|
57
|
-
onInsertImage: () => void
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* `/`-triggered slash menu. The plugin owns the suggestion lifecycle (range
|
|
62
|
-
* detection + command invocation); rendering is React-side via a Base UI
|
|
63
|
-
* Popover anchored to a virtual element. Keyboard events are NOT forwarded
|
|
64
|
-
* through the Suggestion plugin — TiptapEditor installs a document-level
|
|
65
|
-
* capture-phase keydown listener while the menu is open, because Base UI's
|
|
66
|
-
* focus manager can briefly steal focus from the editor when the popup mounts
|
|
67
|
-
* and the suggestion plugin's `handleKeyDown` only fires when the editor has
|
|
68
|
-
* focus.
|
|
69
|
-
*/
|
|
70
|
-
export const SlashCommandExtension = Extension.create<SlashCommandOptions>({
|
|
71
|
-
name: 'slashCommand',
|
|
72
|
-
|
|
73
|
-
addOptions() {
|
|
74
|
-
return {
|
|
75
|
-
blocks: [],
|
|
76
|
-
mergeTags: [],
|
|
77
|
-
onStateChange: () => {},
|
|
78
|
-
hasUpload: false,
|
|
79
|
-
onInsertImage: () => {},
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
addProseMirrorPlugins() {
|
|
84
|
-
const blocks = this.options.blocks
|
|
85
|
-
const mergeTags = this.options.mergeTags
|
|
86
|
-
const emit = this.options.onStateChange
|
|
87
|
-
const hasUpload = this.options.hasUpload
|
|
88
|
-
const onInsertImage = this.options.onInsertImage
|
|
89
|
-
|
|
90
|
-
return [
|
|
91
|
-
Suggestion({
|
|
92
|
-
pluginKey: SLASH_PLUGIN_KEY,
|
|
93
|
-
editor: this.editor,
|
|
94
|
-
char: '/',
|
|
95
|
-
startOfLine: false,
|
|
96
|
-
allowSpaces: false,
|
|
97
|
-
items: ({ query }: { query: string }) => buildSlashItems(blocks, mergeTags, query, { hasUpload, onInsertImage }),
|
|
98
|
-
command: ({ editor, range, props }: { editor: Editor; range: Range; props: SlashItem }) => {
|
|
99
|
-
props.command({ editor, range })
|
|
100
|
-
},
|
|
101
|
-
render: () => ({
|
|
102
|
-
onStart: (props) => emit(stateFrom(props)),
|
|
103
|
-
onUpdate: (props) => emit(stateFrom(props)),
|
|
104
|
-
// Keys are handled at the document level by TiptapEditor; nothing
|
|
105
|
-
// to do here. Returning false lets PM's keymap handle anything we
|
|
106
|
-
// don't intercept (typing more of the slash query, etc.).
|
|
107
|
-
onKeyDown: () => false,
|
|
108
|
-
onExit: () => emit(null),
|
|
109
|
-
}),
|
|
110
|
-
} satisfies SuggestionOptions<SlashItem, SlashItem>),
|
|
111
|
-
]
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
function stateFrom(props: {
|
|
116
|
-
items: SlashItem[]
|
|
117
|
-
command: (item: SlashItem) => void
|
|
118
|
-
clientRect?: (() => DOMRect | null) | null
|
|
119
|
-
query: string
|
|
120
|
-
}): SlashState {
|
|
121
|
-
return {
|
|
122
|
-
items: props.items,
|
|
123
|
-
command: props.command,
|
|
124
|
-
clientRect: props.clientRect ?? (() => null),
|
|
125
|
-
query: props.query,
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface SlashInsertEntries {
|
|
130
|
-
hasUpload: boolean
|
|
131
|
-
onInsertImage: () => void
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Built-in items mirror the standard rich-text-editor slash menu. Custom
|
|
135
|
-
// blocks append, then merge-tag placeholders.
|
|
136
|
-
//
|
|
137
|
-
// Exported so tests can pin down the menu contents without spinning up an
|
|
138
|
-
// editor instance — the function is pure and deterministic given its
|
|
139
|
-
// inputs.
|
|
140
|
-
export function buildSlashItems(
|
|
141
|
-
blocks: BlockMeta[],
|
|
142
|
-
mergeTags: string[],
|
|
143
|
-
query: string,
|
|
144
|
-
insert: SlashInsertEntries,
|
|
145
|
-
): SlashItem[] {
|
|
146
|
-
const builtins: SlashItem[] = [
|
|
147
|
-
{
|
|
148
|
-
key: 'paragraph', label: 'Text', icon: '¶', group: 'Basic',
|
|
149
|
-
searchKey: 'text paragraph p',
|
|
150
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('paragraph').run(),
|
|
151
|
-
},
|
|
152
|
-
...[1, 2, 3, 4, 5, 6].map((level) => ({
|
|
153
|
-
key: `heading-${level}`,
|
|
154
|
-
label: `Heading ${level}`,
|
|
155
|
-
icon: `H${level}`,
|
|
156
|
-
group: 'Headings',
|
|
157
|
-
searchKey: `heading ${level} h${level}${level === 1 ? ' title' : level === 2 ? ' subtitle' : ''}`,
|
|
158
|
-
command: ({ editor, range }: { editor: Editor; range: Range }) =>
|
|
159
|
-
editor.chain().focus().deleteRange(range).setNode('heading', { level }).run(),
|
|
160
|
-
})),
|
|
161
|
-
{
|
|
162
|
-
key: 'bullet-list', label: 'Bullet list', icon: '•', group: 'Lists',
|
|
163
|
-
searchKey: 'bullet list ul unordered',
|
|
164
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBulletList().run(),
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
key: 'ordered-list', label: 'Numbered list', icon: '1.', group: 'Lists',
|
|
168
|
-
searchKey: 'numbered ordered list ol',
|
|
169
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleOrderedList().run(),
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
key: 'quote', label: 'Quote', icon: '❝', group: 'Basic',
|
|
173
|
-
searchKey: 'quote blockquote',
|
|
174
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
key: 'code', label: 'Code block', icon: '</>', group: 'Basic',
|
|
178
|
-
searchKey: 'code block pre',
|
|
179
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
key: 'hr', label: 'Divider', icon: '—', group: 'Basic',
|
|
183
|
-
searchKey: 'divider hr horizontal rule',
|
|
184
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
|
|
185
|
-
},
|
|
186
|
-
// 3×3 table with a header row — same shape as the toolbar `table` button.
|
|
187
|
-
// No upload gate; tables are pure schema, available everywhere.
|
|
188
|
-
{
|
|
189
|
-
key: 'table', label: 'Table', icon: '⊞', group: 'Insert',
|
|
190
|
-
searchKey: 'table grid rows columns',
|
|
191
|
-
command: ({ editor, range }) =>
|
|
192
|
-
editor.chain().focus().deleteRange(range)
|
|
193
|
-
.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
|
194
|
-
},
|
|
195
|
-
// Collapsible `<details>` block. `setDetails` wraps the cursor's
|
|
196
|
-
// paragraph in a details node with an empty summary; the user starts
|
|
197
|
-
// typing the summary and presses Enter to drop into the body. No
|
|
198
|
-
// upload gate — pure schema, available everywhere.
|
|
199
|
-
{
|
|
200
|
-
key: 'details', label: 'Collapsible block', icon: '▸', group: 'Insert',
|
|
201
|
-
searchKey: 'details collapsible disclosure summary toggle expand',
|
|
202
|
-
command: ({ editor, range }) =>
|
|
203
|
-
editor.chain().focus().deleteRange(range).setDetails().run(),
|
|
204
|
-
},
|
|
205
|
-
// Multi-column grid layout. Two distinct entries (2-col + 3-col) so
|
|
206
|
-
// the user picks the column count from the slash menu directly,
|
|
207
|
-
// matching how the toolbar's `grid` button defaults to 2 cols.
|
|
208
|
-
{
|
|
209
|
-
key: 'grid-2', label: 'Two-column grid', icon: '⊞', group: 'Insert',
|
|
210
|
-
searchKey: 'grid columns layout 2 two split side',
|
|
211
|
-
command: ({ editor, range }) =>
|
|
212
|
-
editor.chain().focus().deleteRange(range).setGrid({ columns: 2 }).run(),
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
key: 'grid-3', label: 'Three-column grid', icon: '⊞', group: 'Insert',
|
|
216
|
-
searchKey: 'grid columns layout 3 three split',
|
|
217
|
-
command: ({ editor, range }) =>
|
|
218
|
-
editor.chain().focus().deleteRange(range).setGrid({ columns: 3 }).run(),
|
|
219
|
-
},
|
|
220
|
-
// Image entry shares the toolbar's attach-files dialog; only surfaced
|
|
221
|
-
// when the panel has wired an `UploadAdapter`. Without one, the dialog
|
|
222
|
-
// would post to a missing endpoint — the slash item degrades the same
|
|
223
|
-
// way the toolbar's `attachFiles` button does (server-stripped at meta
|
|
224
|
-
// build time when no adapter is set).
|
|
225
|
-
...(insert.hasUpload ? [{
|
|
226
|
-
key: 'image', label: 'Image', icon: '🖼', group: 'Insert',
|
|
227
|
-
searchKey: 'image upload media file attach',
|
|
228
|
-
command: ({ editor, range }: { editor: Editor; range: Range }) => {
|
|
229
|
-
// Drop the slash range first so the user doesn't return to a
|
|
230
|
-
// dangling `/image` after closing the dialog. Inserting the
|
|
231
|
-
// actual image happens inside the dialog's upload handler.
|
|
232
|
-
editor.chain().focus().deleteRange(range).run()
|
|
233
|
-
insert.onInsertImage()
|
|
234
|
-
},
|
|
235
|
-
}] satisfies SlashItem[] : []),
|
|
236
|
-
{
|
|
237
|
-
key: 'align-left', label: 'Align left', icon: '⇤', group: 'Align',
|
|
238
|
-
searchKey: 'align left start',
|
|
239
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setTextAlign('left').run(),
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
key: 'align-center', label: 'Align center', icon: '⇔', group: 'Align',
|
|
243
|
-
searchKey: 'align center middle',
|
|
244
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setTextAlign('center').run(),
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
key: 'align-right', label: 'Align right', icon: '⇥', group: 'Align',
|
|
248
|
-
searchKey: 'align right end',
|
|
249
|
-
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setTextAlign('right').run(),
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
key: 'clear-format', label: 'Clear formatting', icon: '⌫', group: 'Basic',
|
|
253
|
-
searchKey: 'clear formatting reset',
|
|
254
|
-
command: ({ editor, range }) =>
|
|
255
|
-
editor.chain().focus().deleteRange(range).clearNodes().unsetAllMarks().run(),
|
|
256
|
-
},
|
|
257
|
-
// Inline-mark size variants. Slash-menu form leaves the slash range in
|
|
258
|
-
// place rather than swallowing it, so the user runs the command on the
|
|
259
|
-
// word they were just typing — the alternative ("/lead" deletes the
|
|
260
|
-
// range, then user types more) requires re-positioning the cursor and
|
|
261
|
-
// breaks the "type-toggle-keep-typing" rhythm authors use most.
|
|
262
|
-
{
|
|
263
|
-
key: 'lead', label: 'Lead', icon: 'P+', group: 'Style',
|
|
264
|
-
searchKey: 'lead lede intro paragraph emphasis',
|
|
265
|
-
command: ({ editor, range }) =>
|
|
266
|
-
editor.chain().focus().deleteRange(range).toggleMark('lead').run(),
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
key: 'small', label: 'Small', icon: 'P-', group: 'Style',
|
|
270
|
-
searchKey: 'small fine print footnote caption',
|
|
271
|
-
command: ({ editor, range }) =>
|
|
272
|
-
editor.chain().focus().deleteRange(range).toggleMark('small').run(),
|
|
273
|
-
},
|
|
274
|
-
]
|
|
275
|
-
|
|
276
|
-
const customs: SlashItem[] = blocks.map((b) => ({
|
|
277
|
-
key: `block:${b.name}`,
|
|
278
|
-
label: b.label,
|
|
279
|
-
icon: b.icon,
|
|
280
|
-
group: 'Blocks',
|
|
281
|
-
searchKey: `${b.label} ${b.name} block`,
|
|
282
|
-
command: ({ editor, range }) => {
|
|
283
|
-
// Use insertContent directly with explicit attrs rather than chaining
|
|
284
|
-
// through our custom `insertBlock` command — chained custom commands
|
|
285
|
-
// sometimes drop the `attrs` payload depending on Tiptap version.
|
|
286
|
-
editor
|
|
287
|
-
.chain()
|
|
288
|
-
.focus()
|
|
289
|
-
.deleteRange(range)
|
|
290
|
-
.insertContent({
|
|
291
|
-
type: 'pilotiqBlock',
|
|
292
|
-
attrs: {
|
|
293
|
-
blockType: b.name,
|
|
294
|
-
blockData: defaultsFromSchema(b),
|
|
295
|
-
},
|
|
296
|
-
})
|
|
297
|
-
.run()
|
|
298
|
-
},
|
|
299
|
-
}))
|
|
300
|
-
|
|
301
|
-
const merges: SlashItem[] = mergeTags.map((id) => ({
|
|
302
|
-
key: `merge-tag:${id}`,
|
|
303
|
-
label: `{{ ${id} }}`,
|
|
304
|
-
icon: '{{}}',
|
|
305
|
-
group: 'Merge tags',
|
|
306
|
-
searchKey: `merge tag placeholder ${id}`,
|
|
307
|
-
command: ({ editor, range }) => {
|
|
308
|
-
editor
|
|
309
|
-
.chain()
|
|
310
|
-
.focus()
|
|
311
|
-
.deleteRange(range)
|
|
312
|
-
.insertContent({ type: 'mergeTag', attrs: { id } })
|
|
313
|
-
.run()
|
|
314
|
-
},
|
|
315
|
-
}))
|
|
316
|
-
|
|
317
|
-
const all = [...builtins, ...customs, ...merges]
|
|
318
|
-
if (!query) return all
|
|
319
|
-
|
|
320
|
-
const needle = query.toLowerCase()
|
|
321
|
-
return all.filter((item) =>
|
|
322
|
-
`${item.label} ${item.searchKey} ${item.group ?? ''}`.toLowerCase().includes(needle),
|
|
323
|
-
)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function defaultsFromSchema(block: BlockMeta): Record<string, unknown> {
|
|
327
|
-
const out: Record<string, unknown> = {}
|
|
328
|
-
for (const f of block.schema) {
|
|
329
|
-
out[f.name] = ''
|
|
330
|
-
}
|
|
331
|
-
return out
|
|
332
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { Mark, mergeAttributes } from '@tiptap/core'
|
|
2
|
-
|
|
3
|
-
declare module '@tiptap/core' {
|
|
4
|
-
interface Commands<ReturnType> {
|
|
5
|
-
lead: {
|
|
6
|
-
/** Toggle the `lead` mark on the current selection. */
|
|
7
|
-
toggleLead: () => ReturnType
|
|
8
|
-
}
|
|
9
|
-
small: {
|
|
10
|
-
/** Toggle the `small` mark on the current selection. */
|
|
11
|
-
toggleSmall: () => ReturnType
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Two inline marks for paragraph-style size variants beyond the standard
|
|
18
|
-
* heading levels:
|
|
19
|
-
*
|
|
20
|
-
* - `lead` — opening / lede paragraph styling. Renders as
|
|
21
|
-
* `<span class="lead">…</span>` so authors keep paragraph
|
|
22
|
-
* semantics; styling is owned by the consumer's CSS (the
|
|
23
|
-
* adapter doesn't ship a `.lead` rule — every site already
|
|
24
|
-
* has one).
|
|
25
|
-
* - `small` — semantic `<small>` mark. Mirrors the HTML element so
|
|
26
|
-
* read-side renderers don't need a special class to style
|
|
27
|
-
* fine print.
|
|
28
|
-
*
|
|
29
|
-
* Both marks live in the standard inline group so they compose with bold,
|
|
30
|
-
* italic, color, etc. without exclusivity rules. Excluding each other isn't
|
|
31
|
-
* useful — a `<small lead>` selection would be inert visually anyway.
|
|
32
|
-
*/
|
|
33
|
-
export const LeadMarkExtension = Mark.create({
|
|
34
|
-
name: 'lead',
|
|
35
|
-
|
|
36
|
-
parseHTML() {
|
|
37
|
-
return [{ tag: 'span.lead' }]
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
renderHTML({ HTMLAttributes }) {
|
|
41
|
-
return ['span', mergeAttributes({ class: 'lead' }, HTMLAttributes), 0]
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
addCommands() {
|
|
45
|
-
return {
|
|
46
|
-
toggleLead:
|
|
47
|
-
() =>
|
|
48
|
-
({ commands }) =>
|
|
49
|
-
commands.toggleMark(this.name),
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
export const SmallMarkExtension = Mark.create({
|
|
55
|
-
name: 'small',
|
|
56
|
-
|
|
57
|
-
parseHTML() {
|
|
58
|
-
return [{ tag: 'small' }]
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
renderHTML({ HTMLAttributes }) {
|
|
62
|
-
return ['small', mergeAttributes(HTMLAttributes), 0]
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
addCommands() {
|
|
66
|
-
return {
|
|
67
|
-
toggleSmall:
|
|
68
|
-
() =>
|
|
69
|
-
({ commands }) =>
|
|
70
|
-
commands.toggleMark(this.name),
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
})
|
package/src/index.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
RichTextField,
|
|
3
|
-
DEFAULT_TOOLBAR_GROUPS,
|
|
4
|
-
DEFAULT_TEXT_COLORS,
|
|
5
|
-
DEFAULT_HIGHLIGHT_COLORS,
|
|
6
|
-
type ColorSwatch,
|
|
7
|
-
type RichTextAttachmentVisibility,
|
|
8
|
-
type RichTextFieldMeta,
|
|
9
|
-
type RichTextStorage,
|
|
10
|
-
type ToolbarButtonId,
|
|
11
|
-
type ToolbarGroups,
|
|
12
|
-
} from './RichTextField.js'
|
|
13
|
-
export { Block, type BlockMeta } from './Block.js'
|
|
14
|
-
export {
|
|
15
|
-
MentionProvider,
|
|
16
|
-
type MentionItem,
|
|
17
|
-
type MentionProviderMeta,
|
|
18
|
-
} from './MentionProvider.js'
|
|
19
|
-
export { registerTiptap } from './register.js'
|
|
20
|
-
export {
|
|
21
|
-
createPlainTextEditor,
|
|
22
|
-
plainTextOf,
|
|
23
|
-
plainTextToDoc,
|
|
24
|
-
type PlainTextEditorOptions,
|
|
25
|
-
} from './PlainTextEditor.js'
|
|
26
|
-
export { tiptap } from './plugin.js'
|
|
27
|
-
export { TiptapEditor } from './react/TiptapEditor.js'
|
|
28
|
-
export {
|
|
29
|
-
AiSuggestionExtension,
|
|
30
|
-
aiSuggestionPluginKey,
|
|
31
|
-
upsertSuggestion,
|
|
32
|
-
upsertSuggestions,
|
|
33
|
-
removeSuggestion,
|
|
34
|
-
remapSuggestions,
|
|
35
|
-
sortForApproveAll,
|
|
36
|
-
clampPos,
|
|
37
|
-
type AiSuggestion,
|
|
38
|
-
type AiSuggestionExtensionOptions,
|
|
39
|
-
} from './extensions/AiSuggestionExtension.js'
|
|
40
|
-
export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js'
|
|
41
|
-
export {
|
|
42
|
-
AiInlineDiffExtension,
|
|
43
|
-
aiInlineDiffPluginKey,
|
|
44
|
-
getAiInlineDiffState,
|
|
45
|
-
type AiInlineDiffExtensionOptions,
|
|
46
|
-
} from './extensions/AiInlineDiffExtension.js'
|
|
47
|
-
export {
|
|
48
|
-
planReplaceBlock,
|
|
49
|
-
planInsertBlockBefore,
|
|
50
|
-
planDeleteBlock,
|
|
51
|
-
planUpdateBlockMark,
|
|
52
|
-
summarizeBlockStructure,
|
|
53
|
-
type BlockMarkRange,
|
|
54
|
-
type TransactionModifier,
|
|
55
|
-
} from './surgicalOps.js'
|
|
56
|
-
export {
|
|
57
|
-
renderRichTextToHtml,
|
|
58
|
-
isRichTextValue,
|
|
59
|
-
type RenderRichTextOptions,
|
|
60
|
-
type TiptapNode,
|
|
61
|
-
type TiptapMark,
|
|
62
|
-
} from './render.js'
|
package/src/markdownExtension.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Re-export of `tiptap-markdown`'s `Markdown` extension. The .ts source
|
|
2
|
-
// here only types the re-export; the .js emitted alongside it under
|
|
3
|
-
// `dist/` is REPLACED at build time by an esbuild-produced ESM bundle
|
|
4
|
-
// that inlines `tiptap-markdown` + `markdown-it` + `markdown-it-task-lists`.
|
|
5
|
-
//
|
|
6
|
-
// Why bundle. `tiptap-markdown@0.9.0`'s task-list node imports
|
|
7
|
-
// `markdown-it-task-lists` as `import x from 'markdown-it-task-lists'`,
|
|
8
|
-
// but that package ships pure CJS (`module.exports = fn`) with no
|
|
9
|
-
// `default` export. Vite's dev runtime serves the raw CJS and does not
|
|
10
|
-
// synthesize a `default`, so the import throws at client-bundle init and
|
|
11
|
-
// the whole admin app silently fails to mount. Pre-bundling the chain
|
|
12
|
-
// here moves CJS↔ESM interop to @pilotiq/tiptap's build step (esbuild
|
|
13
|
-
// handles it cleanly) so downstream consumers don't have to wire
|
|
14
|
-
// `optimizeDeps.include` for the CJS transitive.
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
-
import { Markdown as MarkdownImpl } from 'tiptap-markdown'
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
export const Markdown: any = MarkdownImpl
|
package/src/markdownStorage.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Editor } from '@tiptap/core'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Typed accessor for the `tiptap-markdown` extension's editor.storage slot.
|
|
5
|
-
*
|
|
6
|
-
* Tiptap's `Storage` type is a generic that defaults to `any` (see
|
|
7
|
-
* `@tiptap/core` `ExtendableConfig<Options, Storage = any>`), so the
|
|
8
|
-
* markdown extension's contributions aren't reachable through a clean
|
|
9
|
-
* module augmentation. This module centralizes the structural narrow in
|
|
10
|
-
* one place — every caller used to repeat `(editor.storage as any).markdown`
|
|
11
|
-
* with the same `typeof getMarkdown === 'function'` guard.
|
|
12
|
-
*
|
|
13
|
-
* The shape mirrors `tiptap-markdown@^0.9.0`'s actual runtime contract.
|
|
14
|
-
* If the markdown extension isn't installed (or hasn't initialized yet),
|
|
15
|
-
* the accessors return `undefined` / `''` rather than throwing.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
interface MarkdownStorage {
|
|
19
|
-
getMarkdown(): string
|
|
20
|
-
parser: { parse(source: string): string }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readStorage(editor: Editor): unknown {
|
|
24
|
-
return (editor.storage as unknown as Record<string, unknown>)['markdown']
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getMarkdownStorage(editor: Editor): MarkdownStorage | undefined {
|
|
28
|
-
const s = readStorage(editor)
|
|
29
|
-
if (
|
|
30
|
-
s !== null &&
|
|
31
|
-
typeof s === 'object' &&
|
|
32
|
-
typeof (s as MarkdownStorage).getMarkdown === 'function' &&
|
|
33
|
-
typeof (s as { parser?: { parse?: unknown } }).parser?.parse === 'function'
|
|
34
|
-
) {
|
|
35
|
-
return s as MarkdownStorage
|
|
36
|
-
}
|
|
37
|
-
return undefined
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Read the editor's current content as a markdown string. Returns '' when the markdown extension isn't installed. */
|
|
41
|
-
export function getMarkdownString(editor: Editor): string {
|
|
42
|
-
return getMarkdownStorage(editor)?.getMarkdown() ?? ''
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Parse a markdown source string to HTML via the markdown extension's parser. Returns undefined when unavailable. */
|
|
46
|
-
export function parseMarkdownToHtml(editor: Editor, source: string): string | undefined {
|
|
47
|
-
const html = getMarkdownStorage(editor)?.parser.parse(source)
|
|
48
|
-
return typeof html === 'string' ? html : undefined
|
|
49
|
-
}
|