@limetech/lime-elements 39.40.0 → 39.42.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/CHANGELOG.md +20 -0
- package/dist/cjs/{image-resize-FAwzj92q.js → image-resize-BWRGvgkM.js} +42 -2
- package/dist/cjs/index.cjs.js +3 -2
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-action-bar_3.cjs.entry.js +1 -1
- package/dist/cjs/limel-ai-avatar.cjs.entry.js +1 -1
- package/dist/cjs/limel-callout.cjs.entry.js +1 -1
- package/dist/cjs/limel-chart.cjs.entry.js +1 -1
- package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
- package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
- package/dist/cjs/limel-code-diff.cjs.entry.js +1 -1
- package/dist/cjs/limel-code-editor.cjs.entry.js +1 -1
- package/dist/cjs/limel-collapsible-section.cjs.entry.js +1 -1
- package/dist/cjs/limel-drag-handle.cjs.entry.js +1 -1
- package/dist/cjs/limel-email-viewer.cjs.entry.js +1 -1
- package/dist/cjs/limel-file-viewer.cjs.entry.js +1 -1
- package/dist/cjs/limel-file.cjs.entry.js +84 -18
- package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +1 -1
- package/dist/cjs/limel-list-item.cjs.entry.js +1 -1
- package/dist/cjs/limel-picker.cjs.entry.js +1 -1
- package/dist/cjs/limel-profile-picture.cjs.entry.js +2 -2
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +440 -212
- package/dist/cjs/limel-snackbar.cjs.entry.js +1 -1
- package/dist/cjs/limel-table.cjs.entry.js +1 -1
- package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
- package/dist/cjs/limel-text-editor.cjs.entry.js +2 -2
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{translations-VEsa81QY.js → translations-Bnw00bkf.js} +8 -0
- package/dist/cjs/{types-Bu6wcdI0.js → types-C7gNcwzE.js} +12 -0
- package/dist/collection/components/file/file.js +119 -18
- package/dist/collection/components/profile-picture/profile-picture.js +1 -1
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/inserter.js +101 -30
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/node.js +160 -54
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/view.js +6 -1
- package/dist/collection/components/text-editor/prosemirror-adapter/prosemirror-adapter.js +51 -5
- package/dist/collection/components/text-editor/text-editor.js +33 -3
- package/dist/collection/components/text-editor/text-editor.types.js +10 -1
- package/dist/collection/components/text-editor/utils/content-type-converter.js +30 -1
- package/dist/collection/components/text-editor/utils/html-converter.js +4 -2
- package/dist/collection/components/text-editor/utils/markdown-converter.js +9 -5
- package/dist/collection/translations/da.js +1 -0
- package/dist/collection/translations/de.js +1 -0
- package/dist/collection/translations/en.js +1 -0
- package/dist/collection/translations/fi.js +1 -0
- package/dist/collection/translations/fr.js +1 -0
- package/dist/collection/translations/nl.js +1 -0
- package/dist/collection/translations/no.js +1 -0
- package/dist/collection/translations/sv.js +1 -0
- package/dist/collection/util/image-resize.js +42 -2
- package/dist/esm/{image-resize-DPcww8rE.js → image-resize-65ma7B85.js} +42 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-action-bar_3.entry.js +1 -1
- package/dist/esm/limel-ai-avatar.entry.js +1 -1
- package/dist/esm/limel-callout.entry.js +1 -1
- package/dist/esm/limel-chart.entry.js +1 -1
- package/dist/esm/limel-chip-set.entry.js +1 -1
- package/dist/esm/limel-chip_2.entry.js +1 -1
- package/dist/esm/limel-code-diff.entry.js +1 -1
- package/dist/esm/limel-code-editor.entry.js +1 -1
- package/dist/esm/limel-collapsible-section.entry.js +1 -1
- package/dist/esm/limel-drag-handle.entry.js +1 -1
- package/dist/esm/limel-email-viewer.entry.js +1 -1
- package/dist/esm/limel-file-viewer.entry.js +1 -1
- package/dist/esm/limel-file.entry.js +84 -18
- package/dist/esm/limel-flatpickr-adapter.entry.js +1 -1
- package/dist/esm/limel-list-item.entry.js +1 -1
- package/dist/esm/limel-picker.entry.js +1 -1
- package/dist/esm/limel-profile-picture.entry.js +2 -2
- package/dist/esm/limel-prosemirror-adapter.entry.js +440 -212
- package/dist/esm/limel-snackbar.entry.js +1 -1
- package/dist/esm/limel-table.entry.js +1 -1
- package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
- package/dist/esm/limel-text-editor.entry.js +2 -2
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{translations-EPnIW0K5.js → translations-Cb9R_zmF.js} +8 -0
- package/dist/esm/{types-BWYo6dLf.js → types-C_Yu8dOg.js} +12 -1
- package/dist/lime-elements/index.esm.js +1 -1
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/{p-068e7087.entry.js → p-19c81ded.entry.js} +2 -2
- package/dist/lime-elements/p-1cc3b516.entry.js +1 -0
- package/dist/lime-elements/{p-ca0a8a5a.entry.js → p-1e3cdfa1.entry.js} +1 -1
- package/dist/lime-elements/p-1f35fc44.entry.js +1 -0
- package/dist/lime-elements/{p-2531a103.entry.js → p-2284caa7.entry.js} +1 -1
- package/dist/lime-elements/{p-d8cb7d42.entry.js → p-2f223e46.entry.js} +1 -1
- package/dist/lime-elements/{p-a3a2aafe.entry.js → p-4a09fb2e.entry.js} +1 -1
- package/dist/lime-elements/{p-f197036b.entry.js → p-552d0733.entry.js} +1 -1
- package/dist/lime-elements/p-65ma7B85.js +1 -0
- package/dist/lime-elements/{p-fd78537c.entry.js → p-6fbf20c6.entry.js} +1 -1
- package/dist/lime-elements/{p-5571f0de.entry.js → p-758939be.entry.js} +1 -1
- package/dist/lime-elements/{p-19347686.entry.js → p-7f7e2180.entry.js} +1 -1
- package/dist/lime-elements/{p-2a871a95.entry.js → p-89f9bd6c.entry.js} +1 -1
- package/dist/lime-elements/{p-79caf3f6.entry.js → p-9ae7d11b.entry.js} +1 -1
- package/dist/lime-elements/p-C_Yu8dOg.js +1 -0
- package/dist/lime-elements/p-Cb9R_zmF.js +1 -0
- package/dist/lime-elements/{p-6edf13b1.entry.js → p-a089b580.entry.js} +1 -1
- package/dist/lime-elements/{p-04b4e903.entry.js → p-aa080a8f.entry.js} +1 -1
- package/dist/lime-elements/{p-54961e0e.entry.js → p-c1ceade7.entry.js} +1 -1
- package/dist/lime-elements/{p-e08f770e.entry.js → p-c8116a01.entry.js} +1 -1
- package/dist/lime-elements/{p-36ccc68e.entry.js → p-c8982eef.entry.js} +1 -1
- package/dist/lime-elements/{p-d98168a1.entry.js → p-ca0b86ee.entry.js} +1 -1
- package/dist/lime-elements/{p-1525274d.entry.js → p-d7bb4310.entry.js} +1 -1
- package/dist/lime-elements/p-d7f75a6d.entry.js +1 -0
- package/dist/lime-elements/{p-bb201b0d.entry.js → p-e545c9e8.entry.js} +1 -1
- package/dist/types/components/file/file.d.ts +20 -0
- package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/inserter.d.ts +2 -2
- package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/node.d.ts +41 -19
- package/dist/types/components/text-editor/prosemirror-adapter/prosemirror-adapter.d.ts +13 -1
- package/dist/types/components/text-editor/text-editor.d.ts +10 -2
- package/dist/types/components/text-editor/text-editor.types.d.ts +52 -0
- package/dist/types/components/text-editor/utils/content-type-converter.d.ts +12 -0
- package/dist/types/components/text-editor/utils/html-converter.d.ts +3 -1
- package/dist/types/components/text-editor/utils/markdown-converter.d.ts +3 -1
- package/dist/types/components.d.ts +50 -8
- package/dist/types/translations/da.d.ts +1 -0
- package/dist/types/translations/de.d.ts +1 -0
- package/dist/types/translations/en.d.ts +1 -0
- package/dist/types/translations/fi.d.ts +1 -0
- package/dist/types/translations/fr.d.ts +1 -0
- package/dist/types/translations/nl.d.ts +1 -0
- package/dist/types/translations/no.d.ts +1 -0
- package/dist/types/translations/sv.d.ts +1 -0
- package/dist/types/util/image-resize.d.ts +12 -4
- package/package.json +1 -1
- package/dist/lime-elements/p-366419ee.entry.js +0 -1
- package/dist/lime-elements/p-5cf7464f.entry.js +0 -1
- package/dist/lime-elements/p-BWYo6dLf.js +0 -1
- package/dist/lime-elements/p-DPcww8rE.js +0 -1
- package/dist/lime-elements/p-EPnIW0K5.js +0 -1
- package/dist/lime-elements/p-b7d734d2.entry.js +0 -1
package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/inserter.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
2
|
import { createFileInfo } from "../../../../../util/files";
|
|
3
|
+
import { isInlineImageTag, } from "../../../text-editor.types";
|
|
3
4
|
import { Node, Slice, Fragment } from "prosemirror-model";
|
|
4
5
|
export const pluginKey = new PluginKey('imageInserterPlugin');
|
|
5
|
-
export const createImageInserterPlugin = (imagePastedCallback) => {
|
|
6
|
+
export const createImageInserterPlugin = (imagePastedCallback, inlineImages) => {
|
|
6
7
|
return new Plugin({
|
|
7
8
|
key: pluginKey,
|
|
8
9
|
props: {
|
|
9
10
|
handlePaste: (view, event, slice) => {
|
|
10
|
-
return processPasteEvent(view, event, slice);
|
|
11
|
+
return processPasteEvent(view, event, slice, inlineImages);
|
|
11
12
|
},
|
|
12
13
|
handleDOMEvents: {
|
|
13
14
|
imagePasted: (_, event) => {
|
|
@@ -17,6 +18,83 @@ export const createImageInserterPlugin = (imagePastedCallback) => {
|
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Runs the inline-image upload lifecycle: show a thumbnail, run the upload,
|
|
23
|
+
* then replace it with the resizable image (carrying the file id) or a failed
|
|
24
|
+
* state.
|
|
25
|
+
* @param view
|
|
26
|
+
* @param file
|
|
27
|
+
* @param base64Data
|
|
28
|
+
* @param fileInfo
|
|
29
|
+
* @param inlineImages
|
|
30
|
+
*/
|
|
31
|
+
const runInlineImageUpload = async (view, file, base64Data, fileInfo, inlineImages) => {
|
|
32
|
+
const upload = inlineImages.upload;
|
|
33
|
+
if (!upload) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const inserter = imageInserterFactory(view, base64Data, fileInfo);
|
|
37
|
+
inserter.insertThumbnail();
|
|
38
|
+
try {
|
|
39
|
+
const uploadResult = await upload(file);
|
|
40
|
+
replaceThumbnailWithInlineImage(view, fileInfo, uploadResult, inlineImages);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error('Inline image upload failed', fileInfo.filename, error);
|
|
44
|
+
inserter.insertFailedThumbnail();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Walks the document for the image node correlated with `fileInfoId`, replaces
|
|
49
|
+
* it with a freshly built node, and dispatches the change. The transaction is
|
|
50
|
+
* only dispatched when a match is found.
|
|
51
|
+
* @param view
|
|
52
|
+
* @param fileInfoId
|
|
53
|
+
* @param buildNode - builds the replacement node from the matched node.
|
|
54
|
+
* @returns whether a matching node was found and replaced.
|
|
55
|
+
*/
|
|
56
|
+
const replaceImageNodeByFileInfoId = (view, fileInfoId, buildNode) => {
|
|
57
|
+
const { state, dispatch } = view;
|
|
58
|
+
const tr = state.tr;
|
|
59
|
+
let found = false;
|
|
60
|
+
state.doc.descendants((node, pos) => {
|
|
61
|
+
if (found) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (node.attrs.fileInfoId === fileInfoId) {
|
|
65
|
+
tr.replaceWith(pos, pos + node.nodeSize, buildNode(node));
|
|
66
|
+
found = true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (found) {
|
|
71
|
+
dispatch(tr);
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
};
|
|
75
|
+
const replaceThumbnailWithInlineImage = (view, fileInfo, uploadResult, inlineImages) => {
|
|
76
|
+
const { schema } = view.state;
|
|
77
|
+
// Tag shape: the upload result is a stored id resolved to a src and
|
|
78
|
+
// persisted as the id microformat. Src shape: the result is the src itself.
|
|
79
|
+
const tag = isInlineImageTag(inlineImages) ? inlineImages : undefined;
|
|
80
|
+
const replaced = replaceImageNodeByFileInfoId(view, fileInfo.id, () => {
|
|
81
|
+
var _a;
|
|
82
|
+
return schema.nodes.image.create({
|
|
83
|
+
src: tag ? tag.getUrl(uploadResult) : uploadResult,
|
|
84
|
+
alt: (_a = fileInfo.filename) !== null && _a !== void 0 ? _a : 'file',
|
|
85
|
+
imageId: tag ? uploadResult : '',
|
|
86
|
+
fileInfoId: fileInfo.id,
|
|
87
|
+
state: 'success',
|
|
88
|
+
maxWidth: '100%',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
if (!replaced) {
|
|
92
|
+
// The thumbnail was removed (undo/delete) while the upload was in
|
|
93
|
+
// flight, so the successfully stored file now has nowhere to land and
|
|
94
|
+
// is orphaned in the backend.
|
|
95
|
+
console.warn('Inline image uploaded but its thumbnail is gone; the stored file is now orphaned', { uploadResult, filename: fileInfo.filename });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
20
98
|
export const imageInserterFactory = (view, base64Data, fileInfo) => {
|
|
21
99
|
return {
|
|
22
100
|
fileInfo: fileInfo,
|
|
@@ -34,32 +112,12 @@ const createThumbnailInserter = (view, base64Data, fileInfo) => () => {
|
|
|
34
112
|
dispatch(transaction);
|
|
35
113
|
};
|
|
36
114
|
const createImageInserter = (view, fileInfo) => (src) => {
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
const tr = state.tr;
|
|
40
|
-
state.doc.descendants((node, pos) => {
|
|
41
|
-
if (node.attrs.fileInfoId === fileInfo.id) {
|
|
42
|
-
const imageNodeAttrs = createImageNodeAttrs(src !== null && src !== void 0 ? src : node.attrs.src, fileInfo, 'success');
|
|
43
|
-
const imageNode = schema.nodes.image.create(imageNodeAttrs);
|
|
44
|
-
tr.replaceWith(pos, pos + node.nodeSize, imageNode);
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
dispatch(tr);
|
|
115
|
+
const { schema } = view.state;
|
|
116
|
+
replaceImageNodeByFileInfoId(view, fileInfo.id, (matched) => schema.nodes.image.create(createImageNodeAttrs(src !== null && src !== void 0 ? src : matched.attrs.src, fileInfo, 'success')));
|
|
49
117
|
};
|
|
50
118
|
const createFailedThumbnailInserter = (view, fileInfo) => () => {
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
const tr = state.tr;
|
|
54
|
-
state.doc.descendants((node, pos) => {
|
|
55
|
-
if (node.attrs.fileInfoId === fileInfo.id) {
|
|
56
|
-
const imageNodeAttrs = createImageNodeAttrs(node.attrs.src, fileInfo, 'failed');
|
|
57
|
-
const errorPlaceholderNode = schema.nodes.image.create(imageNodeAttrs);
|
|
58
|
-
tr.replaceWith(pos, pos + node.nodeSize, errorPlaceholderNode);
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
dispatch(tr);
|
|
119
|
+
const { schema } = view.state;
|
|
120
|
+
replaceImageNodeByFileInfoId(view, fileInfo.id, (matched) => schema.nodes.image.create(createImageNodeAttrs(matched.attrs.src, fileInfo, 'failed')));
|
|
63
121
|
};
|
|
64
122
|
function createImageNodeAttrs(src, fileInfo, state) {
|
|
65
123
|
return {
|
|
@@ -129,14 +187,15 @@ const filterImageNodes = (fragment) => {
|
|
|
129
187
|
* @param view - The ProseMirror editor view.
|
|
130
188
|
* @param event - The paste event.
|
|
131
189
|
* @param slice
|
|
190
|
+
* @param inlineImages
|
|
132
191
|
* @returns A boolean; True if an image file was pasted to prevent default paste behavior, otherwise false.
|
|
133
192
|
*/
|
|
134
|
-
const processPasteEvent = (view, event, slice) => {
|
|
193
|
+
const processPasteEvent = (view, event, slice, inlineImages) => {
|
|
135
194
|
const clipboardData = event.clipboardData;
|
|
136
195
|
if (!clipboardData) {
|
|
137
196
|
return false;
|
|
138
197
|
}
|
|
139
|
-
const isImageFilePasted = handlePastedImages(view, clipboardData);
|
|
198
|
+
const isImageFilePasted = handlePastedImages(view, clipboardData, inlineImages);
|
|
140
199
|
const filteredSlice = new Slice(filterImageNodes(slice.content), slice.openStart, slice.openEnd);
|
|
141
200
|
if (filteredSlice.content.childCount < slice.content.childCount) {
|
|
142
201
|
const { state, dispatch } = view;
|
|
@@ -151,9 +210,10 @@ const processPasteEvent = (view, event, slice) => {
|
|
|
151
210
|
*
|
|
152
211
|
* @param view - The ProseMirror editor view
|
|
153
212
|
* @param clipboardData - The clipboard data transfer object containing potential image files
|
|
213
|
+
* @param inlineImages
|
|
154
214
|
* @returns True if at least one valid image file was found and processed, false otherwise
|
|
155
215
|
*/
|
|
156
|
-
function handlePastedImages(view, clipboardData) {
|
|
216
|
+
function handlePastedImages(view, clipboardData, inlineImages) {
|
|
157
217
|
let isImageFilePasted = false;
|
|
158
218
|
const files = [...(clipboardData.files || [])];
|
|
159
219
|
for (const file of files) {
|
|
@@ -161,8 +221,19 @@ function handlePastedImages(view, clipboardData) {
|
|
|
161
221
|
isImageFilePasted = true;
|
|
162
222
|
const reader = new FileReader();
|
|
163
223
|
reader.onloadend = () => {
|
|
224
|
+
const base64Data = reader.result;
|
|
225
|
+
const fileInfo = createFileInfo(file);
|
|
226
|
+
if (inlineImages) {
|
|
227
|
+
// Once inline images are configured they own the paste
|
|
228
|
+
// lifecycle; never fall back to the legacy imagePasted
|
|
229
|
+
// event. Without an upload handler the paste is a no-op.
|
|
230
|
+
if (inlineImages.upload) {
|
|
231
|
+
runInlineImageUpload(view, file, base64Data, fileInfo, inlineImages);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
164
235
|
view.dom.dispatchEvent(new CustomEvent('imagePasted', {
|
|
165
|
-
detail: imageInserterFactory(view,
|
|
236
|
+
detail: imageInserterFactory(view, base64Data, fileInfo),
|
|
166
237
|
}));
|
|
167
238
|
};
|
|
168
239
|
reader.readAsDataURL(file);
|
|
@@ -1,21 +1,39 @@
|
|
|
1
|
+
import { isInlineImageTag, } from "../../../text-editor.types";
|
|
1
2
|
import translate from "../../../../../global/translations";
|
|
2
3
|
export const imageCache = new Map();
|
|
4
|
+
export const IMAGE_ID_ATTRIBUTE = 'image-id';
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* The attribute names accepted for an inline-image tag. Backs the sanitizer
|
|
7
|
+
* whitelist built in `buildContentWhitelist`, so a stored tag keeps these
|
|
8
|
+
* attributes through parsing. The serializers here enumerate their own output
|
|
9
|
+
* attributes separately and are not driven by this list.
|
|
10
|
+
*/
|
|
11
|
+
export const INLINE_IMAGE_ATTRIBUTES = [
|
|
12
|
+
IMAGE_ID_ATTRIBUTE,
|
|
13
|
+
'width',
|
|
14
|
+
'height',
|
|
15
|
+
'alt',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Builds the `image` NodeSpec, wired for inline-image parsing/serialization
|
|
19
|
+
* when `inlineImages` is configured.
|
|
5
20
|
* @param language
|
|
21
|
+
* @param inlineImages
|
|
6
22
|
*/
|
|
7
|
-
export function getImageNode(language) {
|
|
8
|
-
return { image: createImageNodeSpec(language) };
|
|
23
|
+
export function getImageNode(language, inlineImages) {
|
|
24
|
+
return { image: createImageNodeSpec(language, inlineImages) };
|
|
9
25
|
}
|
|
10
26
|
/**
|
|
11
|
-
*
|
|
27
|
+
* Builds the markdown serializer for the `image` node, emitting the inline-image
|
|
28
|
+
* tag when `inlineImages` is configured and a `<img>` otherwise.
|
|
12
29
|
* @param language
|
|
30
|
+
* @param inlineImages
|
|
13
31
|
*/
|
|
14
|
-
export function getImageNodeMarkdownSerializer(language) {
|
|
15
|
-
return { image: createImageNodeMarkdownSerializer(language) };
|
|
32
|
+
export function getImageNodeMarkdownSerializer(language, inlineImages) {
|
|
33
|
+
return { image: createImageNodeMarkdownSerializer(language, inlineImages) };
|
|
16
34
|
}
|
|
17
35
|
/**
|
|
18
|
-
*
|
|
36
|
+
* Applies the node's stored dimensions to a rendered `<img>` element.
|
|
19
37
|
* @param img
|
|
20
38
|
* @param node
|
|
21
39
|
*/
|
|
@@ -27,8 +45,8 @@ export function applyImageStyles(img, node) {
|
|
|
27
45
|
img.style.maxWidth = node.attrs.maxWidth;
|
|
28
46
|
}
|
|
29
47
|
/**
|
|
30
|
-
* Recursively checks if a ProseMirror node or
|
|
31
|
-
*
|
|
48
|
+
* Recursively checks if a ProseMirror node or any of its descendants is an
|
|
49
|
+
* image node.
|
|
32
50
|
* @param node
|
|
33
51
|
*/
|
|
34
52
|
export function hasImageNode(node) {
|
|
@@ -43,14 +61,20 @@ export function hasImageNode(node) {
|
|
|
43
61
|
}
|
|
44
62
|
return false;
|
|
45
63
|
}
|
|
46
|
-
function createImageNodeMarkdownSerializer(language) {
|
|
64
|
+
function createImageNodeMarkdownSerializer(language, inlineImages) {
|
|
47
65
|
return (markdownSerializerState, node) => {
|
|
48
66
|
const state = node.attrs.state;
|
|
49
67
|
if (!isEditorImageState(state)) {
|
|
50
68
|
return;
|
|
51
69
|
}
|
|
52
70
|
if (state === 'success') {
|
|
53
|
-
const
|
|
71
|
+
const attrs = node.attrs;
|
|
72
|
+
const tag = inlineImages && isInlineImageTag(inlineImages)
|
|
73
|
+
? inlineImages
|
|
74
|
+
: undefined;
|
|
75
|
+
const imageHTML = tag && attrs.imageId
|
|
76
|
+
? getInlineImageHTML(attrs, tag.tagName)
|
|
77
|
+
: getImageHTML(attrs);
|
|
54
78
|
markdownSerializerState.write(imageHTML);
|
|
55
79
|
return;
|
|
56
80
|
}
|
|
@@ -58,6 +82,44 @@ function createImageNodeMarkdownSerializer(language) {
|
|
|
58
82
|
markdownSerializerState.write(statusHTML);
|
|
59
83
|
};
|
|
60
84
|
}
|
|
85
|
+
function escapeAttributeValue(value) {
|
|
86
|
+
return value
|
|
87
|
+
.replaceAll('&', '&')
|
|
88
|
+
.replaceAll('"', '"')
|
|
89
|
+
.replaceAll('<', '<')
|
|
90
|
+
.replaceAll('>', '>');
|
|
91
|
+
}
|
|
92
|
+
// Dimensions are emitted as CSS-unit strings (e.g. `width="300px"`), matching
|
|
93
|
+
// what `applyImageStyles` reads back when the tag is re-parsed inside the
|
|
94
|
+
// editor. The external renderer of this microformat (the `limebb-inline-image`
|
|
95
|
+
// building block) must therefore treat `width`/`height` as CSS lengths, not as
|
|
96
|
+
// bare HTML pixel counts.
|
|
97
|
+
function getInlineImageHTML(attrs, tag) {
|
|
98
|
+
var _a;
|
|
99
|
+
const attributes = [
|
|
100
|
+
`${IMAGE_ID_ATTRIBUTE}="${escapeAttributeValue((_a = attrs.imageId) !== null && _a !== void 0 ? _a : '')}"`,
|
|
101
|
+
attrs.width ? `width="${escapeAttributeValue(attrs.width)}"` : '',
|
|
102
|
+
attrs.height ? `height="${escapeAttributeValue(attrs.height)}"` : '',
|
|
103
|
+
attrs.alt ? `alt="${escapeAttributeValue(attrs.alt)}"` : '',
|
|
104
|
+
].filter(Boolean);
|
|
105
|
+
return `<${tag} ${attributes.join(' ')}></${tag}>`;
|
|
106
|
+
}
|
|
107
|
+
function buildInlineImageDOMAttrs(attrs) {
|
|
108
|
+
var _a;
|
|
109
|
+
const result = {
|
|
110
|
+
[IMAGE_ID_ATTRIBUTE]: (_a = attrs.imageId) !== null && _a !== void 0 ? _a : '',
|
|
111
|
+
};
|
|
112
|
+
if (attrs.alt) {
|
|
113
|
+
result.alt = attrs.alt;
|
|
114
|
+
}
|
|
115
|
+
if (attrs.width) {
|
|
116
|
+
result.width = attrs.width;
|
|
117
|
+
}
|
|
118
|
+
if (attrs.height) {
|
|
119
|
+
result.height = attrs.height;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
61
123
|
function getStatusHTML(state, alt, language) {
|
|
62
124
|
const key = state === 'failed' ? 'failed' : 'loading';
|
|
63
125
|
const text = translate.get(`editor-image-view.${key}`, language, {
|
|
@@ -66,26 +128,23 @@ function getStatusHTML(state, alt, language) {
|
|
|
66
128
|
return `<span>${text}</span>`;
|
|
67
129
|
}
|
|
68
130
|
function getImageHTML(attrs) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
const styleAttribute = style.length > 0 ? ` style="${style.join('')}"` : '';
|
|
86
|
-
return `<img src="${attrs.src}" alt="${attrs.alt}"${styleAttribute} />`;
|
|
131
|
+
const dimensions = [
|
|
132
|
+
['height', attrs.height],
|
|
133
|
+
['width', attrs.width],
|
|
134
|
+
['min-height', attrs.minHeight],
|
|
135
|
+
['min-width', attrs.minWidth],
|
|
136
|
+
['max-width', attrs.maxWidth],
|
|
137
|
+
];
|
|
138
|
+
const style = dimensions
|
|
139
|
+
.filter(([, value]) => value)
|
|
140
|
+
.map(([property, value]) => `${property}: ${value};`)
|
|
141
|
+
.join('');
|
|
142
|
+
const styleAttribute = style
|
|
143
|
+
? ` style="${escapeAttributeValue(style)}"`
|
|
144
|
+
: '';
|
|
145
|
+
return `<img src="${escapeAttributeValue(attrs.src)}" alt="${escapeAttributeValue(attrs.alt)}"${styleAttribute} />`;
|
|
87
146
|
}
|
|
88
|
-
function createImageNodeSpec(language) {
|
|
147
|
+
function createImageNodeSpec(language, inlineImages) {
|
|
89
148
|
return {
|
|
90
149
|
group: 'inline',
|
|
91
150
|
inline: true,
|
|
@@ -93,6 +152,7 @@ function createImageNodeSpec(language) {
|
|
|
93
152
|
src: { default: '' },
|
|
94
153
|
alt: { default: '' },
|
|
95
154
|
fileInfoId: { default: '' },
|
|
155
|
+
imageId: { default: '' },
|
|
96
156
|
height: { default: '' },
|
|
97
157
|
width: { default: '' },
|
|
98
158
|
minHeight: { default: '' },
|
|
@@ -100,33 +160,79 @@ function createImageNodeSpec(language) {
|
|
|
100
160
|
maxWidth: { default: '100%' },
|
|
101
161
|
state: { default: 'success' },
|
|
102
162
|
},
|
|
103
|
-
toDOM: (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
163
|
+
toDOM: createImageToDOM(language, inlineImages),
|
|
164
|
+
parseDOM: createImageParseDOM(inlineImages),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function createImageToDOM(language, inlineImages) {
|
|
168
|
+
return (node) => {
|
|
169
|
+
if (!isEditorImageState(node.attrs.state)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (node.attrs.state !== 'success') {
|
|
110
173
|
return createStatusSpanForState(node.attrs.state, node, language);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
],
|
|
174
|
+
}
|
|
175
|
+
// Emit the inline-image tag for HTML serialization and clipboard;
|
|
176
|
+
// on-screen rendering comes from the NodeView, not this.
|
|
177
|
+
const tag = inlineImages && isInlineImageTag(inlineImages)
|
|
178
|
+
? inlineImages
|
|
179
|
+
: undefined;
|
|
180
|
+
if (tag && node.attrs.imageId) {
|
|
181
|
+
return [
|
|
182
|
+
tag.tagName,
|
|
183
|
+
buildInlineImageDOMAttrs(node.attrs),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
return getOrCreateImageElement(node.attrs.fileInfoId, node);
|
|
128
187
|
};
|
|
129
188
|
}
|
|
189
|
+
function createImageParseDOM(inlineImages) {
|
|
190
|
+
const rules = [];
|
|
191
|
+
const tag = inlineImages && isInlineImageTag(inlineImages)
|
|
192
|
+
? inlineImages
|
|
193
|
+
: undefined;
|
|
194
|
+
if (tag) {
|
|
195
|
+
rules.push({
|
|
196
|
+
tag: tag.tagName,
|
|
197
|
+
getAttrs: (dom) => {
|
|
198
|
+
const imageId = dom.getAttribute(IMAGE_ID_ATTRIBUTE);
|
|
199
|
+
// Skip malformed tags without an id; parsing them would
|
|
200
|
+
// produce a broken image from an empty getUrl('').
|
|
201
|
+
if (!imageId) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
src: tag.getUrl(imageId),
|
|
206
|
+
// Preserve an absent alt as empty so an id-only tag
|
|
207
|
+
// serializes back to the same markup instead of gaining
|
|
208
|
+
// a synthetic alt, keeping the round-trip stable.
|
|
209
|
+
alt: dom.getAttribute('alt') || '',
|
|
210
|
+
imageId: imageId,
|
|
211
|
+
width: dom.getAttribute('width') || '',
|
|
212
|
+
height: dom.getAttribute('height') || '',
|
|
213
|
+
maxWidth: '100%',
|
|
214
|
+
state: 'success',
|
|
215
|
+
fileInfoId: crypto.randomUUID(),
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
rules.push({
|
|
221
|
+
tag: 'img',
|
|
222
|
+
getAttrs: (dom) => {
|
|
223
|
+
return {
|
|
224
|
+
src: dom.getAttribute('src') || '',
|
|
225
|
+
alt: dom.getAttribute('alt') || 'file',
|
|
226
|
+
width: dom.style.width || '',
|
|
227
|
+
height: dom.style.height || '',
|
|
228
|
+
maxWidth: '100%',
|
|
229
|
+
state: 'success',
|
|
230
|
+
fileInfoId: crypto.randomUUID(),
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
return rules;
|
|
235
|
+
}
|
|
130
236
|
function isEditorImageState(state) {
|
|
131
237
|
return state === 'loading' || state === 'failed' || state === 'success';
|
|
132
238
|
}
|
|
@@ -119,7 +119,12 @@ class ImageView {
|
|
|
119
119
|
this.img.alt = node.attrs.alt;
|
|
120
120
|
applyImageStyles(this.img, node);
|
|
121
121
|
this.img.addEventListener('load', () => {
|
|
122
|
-
|
|
122
|
+
// Only measure a freshly inserted image. Re-measuring one that
|
|
123
|
+
// already has a width would rewrite it against the current
|
|
124
|
+
// container size and emit a spurious change event on every load.
|
|
125
|
+
if (!this.node.attrs.width) {
|
|
126
|
+
this.persistDimensions();
|
|
127
|
+
}
|
|
123
128
|
});
|
|
124
129
|
this.dom.append(this.img);
|
|
125
130
|
this.transitionBetweenStates();
|
|
@@ -24,6 +24,7 @@ import { createMenuStateTrackingPlugin } from "./plugins/menu-state-tracking-plu
|
|
|
24
24
|
import { createActionBarInteractionPlugin } from "./plugins/menu-action-interaction-plugin";
|
|
25
25
|
import { createNodeSpec } from "../utils/plugin-factory";
|
|
26
26
|
import { createTriggerPlugin } from "./plugins/trigger/factory";
|
|
27
|
+
import { isInlineImageTag, } from "../text-editor.types";
|
|
27
28
|
import { getTableNodes, getTableEditingPlugins } from "./plugins/table-plugin";
|
|
28
29
|
import { getImageNode, imageCache } from "./plugins/image/node";
|
|
29
30
|
import { getMetadataFromDoc, hasMetadataChanged, } from "../utils/metadata-utils";
|
|
@@ -270,6 +271,7 @@ export class ProsemirrorAdapter {
|
|
|
270
271
|
}
|
|
271
272
|
componentWillLoad() {
|
|
272
273
|
this.getActionBarItems();
|
|
274
|
+
this.validatedInlineImages = this.validateInlineImages();
|
|
273
275
|
this.setupContentConverter();
|
|
274
276
|
}
|
|
275
277
|
componentDidLoad() {
|
|
@@ -295,7 +297,7 @@ export class ProsemirrorAdapter {
|
|
|
295
297
|
(_e = this.view) === null || _e === void 0 ? void 0 : _e.destroy();
|
|
296
298
|
}
|
|
297
299
|
render() {
|
|
298
|
-
return (h(Host, { key: '
|
|
300
|
+
return (h(Host, { key: '27949dced1fd61d1a80c42f58cf1699f05acb52e', onFocus: this.handleFocus }, h("div", { key: '75e941459326f0fb8f8d5042becd570e06cd99fa', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
|
|
299
301
|
}
|
|
300
302
|
renderToolbar() {
|
|
301
303
|
if (this.actionBarItems.length === 0 || this.ui === 'no-toolbar') {
|
|
@@ -309,12 +311,26 @@ export class ProsemirrorAdapter {
|
|
|
309
311
|
}
|
|
310
312
|
return (h("limel-portal", { containerId: this.portalId, visible: this.isLinkMenuOpen, openDirection: "top", inheritParentWidth: true, anchor: this.actionBarElement }, h("limel-text-editor-link-menu", { link: this.link, isOpen: this.isLinkMenuOpen, onLinkChange: this.handleLinkChange, onCancel: this.handleCancelLinkMenu, onSave: this.handleSaveLinkMenu })));
|
|
311
313
|
}
|
|
314
|
+
validateInlineImages() {
|
|
315
|
+
const config = this.inlineImages;
|
|
316
|
+
// Require a hyphen: the tag name becomes a sanitizer whitelist key, so a
|
|
317
|
+
// built-in like `img` would hijack its rules and strip `src` from every
|
|
318
|
+
// real image. Custom element names always have a hyphen; built-ins never.
|
|
319
|
+
const isValidTagName = (name) => /^[a-z][a-z0-9._-]*$/.test(name) && name.includes('-');
|
|
320
|
+
if (config &&
|
|
321
|
+
isInlineImageTag(config) &&
|
|
322
|
+
!isValidTagName(config.tagName)) {
|
|
323
|
+
console.error(`inlineImages.tagName "${config.tagName}" is not a valid custom element name; ignoring inlineImages config.`);
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
return config;
|
|
327
|
+
}
|
|
312
328
|
setupContentConverter() {
|
|
313
329
|
if (this.contentType === 'markdown') {
|
|
314
|
-
this.contentConverter = new MarkdownConverter(this.customElements, this.language);
|
|
330
|
+
this.contentConverter = new MarkdownConverter(this.customElements, this.language, this.validatedInlineImages);
|
|
315
331
|
}
|
|
316
332
|
else if (this.contentType === 'html') {
|
|
317
|
-
this.contentConverter = new HTMLConverter(this.customElements);
|
|
333
|
+
this.contentConverter = new HTMLConverter(this.customElements, this.validatedInlineImages);
|
|
318
334
|
}
|
|
319
335
|
else {
|
|
320
336
|
throw new Error(`Unsupported content type: ${this.contentType}. Only 'markdown' and 'html' are supported.`);
|
|
@@ -347,7 +363,7 @@ export class ProsemirrorAdapter {
|
|
|
347
363
|
if (this.contentType === 'html') {
|
|
348
364
|
nodes = nodes.append(getTableNodes());
|
|
349
365
|
}
|
|
350
|
-
nodes = nodes.append(getImageNode(this.language));
|
|
366
|
+
nodes = nodes.append(getImageNode(this.language, this.validatedInlineImages));
|
|
351
367
|
return new Schema({
|
|
352
368
|
nodes: nodes,
|
|
353
369
|
marks: schema.spec.marks.append({
|
|
@@ -375,7 +391,7 @@ export class ProsemirrorAdapter {
|
|
|
375
391
|
keymap(this.menuCommandFactory.buildKeymap()),
|
|
376
392
|
createTriggerPlugin(this.triggerCharacters, this.contentConverter),
|
|
377
393
|
createLinkPlugin(this.handleNewLinkSelection),
|
|
378
|
-
createImageInserterPlugin(this.imagePasted.emit),
|
|
394
|
+
createImageInserterPlugin(this.imagePasted.emit, this.validatedInlineImages),
|
|
379
395
|
createImageViewPlugin(this.language),
|
|
380
396
|
createMenuStateTrackingPlugin(editorMenuTypesArray, this.menuCommandFactory, this.updateActiveActionBarItems),
|
|
381
397
|
createActionBarInteractionPlugin(this.menuCommandFactory),
|
|
@@ -555,6 +571,36 @@ export class ProsemirrorAdapter {
|
|
|
555
571
|
"setter": false,
|
|
556
572
|
"defaultValue": "[]"
|
|
557
573
|
},
|
|
574
|
+
"inlineImages": {
|
|
575
|
+
"type": "unknown",
|
|
576
|
+
"mutable": false,
|
|
577
|
+
"complexType": {
|
|
578
|
+
"original": "InlineImages",
|
|
579
|
+
"resolved": "InlineImageSrc | InlineImageTag",
|
|
580
|
+
"references": {
|
|
581
|
+
"InlineImages": {
|
|
582
|
+
"location": "import",
|
|
583
|
+
"path": "../text-editor.types",
|
|
584
|
+
"id": "src/components/text-editor/text-editor.types.ts::InlineImages",
|
|
585
|
+
"referenceLocation": "InlineImages"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"required": false,
|
|
590
|
+
"optional": true,
|
|
591
|
+
"docs": {
|
|
592
|
+
"tags": [{
|
|
593
|
+
"name": "private",
|
|
594
|
+
"text": undefined
|
|
595
|
+
}, {
|
|
596
|
+
"name": "alpha",
|
|
597
|
+
"text": undefined
|
|
598
|
+
}],
|
|
599
|
+
"text": "Configures inline image support. When set, the editor owns the paste +\nupload lifecycle and persists images either as the configured tag\ncarrying only a file id (`InlineImageTag`), or as a plain `<img src>`\n(`InlineImageSrc`)."
|
|
600
|
+
},
|
|
601
|
+
"getter": false,
|
|
602
|
+
"setter": false
|
|
603
|
+
},
|
|
558
604
|
"triggerCharacters": {
|
|
559
605
|
"type": "unknown",
|
|
560
606
|
"mutable": false,
|
|
@@ -14,8 +14,8 @@ import { createRandomString } from "../../util/random-string";
|
|
|
14
14
|
* @exampleComponent limel-example-text-editor-with-markdown
|
|
15
15
|
* @exampleComponent limel-example-text-editor-with-html
|
|
16
16
|
* @exampleComponent limel-example-text-editor-with-tables
|
|
17
|
-
* @exampleComponent limel-example-text-editor-with-inline-images-file-storage
|
|
18
17
|
* @exampleComponent limel-example-text-editor-with-inline-images-base64
|
|
18
|
+
* @exampleComponent limel-example-text-editor-with-inline-images-custom-tag
|
|
19
19
|
* @exampleComponent limel-example-text-editor-allow-resize
|
|
20
20
|
* @exampleComponent limel-example-text-editor-size
|
|
21
21
|
* @exampleComponent limel-example-text-editor-ui
|
|
@@ -175,13 +175,13 @@ export class TextEditor {
|
|
|
175
175
|
await ((_a = this.adapterElement) === null || _a === void 0 ? void 0 : _a.clear());
|
|
176
176
|
}
|
|
177
177
|
render() {
|
|
178
|
-
return (h(Host, { key: '
|
|
178
|
+
return (h(Host, { key: '14e06e648e7625dd05901740f172c0b93bd1b494' }, h("limel-notched-outline", { key: '81acfc60c0b5a59175c33163e9436d17f27a480a', labelId: this.editorId, label: this.label, required: this.required, invalid: this.invalid, disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasFloatingLabel: true }, this.renderEditor(), this.renderPlaceholder()), this.renderHelperLine()));
|
|
179
179
|
}
|
|
180
180
|
renderEditor() {
|
|
181
181
|
if (this.readonly) {
|
|
182
182
|
return (h("limel-markdown", { slot: "content", value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId }));
|
|
183
183
|
}
|
|
184
|
-
return (h("limel-prosemirror-adapter", { ref: (el) => (this.adapterElement = el), slot: "content", "aria-placeholder": this.placeholder, contentType: this.contentType, onChange: this.handleChange, onImagePasted: this.handleImagePasted, onImageRemoved: this.handleImageRemoved, onMetadataChange: this.handleMetadataChange, customElements: this.customElements, value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId, "aria-disabled": this.disabled, "aria-invalid": this.invalid, "aria-required": this.required, language: this.language, triggerCharacters: this.triggers, disabled: this.disabled, ui: this.ui }));
|
|
184
|
+
return (h("limel-prosemirror-adapter", { ref: (el) => (this.adapterElement = el), slot: "content", "aria-placeholder": this.placeholder, contentType: this.contentType, onChange: this.handleChange, onImagePasted: this.handleImagePasted, onImageRemoved: this.handleImageRemoved, onMetadataChange: this.handleMetadataChange, customElements: this.customElements, inlineImages: this.inlineImages, value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId, "aria-disabled": this.disabled, "aria-invalid": this.invalid, "aria-required": this.required, language: this.language, triggerCharacters: this.triggers, disabled: this.disabled, ui: this.ui }));
|
|
185
185
|
}
|
|
186
186
|
renderPlaceholder() {
|
|
187
187
|
if (!this.placeholder || this.value) {
|
|
@@ -418,6 +418,36 @@ export class TextEditor {
|
|
|
418
418
|
"setter": false,
|
|
419
419
|
"defaultValue": "[]"
|
|
420
420
|
},
|
|
421
|
+
"inlineImages": {
|
|
422
|
+
"type": "unknown",
|
|
423
|
+
"mutable": false,
|
|
424
|
+
"complexType": {
|
|
425
|
+
"original": "InlineImages",
|
|
426
|
+
"resolved": "InlineImageSrc | InlineImageTag",
|
|
427
|
+
"references": {
|
|
428
|
+
"InlineImages": {
|
|
429
|
+
"location": "import",
|
|
430
|
+
"path": "./text-editor.types",
|
|
431
|
+
"id": "src/components/text-editor/text-editor.types.ts::InlineImages",
|
|
432
|
+
"referenceLocation": "InlineImages"
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
"required": false,
|
|
437
|
+
"optional": true,
|
|
438
|
+
"docs": {
|
|
439
|
+
"tags": [{
|
|
440
|
+
"name": "private",
|
|
441
|
+
"text": undefined
|
|
442
|
+
}, {
|
|
443
|
+
"name": "alpha",
|
|
444
|
+
"text": undefined
|
|
445
|
+
}],
|
|
446
|
+
"text": "Configures inline image support: the editor owns the paste lifecycle and\nresize, the consumer owns upload, storage format, and URL resolution."
|
|
447
|
+
},
|
|
448
|
+
"getter": false,
|
|
449
|
+
"setter": false
|
|
450
|
+
},
|
|
421
451
|
"triggers": {
|
|
422
452
|
"type": "unknown",
|
|
423
453
|
"mutable": false,
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Narrows `InlineImages` to its tag shape, exposing `tagName`/`getUrl`.
|
|
3
|
+
*
|
|
4
|
+
* @param config - the inline-images configuration to test
|
|
5
|
+
* @returns whether images are persisted as a custom id-carrying element
|
|
6
|
+
* @alpha
|
|
7
|
+
*/
|
|
8
|
+
export function isInlineImageTag(config) {
|
|
9
|
+
return 'tagName' in config;
|
|
10
|
+
}
|