@lowdefy/blocks-tiptap 5.6.0 → 6.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/dist/blocks/TiptapInput/TiptapInput.js +37 -81
- package/dist/blocks/TiptapInput/gallery.yaml +210 -0
- package/dist/blocks/TiptapInput/meta.js +10 -2
- package/dist/blocks/TiptapInput/style.css +60 -0
- package/dist/blocks/TiptapInput/tests/TiptapInput.e2e.yaml +53 -0
- package/dist/blocks/TiptapInput/tests.yaml +65 -0
- package/dist/blocks/TiptapInput/useTiptapState.js +6 -5
- package/dist/blocks/TiptapMentionInput/TiptapMentionInput.js +37 -80
- package/dist/blocks/TiptapMentionInput/gallery.yaml +296 -0
- package/dist/blocks/TiptapMentionInput/meta.js +10 -2
- package/dist/blocks/TiptapMentionInput/{style.module.css → style.css} +39 -39
- package/dist/blocks/TiptapMentionInput/suggestion.js +60 -40
- package/dist/blocks/TiptapMentionInput/tests/TiptapMentionInput.e2e.yaml +140 -0
- package/dist/blocks/TiptapMentionInput/tests.yaml +90 -0
- package/dist/blocks/TiptapMentionInput/useTiptapMentionState.js +5 -4
- package/dist/blocks/utils/PopoverMenu.js +54 -21
- package/dist/blocks/utils/buildExtensions.js +11 -13
- package/dist/blocks/utils/buildStarterKit.js +48 -0
- package/dist/blocks/utils/fileUpload.js +63 -0
- package/dist/blocks/utils/statusClass.js +1 -1
- package/dist/blocks/utils/{style.module.css → style.css} +64 -50
- package/dist/blocks/utils/useTiptapEditor.js +106 -0
- package/package.json +24 -25
- package/dist/blocks/TiptapInput/style.module.css +0 -60
- package/dist/blocks/utils/s3FileUpload.js +0 -53
|
@@ -50,27 +50,67 @@ function suggestion({ methods, char = '@', allowSpaces = true, limit = 5 }) {
|
|
|
50
50
|
render: ()=>{
|
|
51
51
|
let component;
|
|
52
52
|
let popup;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
let current;
|
|
54
|
+
const start = (props)=>{
|
|
55
|
+
current = {
|
|
56
|
+
from: props.range.from,
|
|
57
|
+
query: props.query
|
|
58
|
+
};
|
|
59
|
+
component = new ReactRenderer(MentionList, {
|
|
60
|
+
props: {
|
|
61
|
+
...props,
|
|
62
|
+
methods
|
|
63
|
+
},
|
|
64
|
+
editor: props.editor
|
|
65
|
+
});
|
|
66
|
+
if (!props.clientRect) return;
|
|
67
|
+
popup = tippy('body', {
|
|
68
|
+
getReferenceClientRect: props.clientRect,
|
|
69
|
+
appendTo: ()=>document.body,
|
|
70
|
+
content: component.element,
|
|
71
|
+
showOnCreate: true,
|
|
72
|
+
interactive: true,
|
|
73
|
+
trigger: 'manual',
|
|
74
|
+
placement: 'bottom-start'
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const exit = ()=>{
|
|
78
|
+
// Tear down in this order — React portal first, tippy popup after —
|
|
79
|
+
// to avoid "NotFoundError: Failed to execute 'removeChild'" when the
|
|
80
|
+
// user navigates away (e.g. clicks another item) while the mention
|
|
81
|
+
// popup is open. MentionList is rendered as a React portal into
|
|
82
|
+
// component.element, which tippy relocates into document.body.
|
|
83
|
+
// Destroying tippy first detaches that container out from under
|
|
84
|
+
// React's portal teardown; destroying the component first lets React
|
|
85
|
+
// remove the MentionList DOM cleanly while the container is still
|
|
86
|
+
// attached. The tippy teardown is then deferred to a microtask so it
|
|
87
|
+
// never runs synchronously inside React's own unmount commit.
|
|
88
|
+
component?.destroy();
|
|
89
|
+
component = undefined;
|
|
90
|
+
const instance = popup?.[0];
|
|
91
|
+
popup = undefined;
|
|
92
|
+
if (instance && !instance.state.isDestroyed) {
|
|
93
|
+
queueMicrotask(()=>{
|
|
94
|
+
if (!instance.state.isDestroyed) instance.destroy();
|
|
71
95
|
});
|
|
72
|
-
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
onStart: start,
|
|
73
100
|
onUpdate (props) {
|
|
101
|
+
// A suggestion that moves to a new trigger with a new query (e.g. a
|
|
102
|
+
// second "@" typed while the first was open) restarted in TipTap v2,
|
|
103
|
+
// which also showed a popup that an outside click had hidden. v3
|
|
104
|
+
// reports it as an update, so restart it here.
|
|
105
|
+
if (props.range.from !== current?.from && props.query !== current?.query) {
|
|
106
|
+
exit();
|
|
107
|
+
start(props);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
current = {
|
|
111
|
+
from: props.range.from,
|
|
112
|
+
query: props.query
|
|
113
|
+
};
|
|
74
114
|
component?.updateProps(props);
|
|
75
115
|
if (!props.clientRect) return;
|
|
76
116
|
popup?.[0]?.setProps({
|
|
@@ -84,27 +124,7 @@ function suggestion({ methods, char = '@', allowSpaces = true, limit = 5 }) {
|
|
|
84
124
|
}
|
|
85
125
|
return component?.ref?.onKeyDown(props) ?? false;
|
|
86
126
|
},
|
|
87
|
-
onExit
|
|
88
|
-
// Tear down in this order — React portal first, tippy popup after —
|
|
89
|
-
// to avoid "NotFoundError: Failed to execute 'removeChild'" when the
|
|
90
|
-
// user navigates away (e.g. clicks another item) while the mention
|
|
91
|
-
// popup is open. MentionList is rendered as a React portal into
|
|
92
|
-
// component.element, which tippy relocates into document.body.
|
|
93
|
-
// Destroying tippy first detaches that container out from under
|
|
94
|
-
// React's portal teardown; destroying the component first lets React
|
|
95
|
-
// remove the MentionList DOM cleanly while the container is still
|
|
96
|
-
// attached. The tippy teardown is then deferred to a microtask so it
|
|
97
|
-
// never runs synchronously inside React's own unmount commit.
|
|
98
|
-
component?.destroy();
|
|
99
|
-
component = undefined;
|
|
100
|
-
const instance = popup?.[0];
|
|
101
|
-
popup = undefined;
|
|
102
|
-
if (instance && !instance.state.isDestroyed) {
|
|
103
|
-
queueMicrotask(()=>{
|
|
104
|
-
if (!instance.state.isDestroyed) instance.destroy();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
127
|
+
onExit: exit
|
|
108
128
|
};
|
|
109
129
|
}
|
|
110
130
|
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Copyright 2020-2026 Lowdefy, Inc
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
id: tiptap-mention-input
|
|
16
|
+
type: Box
|
|
17
|
+
|
|
18
|
+
events:
|
|
19
|
+
onMount:
|
|
20
|
+
- id: seed_content
|
|
21
|
+
type: SetState
|
|
22
|
+
params:
|
|
23
|
+
tiptap_mention_with_value:
|
|
24
|
+
html: '<p>Hi <span class="tiptap-mention">@Alice</span>.</p>'
|
|
25
|
+
tiptap_mention_disabled:
|
|
26
|
+
html: '<p>Disabled mention editor.</p>'
|
|
27
|
+
# Saved html round-trip: a mention node comes back with a STRING id
|
|
28
|
+
# (data-id) and its label in data-label.
|
|
29
|
+
tiptap_mention_saved:
|
|
30
|
+
html: '<p>Ping <span data-type="mention" class="tiptap-mention" data-id="user_9" data-label="Zed" data-mention-suggestion-char="@">@Zed</span> please.</p>'
|
|
31
|
+
|
|
32
|
+
blocks:
|
|
33
|
+
- id: tiptap_mention_empty
|
|
34
|
+
type: TiptapMentionInput
|
|
35
|
+
|
|
36
|
+
- id: tiptap_mention_placeholder
|
|
37
|
+
type: TiptapMentionInput
|
|
38
|
+
properties:
|
|
39
|
+
placeholder: 'Type @ to mention...'
|
|
40
|
+
|
|
41
|
+
- id: tiptap_mention_with_value
|
|
42
|
+
type: TiptapMentionInput
|
|
43
|
+
|
|
44
|
+
- id: tiptap_mention_disabled
|
|
45
|
+
type: TiptapMentionInput
|
|
46
|
+
properties:
|
|
47
|
+
disabled: true
|
|
48
|
+
|
|
49
|
+
- id: tiptap_mention_options
|
|
50
|
+
type: TiptapMentionInput
|
|
51
|
+
properties:
|
|
52
|
+
placeholder: 'Try @A'
|
|
53
|
+
mentions:
|
|
54
|
+
options:
|
|
55
|
+
- label: Alice
|
|
56
|
+
value:
|
|
57
|
+
_id: user_1
|
|
58
|
+
- label: Bob
|
|
59
|
+
value:
|
|
60
|
+
_id: user_2
|
|
61
|
+
|
|
62
|
+
- id: tiptap_mention_saved
|
|
63
|
+
type: TiptapMentionInput
|
|
64
|
+
|
|
65
|
+
# Options whose value has no scalar identity (the shape an app passes when
|
|
66
|
+
# the whole contact record is the value) — data-id must be omitted, never
|
|
67
|
+
# "[object Object]".
|
|
68
|
+
- id: tiptap_mention_object_values
|
|
69
|
+
type: TiptapMentionInput
|
|
70
|
+
properties:
|
|
71
|
+
placeholder: 'Try @C'
|
|
72
|
+
mentions:
|
|
73
|
+
options:
|
|
74
|
+
- label: Carol
|
|
75
|
+
value:
|
|
76
|
+
contact_id: c_1
|
|
77
|
+
name: Carol
|
|
78
|
+
email: carol@example.com
|
|
79
|
+
|
|
80
|
+
- id: tiptap_mention_groups
|
|
81
|
+
type: TiptapMentionInput
|
|
82
|
+
properties:
|
|
83
|
+
title: Group mentions playground
|
|
84
|
+
placeholder: 'Type @ — People + Roles sections, group chips, hover members'
|
|
85
|
+
mentions:
|
|
86
|
+
limit: 3
|
|
87
|
+
groupMembers:
|
|
88
|
+
finance:
|
|
89
|
+
- name: Jane Doe
|
|
90
|
+
email: jane@example.com
|
|
91
|
+
- name: John Smith
|
|
92
|
+
email: john@example.com
|
|
93
|
+
devs:
|
|
94
|
+
- name: Ada Lovelace
|
|
95
|
+
email: ada@example.com
|
|
96
|
+
- name: Alan Turing
|
|
97
|
+
email: alan@example.com
|
|
98
|
+
# people carry value.href → <a>; groups omit it → nullish → <span>
|
|
99
|
+
getHref:
|
|
100
|
+
_function:
|
|
101
|
+
__args: 0.value.href
|
|
102
|
+
options:
|
|
103
|
+
- label: Alice
|
|
104
|
+
value:
|
|
105
|
+
name: Alice
|
|
106
|
+
href: '/contacts?_id=user_1'
|
|
107
|
+
tag:
|
|
108
|
+
section: People
|
|
109
|
+
- label: Amir
|
|
110
|
+
value:
|
|
111
|
+
name: Amir
|
|
112
|
+
href: '/contacts?_id=user_2'
|
|
113
|
+
tag:
|
|
114
|
+
section: People
|
|
115
|
+
- label: Anna
|
|
116
|
+
value:
|
|
117
|
+
name: Anna
|
|
118
|
+
href: '/contacts?_id=user_3'
|
|
119
|
+
tag:
|
|
120
|
+
section: People
|
|
121
|
+
- label: Aaron
|
|
122
|
+
value:
|
|
123
|
+
name: Aaron
|
|
124
|
+
href: '/contacts?_id=user_4'
|
|
125
|
+
tag:
|
|
126
|
+
section: People
|
|
127
|
+
- label: Finance
|
|
128
|
+
value:
|
|
129
|
+
role: finance-admin
|
|
130
|
+
tag:
|
|
131
|
+
section: Roles
|
|
132
|
+
group: finance
|
|
133
|
+
color: '#722ed1'
|
|
134
|
+
- label: Developers
|
|
135
|
+
value:
|
|
136
|
+
role: developer
|
|
137
|
+
tag:
|
|
138
|
+
section: Roles
|
|
139
|
+
group: devs
|
|
140
|
+
color: '#13c2c2'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Copyright 2020-2026 Lowdefy, Inc
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
- id: default
|
|
16
|
+
type: TiptapMentionInput
|
|
17
|
+
- id: properties.placeholder
|
|
18
|
+
type: TiptapMentionInput
|
|
19
|
+
properties:
|
|
20
|
+
placeholder: 'Type @ to mention...'
|
|
21
|
+
- id: properties.disabled
|
|
22
|
+
type: TiptapMentionInput
|
|
23
|
+
properties:
|
|
24
|
+
disabled: true
|
|
25
|
+
- id: properties.mentions.options
|
|
26
|
+
type: TiptapMentionInput
|
|
27
|
+
properties:
|
|
28
|
+
mentions:
|
|
29
|
+
options:
|
|
30
|
+
- label: Alice
|
|
31
|
+
value:
|
|
32
|
+
_id: user_1
|
|
33
|
+
- label: Bob
|
|
34
|
+
value:
|
|
35
|
+
_id: user_2
|
|
36
|
+
- id: properties.mentions.char = '#'
|
|
37
|
+
type: TiptapMentionInput
|
|
38
|
+
properties:
|
|
39
|
+
placeholder: 'Type # to tag'
|
|
40
|
+
mentions:
|
|
41
|
+
char: '#'
|
|
42
|
+
options:
|
|
43
|
+
- label: bug
|
|
44
|
+
value:
|
|
45
|
+
_id: tag_bug
|
|
46
|
+
- id: properties.mentions.limit with sections
|
|
47
|
+
type: TiptapMentionInput
|
|
48
|
+
properties:
|
|
49
|
+
mentions:
|
|
50
|
+
limit: 2
|
|
51
|
+
options:
|
|
52
|
+
- label: Alice
|
|
53
|
+
value:
|
|
54
|
+
_id: user_1
|
|
55
|
+
tag:
|
|
56
|
+
section: People
|
|
57
|
+
- label: Bob
|
|
58
|
+
value:
|
|
59
|
+
_id: user_2
|
|
60
|
+
tag:
|
|
61
|
+
section: People
|
|
62
|
+
- label: Finance
|
|
63
|
+
value:
|
|
64
|
+
_id: role_finance
|
|
65
|
+
tag:
|
|
66
|
+
section: Roles
|
|
67
|
+
- id: properties.mentions group chips with tag.color
|
|
68
|
+
type: TiptapMentionInput
|
|
69
|
+
properties:
|
|
70
|
+
mentions:
|
|
71
|
+
options:
|
|
72
|
+
- label: Alice
|
|
73
|
+
value:
|
|
74
|
+
_id: user_1
|
|
75
|
+
tag:
|
|
76
|
+
section: People
|
|
77
|
+
- label: Finance
|
|
78
|
+
value:
|
|
79
|
+
role: finance-admin
|
|
80
|
+
tag:
|
|
81
|
+
section: Roles
|
|
82
|
+
group: finance
|
|
83
|
+
color: '#722ed1'
|
|
84
|
+
- label: Developers
|
|
85
|
+
value:
|
|
86
|
+
role: developer
|
|
87
|
+
tag:
|
|
88
|
+
section: Roles
|
|
89
|
+
group: devs
|
|
90
|
+
color: '#13c2c2'
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { useRef } from 'react';
|
|
16
16
|
import TurndownService from 'turndown';
|
|
17
|
-
import
|
|
17
|
+
import fileUpload from '../utils/fileUpload.js';
|
|
18
18
|
// Ref-tracked controller for the mention editor's value. Mirrors TiptapInput's
|
|
19
19
|
// useTiptapState but also extracts mentions from the document.
|
|
20
|
-
function useTiptapMentionState({ value, methods }) {
|
|
20
|
+
function useTiptapMentionState({ value, methods, hasDownloadRequest }) {
|
|
21
21
|
const valueRef = useRef(value);
|
|
22
22
|
valueRef.current = value;
|
|
23
23
|
const turndownService = new TurndownService();
|
|
@@ -60,9 +60,10 @@ function useTiptapMentionState({ value, methods }) {
|
|
|
60
60
|
});
|
|
61
61
|
};
|
|
62
62
|
const insertImage = async (editor, file, pos)=>{
|
|
63
|
-
const url = await
|
|
63
|
+
const url = await fileUpload({
|
|
64
64
|
file,
|
|
65
|
-
methods
|
|
65
|
+
methods,
|
|
66
|
+
hasDownloadRequest
|
|
66
67
|
});
|
|
67
68
|
// The upload is async: bail if the editor was torn down meanwhile (e.g. the
|
|
68
69
|
// surrounding page navigated away before the upload resolved), otherwise the
|
|
@@ -35,36 +35,69 @@ const HIGHLIGHT_SWATCHES = [
|
|
|
35
35
|
fill: 'rgba(170, 0, 255, 0.5)'
|
|
36
36
|
}
|
|
37
37
|
];
|
|
38
|
+
// Placement of the TipTap v2 (tippy.js) bubble menu: 10px above the selection,
|
|
39
|
+
// flipping below when that would leave less than 5px to the viewport edge, and
|
|
40
|
+
// kept 5px from the sides. v3 positions the menu with Floating UI and applies
|
|
41
|
+
// the offset after flip, so flip's padding includes the offset.
|
|
42
|
+
const FLOATING_OPTIONS = {
|
|
43
|
+
placement: 'top',
|
|
44
|
+
offset: 10,
|
|
45
|
+
flip: {
|
|
46
|
+
padding: 15
|
|
47
|
+
},
|
|
48
|
+
shift: {
|
|
49
|
+
padding: 5
|
|
50
|
+
}
|
|
51
|
+
};
|
|
38
52
|
function hasExt(editor, name) {
|
|
39
53
|
return editor.extensionManager.extensions.some((ext)=>ext.name === name);
|
|
40
54
|
}
|
|
55
|
+
function createMenuElements() {
|
|
56
|
+
if (typeof document === 'undefined') return null;
|
|
57
|
+
// `root` is the element the plugin positions and mounts, like tippy's root
|
|
58
|
+
// in v2 (same z-index). `content` is the styled menu the buttons render into.
|
|
59
|
+
// The root is absolute from the start so the plugin measures its final size
|
|
60
|
+
// the first time it positions it.
|
|
61
|
+
const root = document.createElement('div');
|
|
62
|
+
root.style.position = 'absolute';
|
|
63
|
+
root.style.visibility = 'hidden';
|
|
64
|
+
root.style.zIndex = '9999';
|
|
65
|
+
const content = document.createElement('div');
|
|
66
|
+
content.className = 'tiptap-popover';
|
|
67
|
+
root.appendChild(content);
|
|
68
|
+
return {
|
|
69
|
+
root,
|
|
70
|
+
content
|
|
71
|
+
};
|
|
72
|
+
}
|
|
41
73
|
// Custom bubble menu built on tiptap's BubbleMenuPlugin instead of the
|
|
42
|
-
// `<BubbleMenu>` React wrapper. The
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// where it rendered it, throwing "NotFoundError: Failed to execute
|
|
48
|
-
// 'removeChild' on 'Node'".
|
|
74
|
+
// `<BubbleMenu>` React wrapper. The plugin moves its menu element in and out of
|
|
75
|
+
// the DOM itself. When the editor block later unmounts — e.g. the surrounding
|
|
76
|
+
// page navigates away, or `disabled` flips and unmounts this menu — React
|
|
77
|
+
// would try to removeChild an element that is no longer where it rendered it,
|
|
78
|
+
// throwing "NotFoundError: Failed to execute 'removeChild' on 'Node'".
|
|
49
79
|
//
|
|
50
|
-
// Here we own the
|
|
51
|
-
// with a portal. React only ever removes the buttons from our
|
|
52
|
-
// their parent), never the plugin-
|
|
53
|
-
// gone.
|
|
80
|
+
// Here we own the menu elements ourselves and render the buttons into them
|
|
81
|
+
// with a portal. React only ever removes the buttons from our content element
|
|
82
|
+
// (always their parent), never the plugin-managed root, so the teardown race
|
|
83
|
+
// is gone.
|
|
54
84
|
const PopoverMenu = ({ editor })=>{
|
|
55
|
-
const [
|
|
85
|
+
const [menu] = useState(createMenuElements);
|
|
56
86
|
const showBold = hasExt(editor, 'bold');
|
|
57
87
|
const showItalic = hasExt(editor, 'italic');
|
|
58
88
|
const showStrike = hasExt(editor, 'strike');
|
|
59
89
|
const showHighlight = hasExt(editor, 'highlight');
|
|
60
90
|
const hasTools = showBold || showItalic || showStrike || showHighlight;
|
|
61
91
|
useEffect(()=>{
|
|
62
|
-
if (!
|
|
63
|
-
container.className = 'tiptap-popover';
|
|
92
|
+
if (!menu || !editor || editor.isDestroyed || !hasTools) return undefined;
|
|
64
93
|
const plugin = BubbleMenuPlugin({
|
|
65
94
|
pluginKey: 'bubbleMenu',
|
|
66
95
|
editor,
|
|
67
|
-
element:
|
|
96
|
+
element: menu.root,
|
|
97
|
+
// v2 mounted the menu beside the editor wrapper; v3 defaults to inside it.
|
|
98
|
+
// `options.element` is the EditorContent wrapper once it has mounted.
|
|
99
|
+
appendTo: ()=>editor.options.element?.parentElement,
|
|
100
|
+
options: FLOATING_OPTIONS,
|
|
68
101
|
shouldShow: ({ editor: menuEditor, view, state, from, to })=>{
|
|
69
102
|
if (menuEditor.isActive('image')) return false;
|
|
70
103
|
const { selection } = state;
|
|
@@ -79,19 +112,19 @@ const PopoverMenu = ({ editor })=>{
|
|
|
79
112
|
});
|
|
80
113
|
editor.registerPlugin(plugin);
|
|
81
114
|
return ()=>{
|
|
82
|
-
// Tear the plugin (
|
|
83
|
-
// unmounts the portal, removing the buttons from our still-intact
|
|
84
|
-
//
|
|
115
|
+
// Tear the plugin down first (it removes the root from the DOM). React
|
|
116
|
+
// then unmounts the portal, removing the buttons from our still-intact
|
|
117
|
+
// content element.
|
|
85
118
|
if (!editor.isDestroyed) {
|
|
86
119
|
editor.unregisterPlugin('bubbleMenu');
|
|
87
120
|
}
|
|
88
121
|
};
|
|
89
122
|
}, [
|
|
90
123
|
editor,
|
|
91
|
-
|
|
124
|
+
menu,
|
|
92
125
|
hasTools
|
|
93
126
|
]);
|
|
94
|
-
if (!
|
|
127
|
+
if (!menu || !hasTools) return null;
|
|
95
128
|
return /*#__PURE__*/ createPortal(/*#__PURE__*/ React.createElement(React.Fragment, null, showBold && /*#__PURE__*/ React.createElement(AiOutlineBold, {
|
|
96
129
|
className: "tiptap-icon",
|
|
97
130
|
onClick: ()=>editor.chain().focus().toggleBold().run()
|
|
@@ -110,6 +143,6 @@ const PopoverMenu = ({ editor })=>{
|
|
|
110
143
|
onClick: ()=>editor.chain().focus().toggleHighlight({
|
|
111
144
|
color: fill
|
|
112
145
|
}).run()
|
|
113
|
-
}))),
|
|
146
|
+
}))), menu.content);
|
|
114
147
|
};
|
|
115
148
|
export default PopoverMenu;
|
|
@@ -15,14 +15,11 @@
|
|
|
15
15
|
*/ import FileHandler from '@tiptap/extension-file-handler';
|
|
16
16
|
import Highlight from '@tiptap/extension-highlight';
|
|
17
17
|
import Image from '@tiptap/extension-image';
|
|
18
|
-
import Placeholder from '@tiptap/
|
|
19
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
18
|
+
import { Placeholder } from '@tiptap/extensions';
|
|
20
19
|
import LinkExtension from '@tiptap/extension-link';
|
|
21
|
-
import Table from '@tiptap/extension-table';
|
|
22
|
-
import TableCell from '@tiptap/extension-table-cell';
|
|
23
|
-
import TableHeader from '@tiptap/extension-table-header';
|
|
24
|
-
import TableRow from '@tiptap/extension-table-row';
|
|
20
|
+
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
|
|
25
21
|
import { type } from '@lowdefy/helpers';
|
|
22
|
+
import buildStarterKit from './buildStarterKit.js';
|
|
26
23
|
const DEFAULT_IMAGE_MIME_TYPES = [
|
|
27
24
|
'image/jpeg',
|
|
28
25
|
'image/png',
|
|
@@ -58,15 +55,14 @@ function merge(defaults, overrides) {
|
|
|
58
55
|
...overrides
|
|
59
56
|
};
|
|
60
57
|
}
|
|
61
|
-
function buildExtensions({ properties, insertImage, mentionExtension, uploadEnabled }) {
|
|
58
|
+
function buildExtensions({ properties, getPlaceholder, insertImage, mentionExtension, uploadEnabled }) {
|
|
62
59
|
const image = merge(DEFAULTS.image, properties.image);
|
|
63
60
|
const table = merge(DEFAULTS.table, properties.table);
|
|
64
61
|
const link = merge(DEFAULTS.link, properties.link);
|
|
65
62
|
const highlight = merge(DEFAULTS.highlight, properties.highlight);
|
|
66
|
-
const starterKitOptions = type.isObject(properties.starterKit) ? properties.starterKit : {};
|
|
67
63
|
const allowedMimeTypes = type.isArray(properties.allowedMimeTypes) ? properties.allowedMimeTypes : DEFAULT_IMAGE_MIME_TYPES;
|
|
68
64
|
const extensions = [
|
|
69
|
-
|
|
65
|
+
buildStarterKit(properties.starterKit)
|
|
70
66
|
];
|
|
71
67
|
if (!table.disabled) {
|
|
72
68
|
extensions.push(Table.configure({
|
|
@@ -84,9 +80,8 @@ function buildExtensions({ properties, insertImage, mentionExtension, uploadEnab
|
|
|
84
80
|
}));
|
|
85
81
|
}
|
|
86
82
|
extensions.push(Placeholder.configure({
|
|
87
|
-
placeholder:
|
|
88
|
-
showOnlyWhenEditable: false
|
|
89
|
-
considerAnyAsEmpty: true
|
|
83
|
+
placeholder: getPlaceholder,
|
|
84
|
+
showOnlyWhenEditable: false
|
|
90
85
|
}));
|
|
91
86
|
if (!highlight.disabled) {
|
|
92
87
|
extensions.push(Highlight.configure({
|
|
@@ -101,7 +96,10 @@ function buildExtensions({ properties, insertImage, mentionExtension, uploadEnab
|
|
|
101
96
|
autolink: link.autolink,
|
|
102
97
|
linkOnPaste: link.linkOnPaste,
|
|
103
98
|
openOnClick: link.openOnClick,
|
|
104
|
-
defaultProtocol: link.defaultProtocol
|
|
99
|
+
defaultProtocol: link.defaultProtocol,
|
|
100
|
+
// TipTap v2's default. v3's default also skips bare IP addresses and hosts without a TLD
|
|
101
|
+
// (e.g. localhost:3000), and applies to pasted URLs too.
|
|
102
|
+
shouldAutoLink: (url)=>!!url
|
|
105
103
|
}));
|
|
106
104
|
}
|
|
107
105
|
if (mentionExtension) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import StarterKit from '@tiptap/starter-kit';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
// TipTap v3's StarterKit bundles extensions the v2 kit did not. They default to off so the
|
|
18
|
+
// editor keeps its v2 extension set: Underline would keep <u> in the saved html, TrailingNode
|
|
19
|
+
// appends an empty paragraph after a final table or image, and ListKeymap changes Backspace and
|
|
20
|
+
// Delete in lists.
|
|
21
|
+
const V3_KIT_DEFAULTS = {
|
|
22
|
+
listKeymap: false,
|
|
23
|
+
trailingNode: false,
|
|
24
|
+
underline: false
|
|
25
|
+
};
|
|
26
|
+
// Builds the StarterKit from the block's `starterKit` property, which is written against the v2
|
|
27
|
+
// kit, so that it enables the same extensions as v2 did.
|
|
28
|
+
function buildStarterKit(starterKit) {
|
|
29
|
+
const { history, ...options } = type.isObject(starterKit) ? starterKit : {};
|
|
30
|
+
// v3 renamed the History extension to UndoRedo.
|
|
31
|
+
if (!type.isUndefined(history) && type.isUndefined(options.undoRedo)) {
|
|
32
|
+
options.undoRedo = history;
|
|
33
|
+
}
|
|
34
|
+
// v3 CodeBlock inserts a paragraph above a code block at the top of the document on ArrowUp.
|
|
35
|
+
if (options.codeBlock !== false) {
|
|
36
|
+
options.codeBlock = {
|
|
37
|
+
exitOnArrowUp: false,
|
|
38
|
+
...type.isObject(options.codeBlock) ? options.codeBlock : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return StarterKit.configure({
|
|
42
|
+
...V3_KIT_DEFAULTS,
|
|
43
|
+
...options,
|
|
44
|
+
// The Link extension is added by buildExtensions from the block's `link` property.
|
|
45
|
+
link: false
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export default buildStarterKit;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import getLegacyObjectUrl from '@lowdefy/blocks-files/utils/getLegacyObjectUrl.js';
|
|
17
|
+
import getUploadPolicy from '@lowdefy/blocks-files/utils/getUploadPolicy.js';
|
|
18
|
+
import uploadFile from '@lowdefy/blocks-files/utils/uploadFile.js';
|
|
19
|
+
// Uploads a file through the app's upload-policy request (any storage
|
|
20
|
+
// provider) and resolves a display URL for the inline image. Inline images
|
|
21
|
+
// persist in saved markdown, so they must be public assets: the upload
|
|
22
|
+
// request should set the provider's public-read acl, and the download request
|
|
23
|
+
// should set public: true to return a stable, non-expiring URL. Without a
|
|
24
|
+
// download request the legacy unsigned object URL is used (S3-shaped,
|
|
25
|
+
// deprecated).
|
|
26
|
+
async function fileUpload({ file, methods, hasDownloadRequest }) {
|
|
27
|
+
if (type.isNone(file)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const descriptor = await getUploadPolicy({
|
|
31
|
+
methods,
|
|
32
|
+
file
|
|
33
|
+
});
|
|
34
|
+
file.bucket = descriptor.bucket;
|
|
35
|
+
file.key = descriptor.key;
|
|
36
|
+
await uploadFile({
|
|
37
|
+
descriptor,
|
|
38
|
+
file
|
|
39
|
+
});
|
|
40
|
+
if (hasDownloadRequest) {
|
|
41
|
+
const response = await methods.triggerEvent({
|
|
42
|
+
name: '__getDownloadPolicy',
|
|
43
|
+
event: {
|
|
44
|
+
file: {
|
|
45
|
+
bucket: descriptor.bucket,
|
|
46
|
+
key: descriptor.key,
|
|
47
|
+
name: file.name,
|
|
48
|
+
type: file.type
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (response.success !== true) {
|
|
53
|
+
throw new Error('Download policy request error.');
|
|
54
|
+
}
|
|
55
|
+
file.url = response.responses.__getDownloadPolicy.response[0];
|
|
56
|
+
return file.url;
|
|
57
|
+
}
|
|
58
|
+
file.url = getLegacyObjectUrl({
|
|
59
|
+
descriptor
|
|
60
|
+
});
|
|
61
|
+
return file.url;
|
|
62
|
+
}
|
|
63
|
+
export default fileUpload;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ // Map validation.status → wrapper class. Matches the visual behavior of
|
|
16
16
|
// passing status="error" | "warning" to an antd <Input />, but rendered
|
|
17
|
-
// with our own CSS (see utils/style.
|
|
17
|
+
// with our own CSS (see utils/style.css) because antd v6 uses
|
|
18
18
|
// CSS-in-JS, so the .ant-input-status-* classes are not global.
|
|
19
19
|
function statusClass(status) {
|
|
20
20
|
if (status === 'error') return 'tiptap-wrapper-error';
|