@seorii/tiptap 0.3.0 → 0.4.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/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/command/suggest.js +22 -4
- package/dist/plugin/embed.js +1 -1
- package/dist/plugin/iframe.js +1 -1
- package/dist/plugin/image/dragdrop.js +63 -16
- package/dist/plugin/image/index.js +1 -1
- package/dist/plugin/resize/index.d.ts +8 -0
- package/dist/plugin/resize/index.js +454 -0
- package/dist/plugin/upload/skeleton/UploadSkeleton.svelte +97 -0
- package/dist/plugin/upload/skeleton/UploadSkeleton.svelte.d.ts +5 -0
- package/dist/plugin/upload/skeleton/index.d.ts +30 -0
- package/dist/plugin/upload/skeleton/index.js +147 -0
- package/dist/plugin/youtube.js +1 -1
- package/dist/tiptap/Bubble.svelte +139 -34
- package/dist/tiptap/Bubble.svelte.d.ts +1 -0
- package/dist/tiptap/TipTap.svelte +94 -9
- package/dist/tiptap/TipTap.svelte.d.ts +3 -0
- package/dist/tiptap/tiptap.js +2 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Reexport your entry components here
|
|
2
2
|
import TipTap from './tiptap/index.js';
|
|
3
3
|
import { getDetail } from './plugin/command/suggest.js';
|
|
4
|
+
import { insertUploadSkeleton } from './plugin/upload/skeleton/index.js';
|
|
4
5
|
|
|
5
6
|
export default TipTap;
|
|
6
|
-
export { getDetail };
|
|
7
|
+
export { getDetail, insertUploadSkeleton };
|
|
@@ -3,6 +3,7 @@ import i18n from '../../i18n';
|
|
|
3
3
|
import enUs from '../../i18n/en-us/index';
|
|
4
4
|
import koKr from '../../i18n/ko-kr/index';
|
|
5
5
|
import { fallbackUpload, releaseObjectUrlOnImageSettled } from '../image/dragdrop';
|
|
6
|
+
import { insertUploadSkeleton } from '../upload/skeleton';
|
|
6
7
|
import { PluginKey, TextSelection } from '@tiptap/pm/state';
|
|
7
8
|
import Suggestion, {} from '@tiptap/suggestion';
|
|
8
9
|
const normalizeSearch = (value) => value.toLowerCase().trim();
|
|
@@ -156,10 +157,27 @@ export const suggest = {
|
|
|
156
157
|
const file = input.files[0];
|
|
157
158
|
if (!file)
|
|
158
159
|
return;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
const skeleton = insertUploadSkeleton(editor, {
|
|
161
|
+
kind: 'image',
|
|
162
|
+
height: 220
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
const upload = window.__image_uploader ?? fallbackUpload;
|
|
166
|
+
const src = await upload(file);
|
|
167
|
+
if (skeleton) {
|
|
168
|
+
skeleton.replaceWith({
|
|
169
|
+
type: 'image',
|
|
170
|
+
attrs: { src }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
editor.chain().focus().setImage({ src }).run();
|
|
175
|
+
}
|
|
176
|
+
releaseObjectUrlOnImageSettled(editor.view, src);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
skeleton?.remove();
|
|
180
|
+
}
|
|
163
181
|
};
|
|
164
182
|
input.click();
|
|
165
183
|
}
|
package/dist/plugin/embed.js
CHANGED
|
@@ -34,7 +34,7 @@ export default Node.create({
|
|
|
34
34
|
renderHTML({ HTMLAttributes }) {
|
|
35
35
|
return [
|
|
36
36
|
'div',
|
|
37
|
-
this.options.HTMLAttributes,
|
|
37
|
+
mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
|
|
38
38
|
['embed', mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })]
|
|
39
39
|
];
|
|
40
40
|
},
|
package/dist/plugin/iframe.js
CHANGED
|
@@ -31,7 +31,7 @@ export default Node.create({
|
|
|
31
31
|
renderHTML({ HTMLAttributes }) {
|
|
32
32
|
return [
|
|
33
33
|
'div',
|
|
34
|
-
this.options.HTMLAttributes,
|
|
34
|
+
mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
|
|
35
35
|
[
|
|
36
36
|
'iframe',
|
|
37
37
|
mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { insertUploadSkeleton } from '../upload/skeleton';
|
|
2
3
|
export const fallbackUpload = async (image) => URL.createObjectURL(image);
|
|
3
4
|
const OBJECT_URL_PREFIX = 'blob:';
|
|
4
5
|
const OBJECT_URL_REVOKE_TIMEOUT_MS = 30_000;
|
|
@@ -73,14 +74,31 @@ export const dropImagePlugin = () => {
|
|
|
73
74
|
const image = item.getAsFile();
|
|
74
75
|
if (item.type.indexOf('image') === 0) {
|
|
75
76
|
event.preventDefault();
|
|
77
|
+
const skeleton = insertUploadSkeleton({
|
|
78
|
+
state: view.state,
|
|
79
|
+
view
|
|
80
|
+
}, {
|
|
81
|
+
kind: 'image',
|
|
82
|
+
height: 220
|
|
83
|
+
});
|
|
76
84
|
if (upload && image) {
|
|
77
|
-
upload(image)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
upload(image)
|
|
86
|
+
.then((src) => {
|
|
87
|
+
if (skeleton) {
|
|
88
|
+
skeleton.replaceWith({
|
|
89
|
+
type: 'image',
|
|
90
|
+
attrs: { src }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const node = schema.nodes.image.create({ src });
|
|
95
|
+
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
96
|
+
view.dispatch(transaction);
|
|
97
|
+
}
|
|
83
98
|
releaseObjectUrlOnImageSettled(view, src);
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {
|
|
101
|
+
skeleton?.remove();
|
|
84
102
|
});
|
|
85
103
|
}
|
|
86
104
|
}
|
|
@@ -120,20 +138,49 @@ export const dropImagePlugin = () => {
|
|
|
120
138
|
return false;
|
|
121
139
|
images.forEach(async (image) => {
|
|
122
140
|
const reader = new FileReader();
|
|
141
|
+
const skeleton = insertUploadSkeleton({
|
|
142
|
+
state: view.state,
|
|
143
|
+
view
|
|
144
|
+
}, {
|
|
145
|
+
kind: 'image',
|
|
146
|
+
height: 220,
|
|
147
|
+
at: coordinates.pos
|
|
148
|
+
});
|
|
123
149
|
if (upload) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
try {
|
|
151
|
+
const src = await upload(image);
|
|
152
|
+
if (skeleton) {
|
|
153
|
+
skeleton.replaceWith({
|
|
154
|
+
type: 'image',
|
|
155
|
+
attrs: { src }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const node = schema.nodes.image.create({ src });
|
|
160
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
161
|
+
view.dispatch(transaction);
|
|
162
|
+
}
|
|
163
|
+
releaseObjectUrlOnImageSettled(view, src);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
skeleton?.remove();
|
|
167
|
+
}
|
|
131
168
|
}
|
|
132
169
|
else {
|
|
133
170
|
reader.onload = (readerEvent) => {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
const src = readerEvent.target?.result;
|
|
172
|
+
if (typeof src !== 'string') {
|
|
173
|
+
skeleton?.remove();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (skeleton) {
|
|
177
|
+
skeleton.replaceWith({
|
|
178
|
+
type: 'image',
|
|
179
|
+
attrs: { src }
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const node = schema.nodes.image.create({ src });
|
|
137
184
|
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
138
185
|
view.dispatch(transaction);
|
|
139
186
|
};
|
|
@@ -14,7 +14,7 @@ export default (crossorigin = 'anonymous') => Image.extend({
|
|
|
14
14
|
const style = HTMLAttributes.style;
|
|
15
15
|
return [
|
|
16
16
|
'figure',
|
|
17
|
-
{ style },
|
|
17
|
+
{ style, 'data-bubble-menu': 'false' },
|
|
18
18
|
['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
|
19
19
|
];
|
|
20
20
|
},
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
4
|
+
const resizableTypes = ['image', 'iframe', 'embed', 'tiptap-midibus'];
|
|
5
|
+
const typeSet = new Set(resizableTypes);
|
|
6
|
+
const pluginKey = new PluginKey('tiptap-media-height-resize');
|
|
7
|
+
const defaultHeight = {
|
|
8
|
+
image: 360,
|
|
9
|
+
iframe: 600,
|
|
10
|
+
embed: 500,
|
|
11
|
+
'tiptap-midibus': 600,
|
|
12
|
+
attr: 420
|
|
13
|
+
};
|
|
14
|
+
const minHeight = {
|
|
15
|
+
image: 120,
|
|
16
|
+
iframe: 180,
|
|
17
|
+
embed: 200,
|
|
18
|
+
'tiptap-midibus': 220,
|
|
19
|
+
attr: 160
|
|
20
|
+
};
|
|
21
|
+
const maxHeight = 1600;
|
|
22
|
+
function isResizableType(value) {
|
|
23
|
+
return typeSet.has(value);
|
|
24
|
+
}
|
|
25
|
+
function clamp(value, min, max) {
|
|
26
|
+
return Math.min(max, Math.max(min, value));
|
|
27
|
+
}
|
|
28
|
+
function parseNumericSize(value) {
|
|
29
|
+
if (typeof value === 'number')
|
|
30
|
+
return Number.isFinite(value) ? value : null;
|
|
31
|
+
if (typeof value !== 'string')
|
|
32
|
+
return null;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return null;
|
|
36
|
+
const normalized = trimmed.toLowerCase().endsWith('px') ? trimmed.slice(0, -2) : trimmed;
|
|
37
|
+
const parsed = Number.parseFloat(normalized);
|
|
38
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
39
|
+
}
|
|
40
|
+
function normalizeStringAttr(value) {
|
|
41
|
+
if (typeof value !== 'string')
|
|
42
|
+
return null;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed || null;
|
|
45
|
+
}
|
|
46
|
+
function hasResizeHandler(value) {
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
const normalized = value.trim().toLowerCase();
|
|
49
|
+
if (!normalized)
|
|
50
|
+
return true;
|
|
51
|
+
return !['false', '0', 'off', 'no'].includes(normalized);
|
|
52
|
+
}
|
|
53
|
+
return Boolean(value);
|
|
54
|
+
}
|
|
55
|
+
function normalizeNumericAttr(value) {
|
|
56
|
+
const parsed = parseNumericSize(value);
|
|
57
|
+
if (parsed === null)
|
|
58
|
+
return null;
|
|
59
|
+
return String(Math.round(parsed));
|
|
60
|
+
}
|
|
61
|
+
function normalizeWidthAttr(value) {
|
|
62
|
+
if (typeof value === 'number')
|
|
63
|
+
return String(Math.round(value));
|
|
64
|
+
if (typeof value !== 'string')
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return null;
|
|
69
|
+
if (trimmed.endsWith('%'))
|
|
70
|
+
return trimmed;
|
|
71
|
+
return normalizeNumericAttr(trimmed);
|
|
72
|
+
}
|
|
73
|
+
function resolveResizeMeta(node) {
|
|
74
|
+
const typeName = node.type.name;
|
|
75
|
+
const kind = isResizableType(typeName)
|
|
76
|
+
? typeName
|
|
77
|
+
: hasResizeHandler(node.attrs.resizeHandler)
|
|
78
|
+
? 'attr'
|
|
79
|
+
: null;
|
|
80
|
+
if (!kind)
|
|
81
|
+
return null;
|
|
82
|
+
const minFromAttr = parseNumericSize(node.attrs.minHeight);
|
|
83
|
+
const maxFromAttr = parseNumericSize(node.attrs.maxHeight);
|
|
84
|
+
const resolvedMin = minFromAttr !== null ? Math.max(1, minFromAttr) : minHeight[kind];
|
|
85
|
+
const resolvedMax = maxFromAttr !== null ? Math.max(resolvedMin, maxFromAttr) : maxHeight;
|
|
86
|
+
return {
|
|
87
|
+
kind,
|
|
88
|
+
typeName,
|
|
89
|
+
minHeight: resolvedMin,
|
|
90
|
+
maxHeight: resolvedMax
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function resolveImageRatio(node, element) {
|
|
94
|
+
if (element instanceof HTMLImageElement && element.naturalWidth && element.naturalHeight) {
|
|
95
|
+
return element.naturalWidth / element.naturalHeight;
|
|
96
|
+
}
|
|
97
|
+
const rect = element.getBoundingClientRect();
|
|
98
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
99
|
+
return rect.width / rect.height;
|
|
100
|
+
}
|
|
101
|
+
const width = parseNumericSize(node.attrs.width);
|
|
102
|
+
const height = parseNumericSize(node.attrs.height);
|
|
103
|
+
if (width && height)
|
|
104
|
+
return width / height;
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
function resolveStartHeight(kind, node, element) {
|
|
108
|
+
const rect = element.getBoundingClientRect();
|
|
109
|
+
if (rect.height > 0)
|
|
110
|
+
return rect.height;
|
|
111
|
+
const fromAttr = parseNumericSize(node.attrs.height);
|
|
112
|
+
if (fromAttr !== null)
|
|
113
|
+
return fromAttr;
|
|
114
|
+
return defaultHeight[kind];
|
|
115
|
+
}
|
|
116
|
+
function resolveTargetElement(view, pos, resizeMeta, node) {
|
|
117
|
+
const nodeDom = view.nodeDOM(pos);
|
|
118
|
+
if (!(nodeDom instanceof HTMLElement))
|
|
119
|
+
return null;
|
|
120
|
+
if (resizeMeta.kind === 'image') {
|
|
121
|
+
if (nodeDom instanceof HTMLImageElement)
|
|
122
|
+
return nodeDom;
|
|
123
|
+
return nodeDom.querySelector('img');
|
|
124
|
+
}
|
|
125
|
+
if (resizeMeta.kind === 'iframe') {
|
|
126
|
+
if (nodeDom instanceof HTMLIFrameElement)
|
|
127
|
+
return nodeDom;
|
|
128
|
+
return nodeDom.querySelector('iframe');
|
|
129
|
+
}
|
|
130
|
+
if (resizeMeta.kind === 'embed') {
|
|
131
|
+
return (nodeDom.querySelector('[data-tiptap-pdf-container]') ||
|
|
132
|
+
nodeDom.querySelector('embed') ||
|
|
133
|
+
nodeDom);
|
|
134
|
+
}
|
|
135
|
+
if (resizeMeta.kind === 'attr') {
|
|
136
|
+
const preferredSelector = normalizeStringAttr(node.attrs.resizeTarget);
|
|
137
|
+
if (preferredSelector) {
|
|
138
|
+
try {
|
|
139
|
+
const preferredTarget = nodeDom.querySelector(preferredSelector);
|
|
140
|
+
if (preferredTarget)
|
|
141
|
+
return preferredTarget;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// ignore invalid selector values
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return (nodeDom.querySelector('[data-tiptap-resize-target]') ||
|
|
148
|
+
nodeDom.querySelector('[data-resize-target]') ||
|
|
149
|
+
nodeDom.querySelector('iframe, embed, img') ||
|
|
150
|
+
nodeDom);
|
|
151
|
+
}
|
|
152
|
+
return (nodeDom.querySelector('[data-tiptap-midibus-frame]') ||
|
|
153
|
+
nodeDom.querySelector('iframe') ||
|
|
154
|
+
nodeDom);
|
|
155
|
+
}
|
|
156
|
+
function buildResizeAttrs(kind, node, height, imageRatio) {
|
|
157
|
+
const attrs = { ...node.attrs };
|
|
158
|
+
const roundedHeight = String(Math.round(height));
|
|
159
|
+
if (kind === 'image') {
|
|
160
|
+
const roundedWidth = String(Math.max(1, Math.round(height * imageRatio)));
|
|
161
|
+
return { ...attrs, width: roundedWidth, height: roundedHeight };
|
|
162
|
+
}
|
|
163
|
+
if (kind === 'iframe' || kind === 'embed') {
|
|
164
|
+
return { ...attrs, width: attrs.width || '100%', height: roundedHeight };
|
|
165
|
+
}
|
|
166
|
+
return { ...attrs, height: roundedHeight };
|
|
167
|
+
}
|
|
168
|
+
function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta) {
|
|
169
|
+
return Decoration.widget(widgetPos, () => {
|
|
170
|
+
const anchor = document.createElement('div');
|
|
171
|
+
anchor.className = 'tiptap-media-resize-anchor';
|
|
172
|
+
const button = document.createElement('button');
|
|
173
|
+
button.type = 'button';
|
|
174
|
+
button.className = 'tiptap-media-resize-handle';
|
|
175
|
+
button.dataset.resizePos = String(nodePos);
|
|
176
|
+
button.dataset.resizeKind = resizeMeta.kind;
|
|
177
|
+
button.setAttribute('aria-label', 'Resize media height');
|
|
178
|
+
anchor.append(button);
|
|
179
|
+
return anchor;
|
|
180
|
+
}, { side: 1, key: `media-resize-${nodePos}-${resizeMeta.typeName}-${resizeMeta.kind}` });
|
|
181
|
+
}
|
|
182
|
+
function tryCreateNodeSelection(doc, pos) {
|
|
183
|
+
if (pos < 0 || pos > doc.content.size)
|
|
184
|
+
return null;
|
|
185
|
+
const node = doc.nodeAt(pos);
|
|
186
|
+
if (!node || node.type.spec.selectable === false)
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
return NodeSelection.create(doc, pos);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export default Extension.create({
|
|
196
|
+
name: 'tiptap-media-height-resize',
|
|
197
|
+
addOptions() {
|
|
198
|
+
return {
|
|
199
|
+
attributeTypes: [],
|
|
200
|
+
showHandleAlways: true,
|
|
201
|
+
showHandleOnActive: true
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
addGlobalAttributes() {
|
|
205
|
+
const attributeTypes = Array.from(new Set([...(this.options.attributeTypes || []), ...resizableTypes]));
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
types: ['image'],
|
|
209
|
+
attributes: {
|
|
210
|
+
width: {
|
|
211
|
+
default: null,
|
|
212
|
+
parseHTML: (element) => normalizeWidthAttr(element.getAttribute('width') || element.style.width),
|
|
213
|
+
renderHTML: (attributes) => {
|
|
214
|
+
const width = normalizeWidthAttr(attributes.width);
|
|
215
|
+
return width ? { width } : {};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
height: {
|
|
219
|
+
default: null,
|
|
220
|
+
parseHTML: (element) => normalizeNumericAttr(element.getAttribute('height') || element.style.height),
|
|
221
|
+
renderHTML: (attributes) => {
|
|
222
|
+
const height = normalizeNumericAttr(attributes.height);
|
|
223
|
+
return height ? { height } : {};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
types: ['iframe'],
|
|
230
|
+
attributes: {
|
|
231
|
+
width: {
|
|
232
|
+
default: '100%',
|
|
233
|
+
parseHTML: (element) => normalizeWidthAttr(element.getAttribute('width') || element.style.width) || '100%',
|
|
234
|
+
renderHTML: (attributes) => {
|
|
235
|
+
const width = normalizeWidthAttr(attributes.width) || '100%';
|
|
236
|
+
return { width };
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
height: {
|
|
240
|
+
default: '600',
|
|
241
|
+
parseHTML: (element) => normalizeNumericAttr(element.getAttribute('height') || element.style.height) || '600',
|
|
242
|
+
renderHTML: (attributes) => {
|
|
243
|
+
const height = normalizeNumericAttr(attributes.height) || '600';
|
|
244
|
+
return { height };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
types: attributeTypes,
|
|
251
|
+
attributes: {
|
|
252
|
+
resizeHandler: {
|
|
253
|
+
default: false,
|
|
254
|
+
parseHTML: (element) => hasResizeHandler(element.getAttribute('data-resize-handler') ||
|
|
255
|
+
element.getAttribute('resize-handler')),
|
|
256
|
+
renderHTML: (attributes) => hasResizeHandler(attributes.resizeHandler)
|
|
257
|
+
? { 'data-resize-handler': 'true' }
|
|
258
|
+
: {}
|
|
259
|
+
},
|
|
260
|
+
resizeTarget: {
|
|
261
|
+
default: null,
|
|
262
|
+
parseHTML: (element) => normalizeStringAttr(element.getAttribute('data-resize-target')),
|
|
263
|
+
renderHTML: (attributes) => {
|
|
264
|
+
const resizeTarget = normalizeStringAttr(attributes.resizeTarget);
|
|
265
|
+
return resizeTarget ? { 'data-resize-target': resizeTarget } : {};
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
minHeight: {
|
|
269
|
+
default: null,
|
|
270
|
+
parseHTML: (element) => normalizeNumericAttr(element.getAttribute('data-resize-min-height')),
|
|
271
|
+
renderHTML: (attributes) => {
|
|
272
|
+
const minHeight = normalizeNumericAttr(attributes.minHeight);
|
|
273
|
+
return minHeight ? { 'data-resize-min-height': minHeight } : {};
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
maxHeight: {
|
|
277
|
+
default: null,
|
|
278
|
+
parseHTML: (element) => normalizeNumericAttr(element.getAttribute('data-resize-max-height')),
|
|
279
|
+
renderHTML: (attributes) => {
|
|
280
|
+
const maxHeight = normalizeNumericAttr(attributes.maxHeight);
|
|
281
|
+
return maxHeight ? { 'data-resize-max-height': maxHeight } : {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
},
|
|
288
|
+
addProseMirrorPlugins() {
|
|
289
|
+
let removeDragListeners = null;
|
|
290
|
+
return [
|
|
291
|
+
new Plugin({
|
|
292
|
+
key: pluginKey,
|
|
293
|
+
appendTransaction: (transactions, _oldState, newState) => {
|
|
294
|
+
if (!transactions.some((tr) => tr.docChanged))
|
|
295
|
+
return null;
|
|
296
|
+
if (newState.selection instanceof NodeSelection)
|
|
297
|
+
return null;
|
|
298
|
+
const nodeBefore = newState.selection.$from.nodeBefore;
|
|
299
|
+
if (!nodeBefore)
|
|
300
|
+
return null;
|
|
301
|
+
const resizeMeta = resolveResizeMeta(nodeBefore);
|
|
302
|
+
if (!resizeMeta)
|
|
303
|
+
return null;
|
|
304
|
+
const typeName = nodeBefore.type.name;
|
|
305
|
+
const nodePos = newState.selection.from - nodeBefore.nodeSize;
|
|
306
|
+
if (nodePos < 0)
|
|
307
|
+
return null;
|
|
308
|
+
if (newState.doc.nodeAt(nodePos)?.type.name !== typeName)
|
|
309
|
+
return null;
|
|
310
|
+
const nodeSelection = tryCreateNodeSelection(newState.doc, nodePos);
|
|
311
|
+
if (!nodeSelection)
|
|
312
|
+
return null;
|
|
313
|
+
return newState.tr.setSelection(nodeSelection);
|
|
314
|
+
},
|
|
315
|
+
props: {
|
|
316
|
+
decorations: (state) => {
|
|
317
|
+
if (!this.editor.isEditable)
|
|
318
|
+
return DecorationSet.empty;
|
|
319
|
+
const showHandleAlways = this.options.showHandleAlways !== false;
|
|
320
|
+
const showHandleOnActive = this.options.showHandleOnActive !== false;
|
|
321
|
+
if (!showHandleAlways && !showHandleOnActive)
|
|
322
|
+
return DecorationSet.empty;
|
|
323
|
+
const decorations = [];
|
|
324
|
+
const handled = new Set();
|
|
325
|
+
if (showHandleAlways) {
|
|
326
|
+
state.doc.descendants((node, pos) => {
|
|
327
|
+
const resizeMeta = resolveResizeMeta(node);
|
|
328
|
+
if (!resizeMeta || resizeMeta.kind === 'image' || node.isInline)
|
|
329
|
+
return;
|
|
330
|
+
decorations.push(createResizeHandleDecoration(pos, pos + node.nodeSize, resizeMeta));
|
|
331
|
+
handled.add(pos);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (showHandleOnActive && state.selection instanceof NodeSelection) {
|
|
335
|
+
const pos = state.selection.from;
|
|
336
|
+
const resizeMeta = resolveResizeMeta(state.selection.node);
|
|
337
|
+
if (resizeMeta && !handled.has(pos)) {
|
|
338
|
+
decorations.push(createResizeHandleDecoration(pos, pos + state.selection.node.nodeSize, resizeMeta));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!decorations.length)
|
|
342
|
+
return DecorationSet.empty;
|
|
343
|
+
return DecorationSet.create(state.doc, decorations);
|
|
344
|
+
},
|
|
345
|
+
handleDOMEvents: {
|
|
346
|
+
mousedown: (view, event) => {
|
|
347
|
+
if (!this.editor.isEditable)
|
|
348
|
+
return false;
|
|
349
|
+
if (!(event.target instanceof HTMLElement))
|
|
350
|
+
return false;
|
|
351
|
+
const handle = event.target.closest('.tiptap-media-resize-handle');
|
|
352
|
+
if (!handle)
|
|
353
|
+
return false;
|
|
354
|
+
const pos = Number.parseInt(handle.dataset.resizePos || '', 10);
|
|
355
|
+
if (!Number.isFinite(pos))
|
|
356
|
+
return false;
|
|
357
|
+
const node = view.state.doc.nodeAt(pos);
|
|
358
|
+
if (!node)
|
|
359
|
+
return false;
|
|
360
|
+
const resizeMeta = resolveResizeMeta(node);
|
|
361
|
+
if (!resizeMeta)
|
|
362
|
+
return false;
|
|
363
|
+
const resizeKind = handle.dataset.resizeKind;
|
|
364
|
+
if (resizeKind && resizeMeta.kind !== resizeKind)
|
|
365
|
+
return false;
|
|
366
|
+
const target = resolveTargetElement(view, pos, resizeMeta, node);
|
|
367
|
+
if (!target)
|
|
368
|
+
return false;
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
const startY = event.clientY;
|
|
372
|
+
const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
|
|
373
|
+
const imageRatio = resizeMeta.kind === 'image' ? resolveImageRatio(node, target) : 1;
|
|
374
|
+
const shouldShowProxy = resizeMeta.kind !== 'image';
|
|
375
|
+
let resizeProxy = null;
|
|
376
|
+
let restoreTarget = null;
|
|
377
|
+
let frame = 0;
|
|
378
|
+
let pendingHeight = startHeight;
|
|
379
|
+
if (shouldShowProxy && target.parentElement) {
|
|
380
|
+
const targetElement = target;
|
|
381
|
+
const originalDisplay = targetElement.style.display;
|
|
382
|
+
resizeProxy = document.createElement('div');
|
|
383
|
+
resizeProxy.className = 'tiptap-media-resize-proxy';
|
|
384
|
+
resizeProxy.style.height = `${Math.round(startHeight)}px`;
|
|
385
|
+
target.parentElement.insertBefore(resizeProxy, targetElement);
|
|
386
|
+
targetElement.style.display = 'none';
|
|
387
|
+
restoreTarget = () => {
|
|
388
|
+
targetElement.style.display = originalDisplay;
|
|
389
|
+
resizeProxy?.remove();
|
|
390
|
+
resizeProxy = null;
|
|
391
|
+
restoreTarget = null;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const dispatchHeight = (height) => {
|
|
395
|
+
const current = view.state.doc.nodeAt(pos);
|
|
396
|
+
if (!current || current.type.name !== resizeMeta.typeName)
|
|
397
|
+
return;
|
|
398
|
+
const currentMeta = resolveResizeMeta(current);
|
|
399
|
+
if (!currentMeta)
|
|
400
|
+
return;
|
|
401
|
+
const nextAttrs = buildResizeAttrs(currentMeta.kind, current, height, imageRatio);
|
|
402
|
+
const nextWidth = 'width' in nextAttrs ? nextAttrs.width : current.attrs.width;
|
|
403
|
+
if (nextAttrs.height === current.attrs.height && nextWidth === current.attrs.width)
|
|
404
|
+
return;
|
|
405
|
+
const tr = view.state.tr.setNodeMarkup(pos, current.type, nextAttrs);
|
|
406
|
+
view.dispatch(tr);
|
|
407
|
+
};
|
|
408
|
+
const previousCursor = document.body.style.cursor;
|
|
409
|
+
const previousSelect = document.body.style.userSelect;
|
|
410
|
+
document.body.style.cursor = 'ns-resize';
|
|
411
|
+
document.body.style.userSelect = 'none';
|
|
412
|
+
const onMove = (moveEvent) => {
|
|
413
|
+
const nextHeight = clamp(startHeight + moveEvent.clientY - startY, resizeMeta.minHeight, resizeMeta.maxHeight);
|
|
414
|
+
pendingHeight = nextHeight;
|
|
415
|
+
if (resizeProxy)
|
|
416
|
+
resizeProxy.style.height = `${Math.round(nextHeight)}px`;
|
|
417
|
+
if (shouldShowProxy)
|
|
418
|
+
return;
|
|
419
|
+
if (frame)
|
|
420
|
+
cancelAnimationFrame(frame);
|
|
421
|
+
frame = requestAnimationFrame(() => dispatchHeight(nextHeight));
|
|
422
|
+
};
|
|
423
|
+
const cleanup = () => {
|
|
424
|
+
if (frame)
|
|
425
|
+
cancelAnimationFrame(frame);
|
|
426
|
+
window.removeEventListener('mousemove', onMove);
|
|
427
|
+
window.removeEventListener('mouseup', onUp);
|
|
428
|
+
document.body.style.cursor = previousCursor;
|
|
429
|
+
document.body.style.userSelect = previousSelect;
|
|
430
|
+
restoreTarget?.();
|
|
431
|
+
removeDragListeners = null;
|
|
432
|
+
};
|
|
433
|
+
const onUp = () => {
|
|
434
|
+
if (shouldShowProxy)
|
|
435
|
+
dispatchHeight(pendingHeight);
|
|
436
|
+
cleanup();
|
|
437
|
+
};
|
|
438
|
+
removeDragListeners?.();
|
|
439
|
+
window.addEventListener('mousemove', onMove);
|
|
440
|
+
window.addEventListener('mouseup', onUp);
|
|
441
|
+
removeDragListeners = cleanup;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
view: () => ({
|
|
447
|
+
destroy: () => {
|
|
448
|
+
removeDragListeners?.();
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
});
|