@puredesktop/platform-editor 1.0.0-beta.1 → 2.1.0
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/package.json +1 -1
- package/src/CollapsePanel.tsx +35 -35
- package/src/DocumentDiffView.tsx +130 -130
- package/src/DocumentEditor.test.ts +118 -118
- package/src/DocumentEditor.tsx +2651 -2631
- package/src/alignedLineDiff.test.ts +52 -52
- package/src/alignedLineDiff.ts +67 -67
- package/src/collectionAssetSrc.ts +2 -2
- package/src/commentUtils.test.ts +350 -350
- package/src/commentUtils.ts +546 -546
- package/src/constants/toolKeys.ts +14 -14
- package/src/contentFormat.test.ts +27 -27
- package/src/contentFormat.ts +18 -18
- package/src/editorExtensions.ts +155 -155
- package/src/extensions/appliedChangeMark.ts +115 -115
- package/src/extensions/autoReviewPrompts.test.ts +529 -529
- package/src/extensions/autoReviewPrompts.ts +1442 -1442
- package/src/extensions/collectionImage.ts +26 -26
- package/src/extensions/collectionImagePaste.ts +215 -215
- package/src/extensions/commentMark.ts +223 -223
- package/src/extensions/documentAssetIdentity.ts +54 -54
- package/src/extensions/figure.ts +92 -92
- package/src/extensions/footnote.ts +186 -186
- package/src/extensions/indexMarker.ts +88 -88
- package/src/extensions/mathEditing.ts +239 -239
- package/src/extensions/mermaidBlock.tsx +297 -297
- package/src/extensions/slashCommands.test.ts +170 -170
- package/src/extensions/slashCommands.ts +746 -746
- package/src/extensions/smartTypography.test.ts +74 -74
- package/src/extensions/smartTypography.ts +120 -120
- package/src/index.ts +138 -137
- package/src/insertCollectionImage.test.ts +60 -60
- package/src/insertCollectionImage.ts +63 -63
- package/src/insertInlineAssetKind.test.ts +67 -67
- package/src/insertInlineAssetKind.ts +49 -49
- package/src/mermaidPreview.test.ts +54 -54
- package/src/mermaidPreview.ts +115 -115
- package/src/styled.ts +697 -676
- package/src/toolbar/ToolBtn.tsx +77 -63
- package/src/toolbar/Toolbar.test.tsx +325 -325
- package/src/toolbar/Toolbar.tsx +662 -624
- package/src/toolbar/groups/HeadingGroup.tsx +175 -153
- package/src/toolbar/overlayTypes.ts +28 -28
- package/src/useEditorExtensions.ts +22 -22
- package/src/utils/markdownUtils.ts +331 -331
package/src/extensions/figure.ts
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
-
|
|
3
|
-
export interface FigureOptions {
|
|
4
|
-
HTMLAttributes: Record<string, unknown>
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
declare module '@tiptap/core' {
|
|
8
|
-
interface Commands<ReturnType> {
|
|
9
|
-
figure: {
|
|
10
|
-
/** Insert a figure block with an image and an editable caption. */
|
|
11
|
-
insertFigure: (opts: {
|
|
12
|
-
src: string
|
|
13
|
-
alt?: string
|
|
14
|
-
caption?: string
|
|
15
|
-
}) => ReturnType
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Figure block — wraps an image and an editable caption. Markdown
|
|
22
|
-
* round-trip is via `` (CommonMark image syntax). The
|
|
23
|
-
* book's render pipeline already wraps such images in
|
|
24
|
-
* `<figure><img/><figcaption/></figure>` for PDF output, so the
|
|
25
|
-
* caption survives end-to-end even though it serializes through the
|
|
26
|
-
* standard image syntax.
|
|
27
|
-
*
|
|
28
|
-
* Image-only nodes (no caption) are still produced by Image nodes directly
|
|
29
|
-
* for inline images that don't deserve a figure wrapper.
|
|
30
|
-
*/
|
|
31
|
-
export const Figure = Node.create<FigureOptions>({
|
|
32
|
-
name: 'figure',
|
|
33
|
-
group: 'block',
|
|
34
|
-
content: 'inline*',
|
|
35
|
-
defining: true,
|
|
36
|
-
isolating: true,
|
|
37
|
-
draggable: true,
|
|
38
|
-
|
|
39
|
-
addOptions() {
|
|
40
|
-
return { HTMLAttributes: {} }
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
addAttributes() {
|
|
44
|
-
return {
|
|
45
|
-
src: {
|
|
46
|
-
default: null,
|
|
47
|
-
parseHTML: el => el.querySelector('img')?.getAttribute('src') ?? null,
|
|
48
|
-
},
|
|
49
|
-
alt: {
|
|
50
|
-
default: '',
|
|
51
|
-
parseHTML: el => el.querySelector('img')?.getAttribute('alt') ?? '',
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
parseHTML() {
|
|
57
|
-
return [
|
|
58
|
-
{
|
|
59
|
-
tag: 'figure',
|
|
60
|
-
getAttrs: element =>
|
|
61
|
-
element instanceof HTMLElement &&
|
|
62
|
-
element.getAttribute('data-type') === 'mermaid'
|
|
63
|
-
? false
|
|
64
|
-
: null,
|
|
65
|
-
contentElement: 'figcaption',
|
|
66
|
-
},
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
renderHTML({ node, HTMLAttributes }) {
|
|
71
|
-
const { src, alt } = node.attrs as { src: string | null; alt: string }
|
|
72
|
-
return [
|
|
73
|
-
'figure',
|
|
74
|
-
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
75
|
-
['img', { src, alt }],
|
|
76
|
-
['figcaption', {}, 0],
|
|
77
|
-
]
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
addCommands() {
|
|
81
|
-
return {
|
|
82
|
-
insertFigure:
|
|
83
|
-
({ src, alt = '', caption = '' }) =>
|
|
84
|
-
({ commands }) =>
|
|
85
|
-
commands.insertContent({
|
|
86
|
-
type: this.name,
|
|
87
|
-
attrs: { src, alt },
|
|
88
|
-
content: caption ? [{ type: 'text', text: caption }] : [],
|
|
89
|
-
}),
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
})
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
+
|
|
3
|
+
export interface FigureOptions {
|
|
4
|
+
HTMLAttributes: Record<string, unknown>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module '@tiptap/core' {
|
|
8
|
+
interface Commands<ReturnType> {
|
|
9
|
+
figure: {
|
|
10
|
+
/** Insert a figure block with an image and an editable caption. */
|
|
11
|
+
insertFigure: (opts: {
|
|
12
|
+
src: string
|
|
13
|
+
alt?: string
|
|
14
|
+
caption?: string
|
|
15
|
+
}) => ReturnType
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Figure block — wraps an image and an editable caption. Markdown
|
|
22
|
+
* round-trip is via `` (CommonMark image syntax). The
|
|
23
|
+
* book's render pipeline already wraps such images in
|
|
24
|
+
* `<figure><img/><figcaption/></figure>` for PDF output, so the
|
|
25
|
+
* caption survives end-to-end even though it serializes through the
|
|
26
|
+
* standard image syntax.
|
|
27
|
+
*
|
|
28
|
+
* Image-only nodes (no caption) are still produced by Image nodes directly
|
|
29
|
+
* for inline images that don't deserve a figure wrapper.
|
|
30
|
+
*/
|
|
31
|
+
export const Figure = Node.create<FigureOptions>({
|
|
32
|
+
name: 'figure',
|
|
33
|
+
group: 'block',
|
|
34
|
+
content: 'inline*',
|
|
35
|
+
defining: true,
|
|
36
|
+
isolating: true,
|
|
37
|
+
draggable: true,
|
|
38
|
+
|
|
39
|
+
addOptions() {
|
|
40
|
+
return { HTMLAttributes: {} }
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
addAttributes() {
|
|
44
|
+
return {
|
|
45
|
+
src: {
|
|
46
|
+
default: null,
|
|
47
|
+
parseHTML: el => el.querySelector('img')?.getAttribute('src') ?? null,
|
|
48
|
+
},
|
|
49
|
+
alt: {
|
|
50
|
+
default: '',
|
|
51
|
+
parseHTML: el => el.querySelector('img')?.getAttribute('alt') ?? '',
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
parseHTML() {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
tag: 'figure',
|
|
60
|
+
getAttrs: element =>
|
|
61
|
+
element instanceof HTMLElement &&
|
|
62
|
+
element.getAttribute('data-type') === 'mermaid'
|
|
63
|
+
? false
|
|
64
|
+
: null,
|
|
65
|
+
contentElement: 'figcaption',
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
71
|
+
const { src, alt } = node.attrs as { src: string | null; alt: string }
|
|
72
|
+
return [
|
|
73
|
+
'figure',
|
|
74
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
75
|
+
['img', { src, alt }],
|
|
76
|
+
['figcaption', {}, 0],
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
addCommands() {
|
|
81
|
+
return {
|
|
82
|
+
insertFigure:
|
|
83
|
+
({ src, alt = '', caption = '' }) =>
|
|
84
|
+
({ commands }) =>
|
|
85
|
+
commands.insertContent({
|
|
86
|
+
type: this.name,
|
|
87
|
+
attrs: { src, alt },
|
|
88
|
+
content: caption ? [{ type: 'text', text: caption }] : [],
|
|
89
|
+
}),
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
})
|
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
-
|
|
3
|
-
declare module '@tiptap/core' {
|
|
4
|
-
interface Commands<ReturnType> {
|
|
5
|
-
footnote: {
|
|
6
|
-
/** Insert an empty footnote marker at the cursor and open it to edit. */
|
|
7
|
-
insertFootnote: () => ReturnType
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Footnote — an inline, atomic reference marker.
|
|
14
|
-
*
|
|
15
|
-
* The body shows only a small superscript number; the note text is embedded on
|
|
16
|
-
* the node (the `text` attribute) and edited in a popover when the marker is
|
|
17
|
-
* clicked. Numbering is by document order, recomputed live. Serializes to
|
|
18
|
-
* `<sup data-footnote data-text="…">` for a clean HTML round-trip; the export
|
|
19
|
-
* pipeline (prepareFootnotes) turns markers into numbered links plus a
|
|
20
|
-
* footnotes section.
|
|
21
|
-
*/
|
|
22
|
-
export const Footnote = Node.create({
|
|
23
|
-
name: 'footnote',
|
|
24
|
-
group: 'inline',
|
|
25
|
-
inline: true,
|
|
26
|
-
atom: true,
|
|
27
|
-
selectable: true,
|
|
28
|
-
|
|
29
|
-
addAttributes() {
|
|
30
|
-
return {
|
|
31
|
-
text: {
|
|
32
|
-
default: '',
|
|
33
|
-
parseHTML: el => el.getAttribute('data-text') ?? '',
|
|
34
|
-
renderHTML: (attrs: { text?: string }) => ({
|
|
35
|
-
'data-text': attrs.text ?? '',
|
|
36
|
-
}),
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
parseHTML() {
|
|
42
|
-
return [{ tag: 'sup[data-footnote]' }]
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
renderHTML({ HTMLAttributes }) {
|
|
46
|
-
return [
|
|
47
|
-
'sup',
|
|
48
|
-
mergeAttributes(HTMLAttributes, {
|
|
49
|
-
'data-footnote': '',
|
|
50
|
-
class: 'footnote-ref',
|
|
51
|
-
}),
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
addCommands() {
|
|
56
|
-
return {
|
|
57
|
-
insertFootnote:
|
|
58
|
-
() =>
|
|
59
|
-
({ commands }) =>
|
|
60
|
-
commands.insertContent({ type: this.name, attrs: { text: '' } }),
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
addNodeView() {
|
|
65
|
-
return ({ node, getPos, editor }) => {
|
|
66
|
-
let current = node
|
|
67
|
-
const dom = document.createElement('sup')
|
|
68
|
-
dom.className = 'footnote-ref'
|
|
69
|
-
dom.setAttribute('data-footnote', '')
|
|
70
|
-
dom.contentEditable = 'false'
|
|
71
|
-
|
|
72
|
-
const renderNumber = (): void => {
|
|
73
|
-
let count = 0
|
|
74
|
-
let mine = 0
|
|
75
|
-
const here = typeof getPos === 'function' ? getPos() : null
|
|
76
|
-
editor.state.doc.descendants((child, pos) => {
|
|
77
|
-
if (child.type.name === 'footnote') {
|
|
78
|
-
count += 1
|
|
79
|
-
if (here !== null && pos === here) mine = count
|
|
80
|
-
}
|
|
81
|
-
return true
|
|
82
|
-
})
|
|
83
|
-
dom.textContent = String(mine || count || 1)
|
|
84
|
-
}
|
|
85
|
-
renderNumber()
|
|
86
|
-
|
|
87
|
-
let popover: HTMLDivElement | null = null
|
|
88
|
-
const closePopover = (): void => {
|
|
89
|
-
popover?.remove()
|
|
90
|
-
popover = null
|
|
91
|
-
document.removeEventListener('mousedown', onDocPointerDown, true)
|
|
92
|
-
}
|
|
93
|
-
const onDocPointerDown = (event: MouseEvent): void => {
|
|
94
|
-
if (
|
|
95
|
-
popover &&
|
|
96
|
-
event.target !== dom &&
|
|
97
|
-
!popover.contains(event.target as globalThis.Node)
|
|
98
|
-
) {
|
|
99
|
-
closePopover()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const openPopover = (): void => {
|
|
103
|
-
if (popover) return
|
|
104
|
-
popover = document.createElement('div')
|
|
105
|
-
popover.className = 'footnote-popover'
|
|
106
|
-
Object.assign(popover.style, {
|
|
107
|
-
position: 'fixed',
|
|
108
|
-
zIndex: '1002',
|
|
109
|
-
width: 'min(360px, calc(100vw - 24px))',
|
|
110
|
-
padding: '8px',
|
|
111
|
-
border: '1px solid var(--platform-colors-border, #d7dbe0)',
|
|
112
|
-
borderRadius: '8px',
|
|
113
|
-
background: 'var(--platform-colors-surface, #fff)',
|
|
114
|
-
boxShadow: '0 18px 42px rgb(23 29 36 / 0.18)',
|
|
115
|
-
})
|
|
116
|
-
const textarea = document.createElement('textarea')
|
|
117
|
-
textarea.value = current.attrs.text ?? ''
|
|
118
|
-
textarea.placeholder = 'Footnote text…'
|
|
119
|
-
textarea.rows = 3
|
|
120
|
-
Object.assign(textarea.style, {
|
|
121
|
-
width: '100%',
|
|
122
|
-
boxSizing: 'border-box',
|
|
123
|
-
resize: 'vertical',
|
|
124
|
-
border: '1px solid var(--platform-colors-border, #d7dbe0)',
|
|
125
|
-
borderRadius: '6px',
|
|
126
|
-
padding: '6px 8px',
|
|
127
|
-
font: 'inherit',
|
|
128
|
-
fontSize: '13px',
|
|
129
|
-
color: 'inherit',
|
|
130
|
-
background: 'var(--platform-colors-surface, #fff)',
|
|
131
|
-
})
|
|
132
|
-
popover.appendChild(textarea)
|
|
133
|
-
|
|
134
|
-
const rect = dom.getBoundingClientRect()
|
|
135
|
-
popover.style.left = `${Math.max(12, rect.left)}px`
|
|
136
|
-
popover.style.top = `${rect.bottom + 6}px`
|
|
137
|
-
document.body.appendChild(popover)
|
|
138
|
-
textarea.focus()
|
|
139
|
-
|
|
140
|
-
const commit = (): void => {
|
|
141
|
-
if (typeof getPos !== 'function') return
|
|
142
|
-
const pos = getPos()
|
|
143
|
-
if (typeof pos !== 'number') return
|
|
144
|
-
const next = textarea.value
|
|
145
|
-
if (next === (current.attrs.text ?? '')) return
|
|
146
|
-
editor.view.dispatch(
|
|
147
|
-
editor.state.tr.setNodeMarkup(pos, undefined, {
|
|
148
|
-
...current.attrs,
|
|
149
|
-
text: next,
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
textarea.addEventListener('blur', () => {
|
|
154
|
-
commit()
|
|
155
|
-
closePopover()
|
|
156
|
-
})
|
|
157
|
-
textarea.addEventListener('keydown', event => {
|
|
158
|
-
if (event.key === 'Escape') {
|
|
159
|
-
event.preventDefault()
|
|
160
|
-
commit()
|
|
161
|
-
closePopover()
|
|
162
|
-
editor.commands.focus()
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
document.addEventListener('mousedown', onDocPointerDown, true)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
dom.addEventListener('mousedown', event => {
|
|
169
|
-
event.preventDefault()
|
|
170
|
-
openPopover()
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
dom,
|
|
175
|
-
update: updated => {
|
|
176
|
-
if (updated.type.name !== 'footnote') return false
|
|
177
|
-
current = updated
|
|
178
|
-
renderNumber()
|
|
179
|
-
return true
|
|
180
|
-
},
|
|
181
|
-
ignoreMutation: () => true,
|
|
182
|
-
destroy: closePopover,
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
})
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
+
|
|
3
|
+
declare module '@tiptap/core' {
|
|
4
|
+
interface Commands<ReturnType> {
|
|
5
|
+
footnote: {
|
|
6
|
+
/** Insert an empty footnote marker at the cursor and open it to edit. */
|
|
7
|
+
insertFootnote: () => ReturnType
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Footnote — an inline, atomic reference marker.
|
|
14
|
+
*
|
|
15
|
+
* The body shows only a small superscript number; the note text is embedded on
|
|
16
|
+
* the node (the `text` attribute) and edited in a popover when the marker is
|
|
17
|
+
* clicked. Numbering is by document order, recomputed live. Serializes to
|
|
18
|
+
* `<sup data-footnote data-text="…">` for a clean HTML round-trip; the export
|
|
19
|
+
* pipeline (prepareFootnotes) turns markers into numbered links plus a
|
|
20
|
+
* footnotes section.
|
|
21
|
+
*/
|
|
22
|
+
export const Footnote = Node.create({
|
|
23
|
+
name: 'footnote',
|
|
24
|
+
group: 'inline',
|
|
25
|
+
inline: true,
|
|
26
|
+
atom: true,
|
|
27
|
+
selectable: true,
|
|
28
|
+
|
|
29
|
+
addAttributes() {
|
|
30
|
+
return {
|
|
31
|
+
text: {
|
|
32
|
+
default: '',
|
|
33
|
+
parseHTML: el => el.getAttribute('data-text') ?? '',
|
|
34
|
+
renderHTML: (attrs: { text?: string }) => ({
|
|
35
|
+
'data-text': attrs.text ?? '',
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
parseHTML() {
|
|
42
|
+
return [{ tag: 'sup[data-footnote]' }]
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
renderHTML({ HTMLAttributes }) {
|
|
46
|
+
return [
|
|
47
|
+
'sup',
|
|
48
|
+
mergeAttributes(HTMLAttributes, {
|
|
49
|
+
'data-footnote': '',
|
|
50
|
+
class: 'footnote-ref',
|
|
51
|
+
}),
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
addCommands() {
|
|
56
|
+
return {
|
|
57
|
+
insertFootnote:
|
|
58
|
+
() =>
|
|
59
|
+
({ commands }) =>
|
|
60
|
+
commands.insertContent({ type: this.name, attrs: { text: '' } }),
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
addNodeView() {
|
|
65
|
+
return ({ node, getPos, editor }) => {
|
|
66
|
+
let current = node
|
|
67
|
+
const dom = document.createElement('sup')
|
|
68
|
+
dom.className = 'footnote-ref'
|
|
69
|
+
dom.setAttribute('data-footnote', '')
|
|
70
|
+
dom.contentEditable = 'false'
|
|
71
|
+
|
|
72
|
+
const renderNumber = (): void => {
|
|
73
|
+
let count = 0
|
|
74
|
+
let mine = 0
|
|
75
|
+
const here = typeof getPos === 'function' ? getPos() : null
|
|
76
|
+
editor.state.doc.descendants((child, pos) => {
|
|
77
|
+
if (child.type.name === 'footnote') {
|
|
78
|
+
count += 1
|
|
79
|
+
if (here !== null && pos === here) mine = count
|
|
80
|
+
}
|
|
81
|
+
return true
|
|
82
|
+
})
|
|
83
|
+
dom.textContent = String(mine || count || 1)
|
|
84
|
+
}
|
|
85
|
+
renderNumber()
|
|
86
|
+
|
|
87
|
+
let popover: HTMLDivElement | null = null
|
|
88
|
+
const closePopover = (): void => {
|
|
89
|
+
popover?.remove()
|
|
90
|
+
popover = null
|
|
91
|
+
document.removeEventListener('mousedown', onDocPointerDown, true)
|
|
92
|
+
}
|
|
93
|
+
const onDocPointerDown = (event: MouseEvent): void => {
|
|
94
|
+
if (
|
|
95
|
+
popover &&
|
|
96
|
+
event.target !== dom &&
|
|
97
|
+
!popover.contains(event.target as globalThis.Node)
|
|
98
|
+
) {
|
|
99
|
+
closePopover()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const openPopover = (): void => {
|
|
103
|
+
if (popover) return
|
|
104
|
+
popover = document.createElement('div')
|
|
105
|
+
popover.className = 'footnote-popover'
|
|
106
|
+
Object.assign(popover.style, {
|
|
107
|
+
position: 'fixed',
|
|
108
|
+
zIndex: '1002',
|
|
109
|
+
width: 'min(360px, calc(100vw - 24px))',
|
|
110
|
+
padding: '8px',
|
|
111
|
+
border: '1px solid var(--platform-colors-border, #d7dbe0)',
|
|
112
|
+
borderRadius: '8px',
|
|
113
|
+
background: 'var(--platform-colors-surface, #fff)',
|
|
114
|
+
boxShadow: '0 18px 42px rgb(23 29 36 / 0.18)',
|
|
115
|
+
})
|
|
116
|
+
const textarea = document.createElement('textarea')
|
|
117
|
+
textarea.value = current.attrs.text ?? ''
|
|
118
|
+
textarea.placeholder = 'Footnote text…'
|
|
119
|
+
textarea.rows = 3
|
|
120
|
+
Object.assign(textarea.style, {
|
|
121
|
+
width: '100%',
|
|
122
|
+
boxSizing: 'border-box',
|
|
123
|
+
resize: 'vertical',
|
|
124
|
+
border: '1px solid var(--platform-colors-border, #d7dbe0)',
|
|
125
|
+
borderRadius: '6px',
|
|
126
|
+
padding: '6px 8px',
|
|
127
|
+
font: 'inherit',
|
|
128
|
+
fontSize: '13px',
|
|
129
|
+
color: 'inherit',
|
|
130
|
+
background: 'var(--platform-colors-surface, #fff)',
|
|
131
|
+
})
|
|
132
|
+
popover.appendChild(textarea)
|
|
133
|
+
|
|
134
|
+
const rect = dom.getBoundingClientRect()
|
|
135
|
+
popover.style.left = `${Math.max(12, rect.left)}px`
|
|
136
|
+
popover.style.top = `${rect.bottom + 6}px`
|
|
137
|
+
document.body.appendChild(popover)
|
|
138
|
+
textarea.focus()
|
|
139
|
+
|
|
140
|
+
const commit = (): void => {
|
|
141
|
+
if (typeof getPos !== 'function') return
|
|
142
|
+
const pos = getPos()
|
|
143
|
+
if (typeof pos !== 'number') return
|
|
144
|
+
const next = textarea.value
|
|
145
|
+
if (next === (current.attrs.text ?? '')) return
|
|
146
|
+
editor.view.dispatch(
|
|
147
|
+
editor.state.tr.setNodeMarkup(pos, undefined, {
|
|
148
|
+
...current.attrs,
|
|
149
|
+
text: next,
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
textarea.addEventListener('blur', () => {
|
|
154
|
+
commit()
|
|
155
|
+
closePopover()
|
|
156
|
+
})
|
|
157
|
+
textarea.addEventListener('keydown', event => {
|
|
158
|
+
if (event.key === 'Escape') {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
commit()
|
|
161
|
+
closePopover()
|
|
162
|
+
editor.commands.focus()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
document.addEventListener('mousedown', onDocPointerDown, true)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
dom.addEventListener('mousedown', event => {
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
openPopover()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
dom,
|
|
175
|
+
update: updated => {
|
|
176
|
+
if (updated.type.name !== 'footnote') return false
|
|
177
|
+
current = updated
|
|
178
|
+
renderNumber()
|
|
179
|
+
return true
|
|
180
|
+
},
|
|
181
|
+
ignoreMutation: () => true,
|
|
182
|
+
destroy: closePopover,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
})
|