@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/{image-resize-FAwzj92q.js → image-resize-BWRGvgkM.js} +42 -2
  3. package/dist/cjs/index.cjs.js +3 -2
  4. package/dist/cjs/lime-elements.cjs.js +1 -1
  5. package/dist/cjs/limel-action-bar_3.cjs.entry.js +1 -1
  6. package/dist/cjs/limel-ai-avatar.cjs.entry.js +1 -1
  7. package/dist/cjs/limel-callout.cjs.entry.js +1 -1
  8. package/dist/cjs/limel-chart.cjs.entry.js +1 -1
  9. package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
  10. package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
  11. package/dist/cjs/limel-code-diff.cjs.entry.js +1 -1
  12. package/dist/cjs/limel-code-editor.cjs.entry.js +1 -1
  13. package/dist/cjs/limel-collapsible-section.cjs.entry.js +1 -1
  14. package/dist/cjs/limel-drag-handle.cjs.entry.js +1 -1
  15. package/dist/cjs/limel-email-viewer.cjs.entry.js +1 -1
  16. package/dist/cjs/limel-file-viewer.cjs.entry.js +1 -1
  17. package/dist/cjs/limel-file.cjs.entry.js +84 -18
  18. package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +1 -1
  19. package/dist/cjs/limel-list-item.cjs.entry.js +1 -1
  20. package/dist/cjs/limel-picker.cjs.entry.js +1 -1
  21. package/dist/cjs/limel-profile-picture.cjs.entry.js +2 -2
  22. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +440 -212
  23. package/dist/cjs/limel-snackbar.cjs.entry.js +1 -1
  24. package/dist/cjs/limel-table.cjs.entry.js +1 -1
  25. package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
  26. package/dist/cjs/limel-text-editor.cjs.entry.js +2 -2
  27. package/dist/cjs/loader.cjs.js +1 -1
  28. package/dist/cjs/{translations-VEsa81QY.js → translations-Bnw00bkf.js} +8 -0
  29. package/dist/cjs/{types-Bu6wcdI0.js → types-C7gNcwzE.js} +12 -0
  30. package/dist/collection/components/file/file.js +119 -18
  31. package/dist/collection/components/profile-picture/profile-picture.js +1 -1
  32. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/inserter.js +101 -30
  33. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/node.js +160 -54
  34. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/view.js +6 -1
  35. package/dist/collection/components/text-editor/prosemirror-adapter/prosemirror-adapter.js +51 -5
  36. package/dist/collection/components/text-editor/text-editor.js +33 -3
  37. package/dist/collection/components/text-editor/text-editor.types.js +10 -1
  38. package/dist/collection/components/text-editor/utils/content-type-converter.js +30 -1
  39. package/dist/collection/components/text-editor/utils/html-converter.js +4 -2
  40. package/dist/collection/components/text-editor/utils/markdown-converter.js +9 -5
  41. package/dist/collection/translations/da.js +1 -0
  42. package/dist/collection/translations/de.js +1 -0
  43. package/dist/collection/translations/en.js +1 -0
  44. package/dist/collection/translations/fi.js +1 -0
  45. package/dist/collection/translations/fr.js +1 -0
  46. package/dist/collection/translations/nl.js +1 -0
  47. package/dist/collection/translations/no.js +1 -0
  48. package/dist/collection/translations/sv.js +1 -0
  49. package/dist/collection/util/image-resize.js +42 -2
  50. package/dist/esm/{image-resize-DPcww8rE.js → image-resize-65ma7B85.js} +42 -2
  51. package/dist/esm/index.js +2 -2
  52. package/dist/esm/lime-elements.js +1 -1
  53. package/dist/esm/limel-action-bar_3.entry.js +1 -1
  54. package/dist/esm/limel-ai-avatar.entry.js +1 -1
  55. package/dist/esm/limel-callout.entry.js +1 -1
  56. package/dist/esm/limel-chart.entry.js +1 -1
  57. package/dist/esm/limel-chip-set.entry.js +1 -1
  58. package/dist/esm/limel-chip_2.entry.js +1 -1
  59. package/dist/esm/limel-code-diff.entry.js +1 -1
  60. package/dist/esm/limel-code-editor.entry.js +1 -1
  61. package/dist/esm/limel-collapsible-section.entry.js +1 -1
  62. package/dist/esm/limel-drag-handle.entry.js +1 -1
  63. package/dist/esm/limel-email-viewer.entry.js +1 -1
  64. package/dist/esm/limel-file-viewer.entry.js +1 -1
  65. package/dist/esm/limel-file.entry.js +84 -18
  66. package/dist/esm/limel-flatpickr-adapter.entry.js +1 -1
  67. package/dist/esm/limel-list-item.entry.js +1 -1
  68. package/dist/esm/limel-picker.entry.js +1 -1
  69. package/dist/esm/limel-profile-picture.entry.js +2 -2
  70. package/dist/esm/limel-prosemirror-adapter.entry.js +440 -212
  71. package/dist/esm/limel-snackbar.entry.js +1 -1
  72. package/dist/esm/limel-table.entry.js +1 -1
  73. package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
  74. package/dist/esm/limel-text-editor.entry.js +2 -2
  75. package/dist/esm/loader.js +1 -1
  76. package/dist/esm/{translations-EPnIW0K5.js → translations-Cb9R_zmF.js} +8 -0
  77. package/dist/esm/{types-BWYo6dLf.js → types-C_Yu8dOg.js} +12 -1
  78. package/dist/lime-elements/index.esm.js +1 -1
  79. package/dist/lime-elements/lime-elements.esm.js +1 -1
  80. package/dist/lime-elements/{p-068e7087.entry.js → p-19c81ded.entry.js} +2 -2
  81. package/dist/lime-elements/p-1cc3b516.entry.js +1 -0
  82. package/dist/lime-elements/{p-ca0a8a5a.entry.js → p-1e3cdfa1.entry.js} +1 -1
  83. package/dist/lime-elements/p-1f35fc44.entry.js +1 -0
  84. package/dist/lime-elements/{p-2531a103.entry.js → p-2284caa7.entry.js} +1 -1
  85. package/dist/lime-elements/{p-d8cb7d42.entry.js → p-2f223e46.entry.js} +1 -1
  86. package/dist/lime-elements/{p-a3a2aafe.entry.js → p-4a09fb2e.entry.js} +1 -1
  87. package/dist/lime-elements/{p-f197036b.entry.js → p-552d0733.entry.js} +1 -1
  88. package/dist/lime-elements/p-65ma7B85.js +1 -0
  89. package/dist/lime-elements/{p-fd78537c.entry.js → p-6fbf20c6.entry.js} +1 -1
  90. package/dist/lime-elements/{p-5571f0de.entry.js → p-758939be.entry.js} +1 -1
  91. package/dist/lime-elements/{p-19347686.entry.js → p-7f7e2180.entry.js} +1 -1
  92. package/dist/lime-elements/{p-2a871a95.entry.js → p-89f9bd6c.entry.js} +1 -1
  93. package/dist/lime-elements/{p-79caf3f6.entry.js → p-9ae7d11b.entry.js} +1 -1
  94. package/dist/lime-elements/p-C_Yu8dOg.js +1 -0
  95. package/dist/lime-elements/p-Cb9R_zmF.js +1 -0
  96. package/dist/lime-elements/{p-6edf13b1.entry.js → p-a089b580.entry.js} +1 -1
  97. package/dist/lime-elements/{p-04b4e903.entry.js → p-aa080a8f.entry.js} +1 -1
  98. package/dist/lime-elements/{p-54961e0e.entry.js → p-c1ceade7.entry.js} +1 -1
  99. package/dist/lime-elements/{p-e08f770e.entry.js → p-c8116a01.entry.js} +1 -1
  100. package/dist/lime-elements/{p-36ccc68e.entry.js → p-c8982eef.entry.js} +1 -1
  101. package/dist/lime-elements/{p-d98168a1.entry.js → p-ca0b86ee.entry.js} +1 -1
  102. package/dist/lime-elements/{p-1525274d.entry.js → p-d7bb4310.entry.js} +1 -1
  103. package/dist/lime-elements/p-d7f75a6d.entry.js +1 -0
  104. package/dist/lime-elements/{p-bb201b0d.entry.js → p-e545c9e8.entry.js} +1 -1
  105. package/dist/types/components/file/file.d.ts +20 -0
  106. package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/inserter.d.ts +2 -2
  107. package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/node.d.ts +41 -19
  108. package/dist/types/components/text-editor/prosemirror-adapter/prosemirror-adapter.d.ts +13 -1
  109. package/dist/types/components/text-editor/text-editor.d.ts +10 -2
  110. package/dist/types/components/text-editor/text-editor.types.d.ts +52 -0
  111. package/dist/types/components/text-editor/utils/content-type-converter.d.ts +12 -0
  112. package/dist/types/components/text-editor/utils/html-converter.d.ts +3 -1
  113. package/dist/types/components/text-editor/utils/markdown-converter.d.ts +3 -1
  114. package/dist/types/components.d.ts +50 -8
  115. package/dist/types/translations/da.d.ts +1 -0
  116. package/dist/types/translations/de.d.ts +1 -0
  117. package/dist/types/translations/en.d.ts +1 -0
  118. package/dist/types/translations/fi.d.ts +1 -0
  119. package/dist/types/translations/fr.d.ts +1 -0
  120. package/dist/types/translations/nl.d.ts +1 -0
  121. package/dist/types/translations/no.d.ts +1 -0
  122. package/dist/types/translations/sv.d.ts +1 -0
  123. package/dist/types/util/image-resize.d.ts +12 -4
  124. package/package.json +1 -1
  125. package/dist/lime-elements/p-366419ee.entry.js +0 -1
  126. package/dist/lime-elements/p-5cf7464f.entry.js +0 -1
  127. package/dist/lime-elements/p-BWYo6dLf.js +0 -1
  128. package/dist/lime-elements/p-DPcww8rE.js +0 -1
  129. package/dist/lime-elements/p-EPnIW0K5.js +0 -1
  130. package/dist/lime-elements/p-b7d734d2.entry.js +0 -1
