@limetech/lime-elements 39.39.1 → 39.41.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 +14 -0
- package/dist/cjs/index.cjs.js +2 -1
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-file.cjs.entry.js +15 -5
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +440 -212
- package/dist/cjs/limel-text-editor.cjs.entry.js +2 -2
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{types-Bu6wcdI0.js → types-C7gNcwzE.js} +12 -0
- package/dist/collection/components/file/file.js +16 -5
- 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/esm/index.js +1 -1
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-file.entry.js +15 -5
- package/dist/esm/limel-prosemirror-adapter.entry.js +440 -212
- package/dist/esm/limel-text-editor.entry.js +2 -2
- package/dist/esm/loader.js +1 -1
- 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-1f35fc44.entry.js +1 -0
- package/dist/lime-elements/p-56781dc7.entry.js +1 -0
- package/dist/lime-elements/p-5cf7464f.entry.js +1 -0
- package/dist/lime-elements/p-C_Yu8dOg.js +1 -0
- package/dist/types/components/file/file.d.ts +3 -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 +34 -6
- package/dist/types/global/shared-types/file.types.d.ts +10 -0
- package/package.json +1 -1
- package/dist/lime-elements/p-366419ee.entry.js +0 -1
- package/dist/lime-elements/p-BWYo6dLf.js +0 -1
- package/dist/lime-elements/p-b7d734d2.entry.js +0 -1
- package/dist/lime-elements/p-b97f2f56.entry.js +0 -1
|
@@ -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
|
+
}
|
|
@@ -1 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import { isInlineImageTag } from "../text-editor.types";
|
|
2
|
+
import { INLINE_IMAGE_ATTRIBUTES } from "../prosemirror-adapter/plugins/image/node";
|
|
3
|
+
/**
|
|
4
|
+
* Build the sanitization whitelist for editor content: the consumer's custom
|
|
5
|
+
* elements, plus (when inline images are configured) the inline-image tag so it
|
|
6
|
+
* survives parsing into an image node. Shared by all content converters.
|
|
7
|
+
*
|
|
8
|
+
* @param customNodes
|
|
9
|
+
* @param inlineImages
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
export function buildContentWhitelist(customNodes, inlineImages) {
|
|
13
|
+
if (!inlineImages || !isInlineImageTag(inlineImages)) {
|
|
14
|
+
return customNodes;
|
|
15
|
+
}
|
|
16
|
+
// Guard the boundary independently of the upstream `validateInlineImages`
|
|
17
|
+
// check: the tag name becomes a sanitizer whitelist key, so a built-in like
|
|
18
|
+
// `img` (no hyphen) would hijack its rules and strip `src` from every real
|
|
19
|
+
// image. Custom element names always contain a hyphen; built-ins never do.
|
|
20
|
+
if (!inlineImages.tagName.includes('-')) {
|
|
21
|
+
return customNodes;
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
...customNodes,
|
|
25
|
+
{
|
|
26
|
+
tagName: inlineImages.tagName,
|
|
27
|
+
attributes: [...INLINE_IMAGE_ATTRIBUTES],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildContentWhitelist, } from "./content-type-converter";
|
|
1
2
|
import { sanitizeHTML } from "../../markdown/markdown-parser";
|
|
2
3
|
import { DOMSerializer } from "prosemirror-model";
|
|
3
4
|
import { hasImageNode } from "../prosemirror-adapter/plugins/image/node";
|
|
@@ -6,9 +7,9 @@ import { hasCustomElementNode } from "./has-custom-element-node";
|
|
|
6
7
|
* @private
|
|
7
8
|
*/
|
|
8
9
|
export class HTMLConverter {
|
|
9
|
-
constructor(plugins) {
|
|
10
|
+
constructor(plugins, inlineImages) {
|
|
10
11
|
this.parseAsHTML = (text) => {
|
|
11
|
-
return sanitizeHTML(text, this.customNodes);
|
|
12
|
+
return sanitizeHTML(text, buildContentWhitelist(this.customNodes, this.inlineImages));
|
|
12
13
|
};
|
|
13
14
|
this.serialize = (view) => {
|
|
14
15
|
if (view.state.doc.textContent.trim() === '' &&
|
|
@@ -21,5 +22,6 @@ export class HTMLConverter {
|
|
|
21
22
|
return div.innerHTML;
|
|
22
23
|
};
|
|
23
24
|
this.customNodes = plugins;
|
|
25
|
+
this.inlineImages = inlineImages;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildContentWhitelist, } from "./content-type-converter";
|
|
1
2
|
import { MarkdownSerializer, defaultMarkdownSerializer, } from "prosemirror-markdown";
|
|
2
3
|
import { markdownToHTML } from "../../markdown/markdown-parser";
|
|
3
4
|
import { getImageNodeMarkdownSerializer, hasImageNode, } from "../prosemirror-adapter/plugins/image/node";
|
|
@@ -15,12 +16,12 @@ const createMarkdownSerializerFunction = (config) => {
|
|
|
15
16
|
state.write(tagClose);
|
|
16
17
|
};
|
|
17
18
|
};
|
|
18
|
-
const buildMarkdownSerializer = (plugins, language) => {
|
|
19
|
+
const buildMarkdownSerializer = (plugins, language, inlineImages) => {
|
|
19
20
|
const customNodes = {};
|
|
20
21
|
for (const plugin of plugins) {
|
|
21
22
|
customNodes[plugin.tagName] = createMarkdownSerializerFunction(plugin);
|
|
22
23
|
}
|
|
23
|
-
const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language)), customNodes);
|
|
24
|
+
const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language, inlineImages)), customNodes);
|
|
24
25
|
const marks = Object.assign(Object.assign({}, defaultMarkdownSerializer.marks), { strikethrough: {
|
|
25
26
|
open: '~~',
|
|
26
27
|
close: '~~',
|
|
@@ -33,9 +34,11 @@ const buildMarkdownSerializer = (plugins, language) => {
|
|
|
33
34
|
* @private
|
|
34
35
|
*/
|
|
35
36
|
export class MarkdownConverter {
|
|
36
|
-
constructor(plugins, language) {
|
|
37
|
+
constructor(plugins, language, inlineImages) {
|
|
37
38
|
this.parseAsHTML = (text) => {
|
|
38
|
-
return markdownToHTML(text, {
|
|
39
|
+
return markdownToHTML(text, {
|
|
40
|
+
whitelist: buildContentWhitelist(this.customNodes, this.inlineImages),
|
|
41
|
+
});
|
|
39
42
|
};
|
|
40
43
|
this.serialize = (view) => {
|
|
41
44
|
if (view.state.doc.textContent.trim() === '' &&
|
|
@@ -45,7 +48,8 @@ export class MarkdownConverter {
|
|
|
45
48
|
}
|
|
46
49
|
return this.markdownSerializer.serialize(view.state.doc);
|
|
47
50
|
};
|
|
48
|
-
this.markdownSerializer = buildMarkdownSerializer(plugins, language);
|
|
51
|
+
this.markdownSerializer = buildMarkdownSerializer(plugins, language, inlineImages);
|
|
49
52
|
this.customNodes = plugins;
|
|
53
|
+
this.inlineImages = inlineImages;
|
|
50
54
|
}
|
|
51
55
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { g as globalConfig } from './config-Dnt5w_Bp.js';
|
|
2
2
|
export { _ as _mapLayout } from './layout-C4zsCPOF.js';
|
|
3
|
-
export { E as EditorMenuTypes, L as LevelMapping, M as MouseButtons, e as editorMenuTypesArray } from './types-
|
|
3
|
+
export { E as EditorMenuTypes, L as LevelMapping, M as MouseButtons, e as editorMenuTypesArray, i as isInlineImageTag } from './types-C_Yu8dOg.js';
|
|
4
4
|
export { r as resizeImage } from './image-resize-DPcww8rE.js';
|
|
5
5
|
export { r as redrawComponents } from './dispatch-resize-event-z_E3sq3p.js';
|
|
6
6
|
|