@@ -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 { state, dispatch } = view;
38
- const { schema } = state;
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 { state, dispatch } = view;
52
- const { schema } = state;
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, reader.result, createFileInfo(file)),
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
- * any of its child nodes is an image node.
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 imageHTML = getImageHTML(node.attrs);
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('&', '&amp;')
88
+ .replaceAll('"', '&quot;')
89
+ .replaceAll('<', '&lt;')
90
+ .replaceAll('>', '&gt;');
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 style = [];
70
- if (attrs.height) {
71
- style.push(`height: ${attrs.height};`);
72
- }
73
- if (attrs.width) {
74
- style.push(`width: ${attrs.width};`);
75
- }
76
- if (attrs.minHeight) {
77
- style.push(`min-height: ${attrs.minHeight};`);
78
- }
79
- if (attrs.minWidth) {
80
- style.push(`min-width: ${attrs.minWidth};`);
81
- }
82
- if (attrs.maxWidth) {
83
- style.push(`max-width: ${attrs.maxWidth};`);
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: (node) => {
104
- if (!isEditorImageState(node.attrs.state)) {
105
- return;
106
- }
107
- if (node.attrs.state === 'success') {
108
- return getOrCreateImageElement(node.attrs.fileInfoId, node);
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
- parseDOM: [
113
- {
114
- tag: 'img',
115
- getAttrs: (dom) => {
116
- return {
117
- src: dom.getAttribute('src') || '',
118
- alt: dom.getAttribute('alt') || 'file',
119
- width: dom.style.width || '',
120
- height: dom.style.height || '',
121
- maxWidth: '100%',
122
- state: 'success',
123
- fileInfoId: crypto.randomUUID(),
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
- this.persistDimensions();
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: '46d850f4bcc3d64ba180276ec9080f2b5b38470e', onFocus: this.handleFocus }, h("div", { key: '7695f4a898726c5e2db5d18257dda0e2e1616ed7', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
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: '12f7643f82a4899efd112dc74e67fbd79be64d9f' }, h("limel-notched-outline", { key: '109e7c03874e141f073a05882ca48188adaced72', 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()));
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
- export {};
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
+ }