@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,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var index$1 = require('./index-DYg_7kkT.js');
4
- var types = require('./types-Bu6wcdI0.js');
4
+ var types = require('./types-C7gNcwzE.js');
5
5
  var markdownParser = require('./markdown-parser-F3YRMjcy.js');
6
6
  var cloneDeep = require('./cloneDeep-hE40Q8aw.js');
7
+ var translations = require('./translations-Bnw00bkf.js');
7
8
  var index = require('./index-ETPz91V5.js');
8
- var translations = require('./translations-VEsa81QY.js');
9
9
  var randomString = require('./random-string-BTzDB2ee.js');
10
10
  var isItem = require('./is-item-DMELyaao.js');
11
11
  var files = require('./files-Bt11HLi4.js');
@@ -15831,6 +15831,307 @@ const menuTranslationIDs = {
15831
15831
  code: 'editor-menu.code',
15832
15832
  };
15833
15833
 
15834
+ const imageCache = new Map();
15835
+ const IMAGE_ID_ATTRIBUTE = 'image-id';
15836
+ /**
15837
+ * The attribute names accepted for an inline-image tag. Backs the sanitizer
15838
+ * whitelist built in `buildContentWhitelist`, so a stored tag keeps these
15839
+ * attributes through parsing. The serializers here enumerate their own output
15840
+ * attributes separately and are not driven by this list.
15841
+ */
15842
+ const INLINE_IMAGE_ATTRIBUTES = [
15843
+ IMAGE_ID_ATTRIBUTE,
15844
+ 'width',
15845
+ 'height',
15846
+ 'alt',
15847
+ ];
15848
+ /**
15849
+ * Builds the `image` NodeSpec, wired for inline-image parsing/serialization
15850
+ * when `inlineImages` is configured.
15851
+ * @param language
15852
+ * @param inlineImages
15853
+ */
15854
+ function getImageNode(language, inlineImages) {
15855
+ return { image: createImageNodeSpec(language, inlineImages) };
15856
+ }
15857
+ /**
15858
+ * Builds the markdown serializer for the `image` node, emitting the inline-image
15859
+ * tag when `inlineImages` is configured and a `<img>` otherwise.
15860
+ * @param language
15861
+ * @param inlineImages
15862
+ */
15863
+ function getImageNodeMarkdownSerializer(language, inlineImages) {
15864
+ return { image: createImageNodeMarkdownSerializer(language, inlineImages) };
15865
+ }
15866
+ /**
15867
+ * Applies the node's stored dimensions to a rendered `<img>` element.
15868
+ * @param img
15869
+ * @param node
15870
+ */
15871
+ function applyImageStyles(img, node) {
15872
+ img.style.height = node.attrs.height;
15873
+ img.style.width = node.attrs.width;
15874
+ img.style.minHeight = node.attrs.minHeight;
15875
+ img.style.minWidth = node.attrs.minWidth;
15876
+ img.style.maxWidth = node.attrs.maxWidth;
15877
+ }
15878
+ /**
15879
+ * Recursively checks if a ProseMirror node or any of its descendants is an
15880
+ * image node.
15881
+ * @param node
15882
+ */
15883
+ function hasImageNode(node) {
15884
+ if (node.type.name === 'image') {
15885
+ return true;
15886
+ }
15887
+ for (let i = 0; i < node.childCount; i++) {
15888
+ const childNode = node.child(i);
15889
+ if (hasImageNode(childNode)) {
15890
+ return true;
15891
+ }
15892
+ }
15893
+ return false;
15894
+ }
15895
+ function createImageNodeMarkdownSerializer(language, inlineImages) {
15896
+ return (markdownSerializerState, node) => {
15897
+ const state = node.attrs.state;
15898
+ if (!isEditorImageState(state)) {
15899
+ return;
15900
+ }
15901
+ if (state === 'success') {
15902
+ const attrs = node.attrs;
15903
+ const tag = inlineImages && types.isInlineImageTag(inlineImages)
15904
+ ? inlineImages
15905
+ : undefined;
15906
+ const imageHTML = tag && attrs.imageId
15907
+ ? getInlineImageHTML(attrs, tag.tagName)
15908
+ : getImageHTML(attrs);
15909
+ markdownSerializerState.write(imageHTML);
15910
+ return;
15911
+ }
15912
+ const statusHTML = getStatusHTML(state, node.attrs.alt, language);
15913
+ markdownSerializerState.write(statusHTML);
15914
+ };
15915
+ }
15916
+ function escapeAttributeValue(value) {
15917
+ return value
15918
+ .replaceAll('&', '&amp;')
15919
+ .replaceAll('"', '&quot;')
15920
+ .replaceAll('<', '&lt;')
15921
+ .replaceAll('>', '&gt;');
15922
+ }
15923
+ // Dimensions are emitted as CSS-unit strings (e.g. `width="300px"`), matching
15924
+ // what `applyImageStyles` reads back when the tag is re-parsed inside the
15925
+ // editor. The external renderer of this microformat (the `limebb-inline-image`
15926
+ // building block) must therefore treat `width`/`height` as CSS lengths, not as
15927
+ // bare HTML pixel counts.
15928
+ function getInlineImageHTML(attrs, tag) {
15929
+ var _a;
15930
+ const attributes = [
15931
+ `${IMAGE_ID_ATTRIBUTE}="${escapeAttributeValue((_a = attrs.imageId) !== null && _a !== void 0 ? _a : '')}"`,
15932
+ attrs.width ? `width="${escapeAttributeValue(attrs.width)}"` : '',
15933
+ attrs.height ? `height="${escapeAttributeValue(attrs.height)}"` : '',
15934
+ attrs.alt ? `alt="${escapeAttributeValue(attrs.alt)}"` : '',
15935
+ ].filter(Boolean);
15936
+ return `<${tag} ${attributes.join(' ')}></${tag}>`;
15937
+ }
15938
+ function buildInlineImageDOMAttrs(attrs) {
15939
+ var _a;
15940
+ const result = {
15941
+ [IMAGE_ID_ATTRIBUTE]: (_a = attrs.imageId) !== null && _a !== void 0 ? _a : '',
15942
+ };
15943
+ if (attrs.alt) {
15944
+ result.alt = attrs.alt;
15945
+ }
15946
+ if (attrs.width) {
15947
+ result.width = attrs.width;
15948
+ }
15949
+ if (attrs.height) {
15950
+ result.height = attrs.height;
15951
+ }
15952
+ return result;
15953
+ }
15954
+ function getStatusHTML(state, alt, language) {
15955
+ const key = state === 'failed' ? 'failed' : 'loading';
15956
+ const text = translations.translate.get(`editor-image-view.${key}`, language, {
15957
+ filename: alt || 'file',
15958
+ });
15959
+ return `<span>${text}</span>`;
15960
+ }
15961
+ function getImageHTML(attrs) {
15962
+ const dimensions = [
15963
+ ['height', attrs.height],
15964
+ ['width', attrs.width],
15965
+ ['min-height', attrs.minHeight],
15966
+ ['min-width', attrs.minWidth],
15967
+ ['max-width', attrs.maxWidth],
15968
+ ];
15969
+ const style = dimensions
15970
+ .filter(([, value]) => value)
15971
+ .map(([property, value]) => `${property}: ${value};`)
15972
+ .join('');
15973
+ const styleAttribute = style
15974
+ ? ` style="${escapeAttributeValue(style)}"`
15975
+ : '';
15976
+ return `<img src="${escapeAttributeValue(attrs.src)}" alt="${escapeAttributeValue(attrs.alt)}"${styleAttribute} />`;
15977
+ }
15978
+ function createImageNodeSpec(language, inlineImages) {
15979
+ return {
15980
+ group: 'inline',
15981
+ inline: true,
15982
+ attrs: {
15983
+ src: { default: '' },
15984
+ alt: { default: '' },
15985
+ fileInfoId: { default: '' },
15986
+ imageId: { default: '' },
15987
+ height: { default: '' },
15988
+ width: { default: '' },
15989
+ minHeight: { default: '' },
15990
+ minWidth: { default: '' },
15991
+ maxWidth: { default: '100%' },
15992
+ state: { default: 'success' },
15993
+ },
15994
+ toDOM: createImageToDOM(language, inlineImages),
15995
+ parseDOM: createImageParseDOM(inlineImages),
15996
+ };
15997
+ }
15998
+ function createImageToDOM(language, inlineImages) {
15999
+ return (node) => {
16000
+ if (!isEditorImageState(node.attrs.state)) {
16001
+ return;
16002
+ }
16003
+ if (node.attrs.state !== 'success') {
16004
+ return createStatusSpanForState(node.attrs.state, node, language);
16005
+ }
16006
+ // Emit the inline-image tag for HTML serialization and clipboard;
16007
+ // on-screen rendering comes from the NodeView, not this.
16008
+ const tag = inlineImages && types.isInlineImageTag(inlineImages)
16009
+ ? inlineImages
16010
+ : undefined;
16011
+ if (tag && node.attrs.imageId) {
16012
+ return [
16013
+ tag.tagName,
16014
+ buildInlineImageDOMAttrs(node.attrs),
16015
+ ];
16016
+ }
16017
+ return getOrCreateImageElement(node.attrs.fileInfoId, node);
16018
+ };
16019
+ }
16020
+ function createImageParseDOM(inlineImages) {
16021
+ const rules = [];
16022
+ const tag = inlineImages && types.isInlineImageTag(inlineImages)
16023
+ ? inlineImages
16024
+ : undefined;
16025
+ if (tag) {
16026
+ rules.push({
16027
+ tag: tag.tagName,
16028
+ getAttrs: (dom) => {
16029
+ const imageId = dom.getAttribute(IMAGE_ID_ATTRIBUTE);
16030
+ // Skip malformed tags without an id; parsing them would
16031
+ // produce a broken image from an empty getUrl('').
16032
+ if (!imageId) {
16033
+ return false;
16034
+ }
16035
+ return {
16036
+ src: tag.getUrl(imageId),
16037
+ // Preserve an absent alt as empty so an id-only tag
16038
+ // serializes back to the same markup instead of gaining
16039
+ // a synthetic alt, keeping the round-trip stable.
16040
+ alt: dom.getAttribute('alt') || '',
16041
+ imageId: imageId,
16042
+ width: dom.getAttribute('width') || '',
16043
+ height: dom.getAttribute('height') || '',
16044
+ maxWidth: '100%',
16045
+ state: 'success',
16046
+ fileInfoId: crypto.randomUUID(),
16047
+ };
16048
+ },
16049
+ });
16050
+ }
16051
+ rules.push({
16052
+ tag: 'img',
16053
+ getAttrs: (dom) => {
16054
+ return {
16055
+ src: dom.getAttribute('src') || '',
16056
+ alt: dom.getAttribute('alt') || 'file',
16057
+ width: dom.style.width || '',
16058
+ height: dom.style.height || '',
16059
+ maxWidth: '100%',
16060
+ state: 'success',
16061
+ fileInfoId: crypto.randomUUID(),
16062
+ };
16063
+ },
16064
+ });
16065
+ return rules;
16066
+ }
16067
+ function isEditorImageState(state) {
16068
+ return state === 'loading' || state === 'failed' || state === 'success';
16069
+ }
16070
+ function getOrCreateImageElement(fileInfoId, node) {
16071
+ let img = imageCache.get(fileInfoId);
16072
+ if (img) {
16073
+ updateImageElement(img, node);
16074
+ }
16075
+ else {
16076
+ img = createImageElement(node);
16077
+ imageCache.set(fileInfoId, img);
16078
+ }
16079
+ return img;
16080
+ }
16081
+ function createStatusSpanForState(state, node, language) {
16082
+ const statusKey = state === 'failed' ? 'failed' : 'loading';
16083
+ return createStatusSpan(statusKey, node, language);
16084
+ }
16085
+ function createStatusSpan(key, node, language) {
16086
+ const text = translations.translate.get(`editor-image-view.${key}`, language, {
16087
+ filename: node.attrs.alt || 'file',
16088
+ });
16089
+ const span = document.createElement('span');
16090
+ span.textContent = text;
16091
+ return span;
16092
+ }
16093
+ function updateImageElement(img, node) {
16094
+ img.alt = node.attrs.alt;
16095
+ applyImageStyles(img, node);
16096
+ return img;
16097
+ }
16098
+ function createImageElement(node) {
16099
+ const img = document.createElement('img');
16100
+ img.src = node.attrs.src;
16101
+ img.alt = node.attrs.alt;
16102
+ applyImageStyles(img, node);
16103
+ return img;
16104
+ }
16105
+
16106
+ /**
16107
+ * Build the sanitization whitelist for editor content: the consumer's custom
16108
+ * elements, plus (when inline images are configured) the inline-image tag so it
16109
+ * survives parsing into an image node. Shared by all content converters.
16110
+ *
16111
+ * @param customNodes
16112
+ * @param inlineImages
16113
+ * @private
16114
+ */
16115
+ function buildContentWhitelist(customNodes, inlineImages) {
16116
+ if (!inlineImages || !types.isInlineImageTag(inlineImages)) {
16117
+ return customNodes;
16118
+ }
16119
+ // Guard the boundary independently of the upstream `validateInlineImages`
16120
+ // check: the tag name becomes a sanitizer whitelist key, so a built-in like
16121
+ // `img` (no hyphen) would hijack its rules and strip `src` from every real
16122
+ // image. Custom element names always contain a hyphen; built-ins never do.
16123
+ if (!inlineImages.tagName.includes('-')) {
16124
+ return customNodes;
16125
+ }
16126
+ return [
16127
+ ...customNodes,
16128
+ {
16129
+ tagName: inlineImages.tagName,
16130
+ attributes: [...INLINE_IMAGE_ATTRIBUTES],
16131
+ },
16132
+ ];
16133
+ }
16134
+
15834
16135
  /* eslint-disable no-bitwise */
15835
16136
 
15836
16137
  const decodeCache = {};
@@ -24673,173 +24974,6 @@ class MarkdownSerializerState {
24673
24974
  }
24674
24975
  }
24675
24976
 
24676
- const imageCache = new Map();
24677
- /**
24678
- *
24679
- * @param language
24680
- */
24681
- function getImageNode(language) {
24682
- return { image: createImageNodeSpec(language) };
24683
- }
24684
- /**
24685
- *
24686
- * @param language
24687
- */
24688
- function getImageNodeMarkdownSerializer(language) {
24689
- return { image: createImageNodeMarkdownSerializer(language) };
24690
- }
24691
- /**
24692
- *
24693
- * @param img
24694
- * @param node
24695
- */
24696
- function applyImageStyles(img, node) {
24697
- img.style.height = node.attrs.height;
24698
- img.style.width = node.attrs.width;
24699
- img.style.minHeight = node.attrs.minHeight;
24700
- img.style.minWidth = node.attrs.minWidth;
24701
- img.style.maxWidth = node.attrs.maxWidth;
24702
- }
24703
- /**
24704
- * Recursively checks if a ProseMirror node or
24705
- * any of its child nodes is an image node.
24706
- * @param node
24707
- */
24708
- function hasImageNode(node) {
24709
- if (node.type.name === 'image') {
24710
- return true;
24711
- }
24712
- for (let i = 0; i < node.childCount; i++) {
24713
- const childNode = node.child(i);
24714
- if (hasImageNode(childNode)) {
24715
- return true;
24716
- }
24717
- }
24718
- return false;
24719
- }
24720
- function createImageNodeMarkdownSerializer(language) {
24721
- return (markdownSerializerState, node) => {
24722
- const state = node.attrs.state;
24723
- if (!isEditorImageState(state)) {
24724
- return;
24725
- }
24726
- if (state === 'success') {
24727
- const imageHTML = getImageHTML(node.attrs);
24728
- markdownSerializerState.write(imageHTML);
24729
- return;
24730
- }
24731
- const statusHTML = getStatusHTML(state, node.attrs.alt, language);
24732
- markdownSerializerState.write(statusHTML);
24733
- };
24734
- }
24735
- function getStatusHTML(state, alt, language) {
24736
- const key = state === 'failed' ? 'failed' : 'loading';
24737
- const text = translations.translate.get(`editor-image-view.${key}`, language, {
24738
- filename: alt || 'file',
24739
- });
24740
- return `<span>${text}</span>`;
24741
- }
24742
- function getImageHTML(attrs) {
24743
- const style = [];
24744
- if (attrs.height) {
24745
- style.push(`height: ${attrs.height};`);
24746
- }
24747
- if (attrs.width) {
24748
- style.push(`width: ${attrs.width};`);
24749
- }
24750
- if (attrs.minHeight) {
24751
- style.push(`min-height: ${attrs.minHeight};`);
24752
- }
24753
- if (attrs.minWidth) {
24754
- style.push(`min-width: ${attrs.minWidth};`);
24755
- }
24756
- if (attrs.maxWidth) {
24757
- style.push(`max-width: ${attrs.maxWidth};`);
24758
- }
24759
- const styleAttribute = style.length > 0 ? ` style="${style.join('')}"` : '';
24760
- return `<img src="${attrs.src}" alt="${attrs.alt}"${styleAttribute} />`;
24761
- }
24762
- function createImageNodeSpec(language) {
24763
- return {
24764
- group: 'inline',
24765
- inline: true,
24766
- attrs: {
24767
- src: { default: '' },
24768
- alt: { default: '' },
24769
- fileInfoId: { default: '' },
24770
- height: { default: '' },
24771
- width: { default: '' },
24772
- minHeight: { default: '' },
24773
- minWidth: { default: '' },
24774
- maxWidth: { default: '100%' },
24775
- state: { default: 'success' },
24776
- },
24777
- toDOM: (node) => {
24778
- if (!isEditorImageState(node.attrs.state)) {
24779
- return;
24780
- }
24781
- if (node.attrs.state === 'success') {
24782
- return getOrCreateImageElement(node.attrs.fileInfoId, node);
24783
- }
24784
- return createStatusSpanForState(node.attrs.state, node, language);
24785
- },
24786
- parseDOM: [
24787
- {
24788
- tag: 'img',
24789
- getAttrs: (dom) => {
24790
- return {
24791
- src: dom.getAttribute('src') || '',
24792
- alt: dom.getAttribute('alt') || 'file',
24793
- width: dom.style.width || '',
24794
- height: dom.style.height || '',
24795
- maxWidth: '100%',
24796
- state: 'success',
24797
- fileInfoId: crypto.randomUUID(),
24798
- };
24799
- },
24800
- },
24801
- ],
24802
- };
24803
- }
24804
- function isEditorImageState(state) {
24805
- return state === 'loading' || state === 'failed' || state === 'success';
24806
- }
24807
- function getOrCreateImageElement(fileInfoId, node) {
24808
- let img = imageCache.get(fileInfoId);
24809
- if (img) {
24810
- updateImageElement(img, node);
24811
- }
24812
- else {
24813
- img = createImageElement(node);
24814
- imageCache.set(fileInfoId, img);
24815
- }
24816
- return img;
24817
- }
24818
- function createStatusSpanForState(state, node, language) {
24819
- const statusKey = state === 'failed' ? 'failed' : 'loading';
24820
- return createStatusSpan(statusKey, node, language);
24821
- }
24822
- function createStatusSpan(key, node, language) {
24823
- const text = translations.translate.get(`editor-image-view.${key}`, language, {
24824
- filename: node.attrs.alt || 'file',
24825
- });
24826
- const span = document.createElement('span');
24827
- span.textContent = text;
24828
- return span;
24829
- }
24830
- function updateImageElement(img, node) {
24831
- img.alt = node.attrs.alt;
24832
- applyImageStyles(img, node);
24833
- return img;
24834
- }
24835
- function createImageElement(node) {
24836
- const img = document.createElement('img');
24837
- img.src = node.attrs.src;
24838
- img.alt = node.attrs.alt;
24839
- applyImageStyles(img, node);
24840
- return img;
24841
- }
24842
-
24843
24977
  /**
24844
24978
  * Recursively checks if a ProseMirror node or
24845
24979
  * any of its child nodes is a registered custom element node.
@@ -24875,12 +25009,12 @@ const createMarkdownSerializerFunction = (config) => {
24875
25009
  state.write(tagClose);
24876
25010
  };
24877
25011
  };
24878
- const buildMarkdownSerializer = (plugins, language) => {
25012
+ const buildMarkdownSerializer = (plugins, language, inlineImages) => {
24879
25013
  const customNodes = {};
24880
25014
  for (const plugin of plugins) {
24881
25015
  customNodes[plugin.tagName] = createMarkdownSerializerFunction(plugin);
24882
25016
  }
24883
- const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language)), customNodes);
25017
+ const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language, inlineImages)), customNodes);
24884
25018
  const marks = Object.assign(Object.assign({}, defaultMarkdownSerializer.marks), { strikethrough: {
24885
25019
  open: '~~',
24886
25020
  close: '~~',
@@ -24893,9 +25027,11 @@ const buildMarkdownSerializer = (plugins, language) => {
24893
25027
  * @private
24894
25028
  */
24895
25029
  class MarkdownConverter {
24896
- constructor(plugins, language) {
25030
+ constructor(plugins, language, inlineImages) {
24897
25031
  this.parseAsHTML = (text) => {
24898
- return markdownParser.markdownToHTML(text, { whitelist: this.customNodes });
25032
+ return markdownParser.markdownToHTML(text, {
25033
+ whitelist: buildContentWhitelist(this.customNodes, this.inlineImages),
25034
+ });
24899
25035
  };
24900
25036
  this.serialize = (view) => {
24901
25037
  if (view.state.doc.textContent.trim() === '' &&
@@ -24905,8 +25041,9 @@ class MarkdownConverter {
24905
25041
  }
24906
25042
  return this.markdownSerializer.serialize(view.state.doc);
24907
25043
  };
24908
- this.markdownSerializer = buildMarkdownSerializer(plugins, language);
25044
+ this.markdownSerializer = buildMarkdownSerializer(plugins, language, inlineImages);
24909
25045
  this.customNodes = plugins;
25046
+ this.inlineImages = inlineImages;
24910
25047
  }
24911
25048
  }
24912
25049
 
@@ -24914,9 +25051,9 @@ class MarkdownConverter {
24914
25051
  * @private
24915
25052
  */
24916
25053
  class HTMLConverter {
24917
- constructor(plugins) {
25054
+ constructor(plugins, inlineImages) {
24918
25055
  this.parseAsHTML = (text) => {
24919
- return markdownParser.sanitizeHTML(text, this.customNodes);
25056
+ return markdownParser.sanitizeHTML(text, buildContentWhitelist(this.customNodes, this.inlineImages));
24920
25057
  };
24921
25058
  this.serialize = (view) => {
24922
25059
  if (view.state.doc.textContent.trim() === '' &&
@@ -24929,6 +25066,7 @@ class HTMLConverter {
24929
25066
  return div.innerHTML;
24930
25067
  };
24931
25068
  this.customNodes = plugins;
25069
+ this.inlineImages = inlineImages;
24932
25070
  }
24933
25071
  }
24934
25072
 
@@ -25329,12 +25467,12 @@ const linkMarkSpec = {
25329
25467
  };
25330
25468
 
25331
25469
  const pluginKey = new PluginKey('imageInserterPlugin');
25332
- const createImageInserterPlugin = (imagePastedCallback) => {
25470
+ const createImageInserterPlugin = (imagePastedCallback, inlineImages) => {
25333
25471
  return new Plugin({
25334
25472
  key: pluginKey,
25335
25473
  props: {
25336
25474
  handlePaste: (view, event, slice) => {
25337
- return processPasteEvent(view, event, slice);
25475
+ return processPasteEvent(view, event, slice, inlineImages);
25338
25476
  },
25339
25477
  handleDOMEvents: {
25340
25478
  imagePasted: (_, event) => {
@@ -25344,6 +25482,83 @@ const createImageInserterPlugin = (imagePastedCallback) => {
25344
25482
  },
25345
25483
  });
25346
25484
  };
25485
+ /**
25486
+ * Runs the inline-image upload lifecycle: show a thumbnail, run the upload,
25487
+ * then replace it with the resizable image (carrying the file id) or a failed
25488
+ * state.
25489
+ * @param view
25490
+ * @param file
25491
+ * @param base64Data
25492
+ * @param fileInfo
25493
+ * @param inlineImages
25494
+ */
25495
+ const runInlineImageUpload = async (view, file, base64Data, fileInfo, inlineImages) => {
25496
+ const upload = inlineImages.upload;
25497
+ if (!upload) {
25498
+ return;
25499
+ }
25500
+ const inserter = imageInserterFactory(view, base64Data, fileInfo);
25501
+ inserter.insertThumbnail();
25502
+ try {
25503
+ const uploadResult = await upload(file);
25504
+ replaceThumbnailWithInlineImage(view, fileInfo, uploadResult, inlineImages);
25505
+ }
25506
+ catch (error) {
25507
+ console.error('Inline image upload failed', fileInfo.filename, error);
25508
+ inserter.insertFailedThumbnail();
25509
+ }
25510
+ };
25511
+ /**
25512
+ * Walks the document for the image node correlated with `fileInfoId`, replaces
25513
+ * it with a freshly built node, and dispatches the change. The transaction is
25514
+ * only dispatched when a match is found.
25515
+ * @param view
25516
+ * @param fileInfoId
25517
+ * @param buildNode - builds the replacement node from the matched node.
25518
+ * @returns whether a matching node was found and replaced.
25519
+ */
25520
+ const replaceImageNodeByFileInfoId = (view, fileInfoId, buildNode) => {
25521
+ const { state, dispatch } = view;
25522
+ const tr = state.tr;
25523
+ let found = false;
25524
+ state.doc.descendants((node, pos) => {
25525
+ if (found) {
25526
+ return false;
25527
+ }
25528
+ if (node.attrs.fileInfoId === fileInfoId) {
25529
+ tr.replaceWith(pos, pos + node.nodeSize, buildNode(node));
25530
+ found = true;
25531
+ return false;
25532
+ }
25533
+ });
25534
+ if (found) {
25535
+ dispatch(tr);
25536
+ }
25537
+ return found;
25538
+ };
25539
+ const replaceThumbnailWithInlineImage = (view, fileInfo, uploadResult, inlineImages) => {
25540
+ const { schema } = view.state;
25541
+ // Tag shape: the upload result is a stored id resolved to a src and
25542
+ // persisted as the id microformat. Src shape: the result is the src itself.
25543
+ const tag = types.isInlineImageTag(inlineImages) ? inlineImages : undefined;
25544
+ const replaced = replaceImageNodeByFileInfoId(view, fileInfo.id, () => {
25545
+ var _a;
25546
+ return schema.nodes.image.create({
25547
+ src: tag ? tag.getUrl(uploadResult) : uploadResult,
25548
+ alt: (_a = fileInfo.filename) !== null && _a !== void 0 ? _a : 'file',
25549
+ imageId: tag ? uploadResult : '',
25550
+ fileInfoId: fileInfo.id,
25551
+ state: 'success',
25552
+ maxWidth: '100%',
25553
+ });
25554
+ });
25555
+ if (!replaced) {
25556
+ // The thumbnail was removed (undo/delete) while the upload was in
25557
+ // flight, so the successfully stored file now has nowhere to land and
25558
+ // is orphaned in the backend.
25559
+ console.warn('Inline image uploaded but its thumbnail is gone; the stored file is now orphaned', { uploadResult, filename: fileInfo.filename });
25560
+ }
25561
+ };
25347
25562
  const imageInserterFactory = (view, base64Data, fileInfo) => {
25348
25563
  return {
25349
25564
  fileInfo: fileInfo,
@@ -25361,32 +25576,12 @@ const createThumbnailInserter = (view, base64Data, fileInfo) => () => {
25361
25576
  dispatch(transaction);
25362
25577
  };
25363
25578
  const createImageInserter = (view, fileInfo) => (src) => {
25364
- const { state, dispatch } = view;
25365
- const { schema } = state;
25366
- const tr = state.tr;
25367
- state.doc.descendants((node, pos) => {
25368
- if (node.attrs.fileInfoId === fileInfo.id) {
25369
- const imageNodeAttrs = createImageNodeAttrs(src !== null && src !== void 0 ? src : node.attrs.src, fileInfo, 'success');
25370
- const imageNode = schema.nodes.image.create(imageNodeAttrs);
25371
- tr.replaceWith(pos, pos + node.nodeSize, imageNode);
25372
- return false;
25373
- }
25374
- });
25375
- dispatch(tr);
25579
+ const { schema } = view.state;
25580
+ replaceImageNodeByFileInfoId(view, fileInfo.id, (matched) => schema.nodes.image.create(createImageNodeAttrs(src !== null && src !== void 0 ? src : matched.attrs.src, fileInfo, 'success')));
25376
25581
  };
25377
25582
  const createFailedThumbnailInserter = (view, fileInfo) => () => {
25378
- const { state, dispatch } = view;
25379
- const { schema } = state;
25380
- const tr = state.tr;
25381
- state.doc.descendants((node, pos) => {
25382
- if (node.attrs.fileInfoId === fileInfo.id) {
25383
- const imageNodeAttrs = createImageNodeAttrs(node.attrs.src, fileInfo, 'failed');
25384
- const errorPlaceholderNode = schema.nodes.image.create(imageNodeAttrs);
25385
- tr.replaceWith(pos, pos + node.nodeSize, errorPlaceholderNode);
25386
- return false;
25387
- }
25388
- });
25389
- dispatch(tr);
25583
+ const { schema } = view.state;
25584
+ replaceImageNodeByFileInfoId(view, fileInfo.id, (matched) => schema.nodes.image.create(createImageNodeAttrs(matched.attrs.src, fileInfo, 'failed')));
25390
25585
  };
25391
25586
  function createImageNodeAttrs(src, fileInfo, state) {
25392
25587
  return {
@@ -25456,14 +25651,15 @@ const filterImageNodes = (fragment) => {
25456
25651
  * @param view - The ProseMirror editor view.
25457
25652
  * @param event - The paste event.
25458
25653
  * @param slice
25654
+ * @param inlineImages
25459
25655
  * @returns A boolean; True if an image file was pasted to prevent default paste behavior, otherwise false.
25460
25656
  */
25461
- const processPasteEvent = (view, event, slice) => {
25657
+ const processPasteEvent = (view, event, slice, inlineImages) => {
25462
25658
  const clipboardData = event.clipboardData;
25463
25659
  if (!clipboardData) {
25464
25660
  return false;
25465
25661
  }
25466
- const isImageFilePasted = handlePastedImages(view, clipboardData);
25662
+ const isImageFilePasted = handlePastedImages(view, clipboardData, inlineImages);
25467
25663
  const filteredSlice = new Slice(filterImageNodes(slice.content), slice.openStart, slice.openEnd);
25468
25664
  if (filteredSlice.content.childCount < slice.content.childCount) {
25469
25665
  const { state, dispatch } = view;
@@ -25478,9 +25674,10 @@ const processPasteEvent = (view, event, slice) => {
25478
25674
  *
25479
25675
  * @param view - The ProseMirror editor view
25480
25676
  * @param clipboardData - The clipboard data transfer object containing potential image files
25677
+ * @param inlineImages
25481
25678
  * @returns True if at least one valid image file was found and processed, false otherwise
25482
25679
  */
25483
- function handlePastedImages(view, clipboardData) {
25680
+ function handlePastedImages(view, clipboardData, inlineImages) {
25484
25681
  let isImageFilePasted = false;
25485
25682
  const files$1 = [...(clipboardData.files || [])];
25486
25683
  for (const file of files$1) {
@@ -25488,8 +25685,19 @@ function handlePastedImages(view, clipboardData) {
25488
25685
  isImageFilePasted = true;
25489
25686
  const reader = new FileReader();
25490
25687
  reader.onloadend = () => {
25688
+ const base64Data = reader.result;
25689
+ const fileInfo = files.createFileInfo(file);
25690
+ if (inlineImages) {
25691
+ // Once inline images are configured they own the paste
25692
+ // lifecycle; never fall back to the legacy imagePasted
25693
+ // event. Without an upload handler the paste is a no-op.
25694
+ if (inlineImages.upload) {
25695
+ runInlineImageUpload(view, file, base64Data, fileInfo, inlineImages);
25696
+ }
25697
+ return;
25698
+ }
25491
25699
  view.dom.dispatchEvent(new CustomEvent('imagePasted', {
25492
- detail: imageInserterFactory(view, reader.result, files.createFileInfo(file)),
25700
+ detail: imageInserterFactory(view, base64Data, fileInfo),
25493
25701
  }));
25494
25702
  };
25495
25703
  reader.readAsDataURL(file);
@@ -25653,7 +25861,12 @@ class ImageView {
25653
25861
  this.img.alt = node.attrs.alt;
25654
25862
  applyImageStyles(this.img, node);
25655
25863
  this.img.addEventListener('load', () => {
25656
- this.persistDimensions();
25864
+ // Only measure a freshly inserted image. Re-measuring one that
25865
+ // already has a width would rewrite it against the current
25866
+ // container size and emit a spurious change event on every load.
25867
+ if (!this.node.attrs.width) {
25868
+ this.persistDimensions();
25869
+ }
25657
25870
  });
25658
25871
  this.dom.append(this.img);
25659
25872
  this.transitionBetweenStates();
@@ -27621,6 +27834,7 @@ const ProsemirrorAdapter = class {
27621
27834
  }
27622
27835
  componentWillLoad() {
27623
27836
  this.getActionBarItems();
27837
+ this.validatedInlineImages = this.validateInlineImages();
27624
27838
  this.setupContentConverter();
27625
27839
  }
27626
27840
  componentDidLoad() {
@@ -27646,7 +27860,7 @@ const ProsemirrorAdapter = class {
27646
27860
  (_e = this.view) === null || _e === void 0 ? void 0 : _e.destroy();
27647
27861
  }
27648
27862
  render() {
27649
- return (index$1.h(index$1.Host, { key: '46d850f4bcc3d64ba180276ec9080f2b5b38470e', onFocus: this.handleFocus }, index$1.h("div", { key: '7695f4a898726c5e2db5d18257dda0e2e1616ed7', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
27863
+ return (index$1.h(index$1.Host, { key: '27949dced1fd61d1a80c42f58cf1699f05acb52e', onFocus: this.handleFocus }, index$1.h("div", { key: '75e941459326f0fb8f8d5042becd570e06cd99fa', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
27650
27864
  }
27651
27865
  renderToolbar() {
27652
27866
  if (this.actionBarItems.length === 0 || this.ui === 'no-toolbar') {
@@ -27660,12 +27874,26 @@ const ProsemirrorAdapter = class {
27660
27874
  }
27661
27875
  return (index$1.h("limel-portal", { containerId: this.portalId, visible: this.isLinkMenuOpen, openDirection: "top", inheritParentWidth: true, anchor: this.actionBarElement }, index$1.h("limel-text-editor-link-menu", { link: this.link, isOpen: this.isLinkMenuOpen, onLinkChange: this.handleLinkChange, onCancel: this.handleCancelLinkMenu, onSave: this.handleSaveLinkMenu })));
27662
27876
  }
27877
+ validateInlineImages() {
27878
+ const config = this.inlineImages;
27879
+ // Require a hyphen: the tag name becomes a sanitizer whitelist key, so a
27880
+ // built-in like `img` would hijack its rules and strip `src` from every
27881
+ // real image. Custom element names always have a hyphen; built-ins never.
27882
+ const isValidTagName = (name) => /^[a-z][a-z0-9._-]*$/.test(name) && name.includes('-');
27883
+ if (config &&
27884
+ types.isInlineImageTag(config) &&
27885
+ !isValidTagName(config.tagName)) {
27886
+ console.error(`inlineImages.tagName "${config.tagName}" is not a valid custom element name; ignoring inlineImages config.`);
27887
+ return undefined;
27888
+ }
27889
+ return config;
27890
+ }
27663
27891
  setupContentConverter() {
27664
27892
  if (this.contentType === 'markdown') {
27665
- this.contentConverter = new MarkdownConverter(this.customElements, this.language);
27893
+ this.contentConverter = new MarkdownConverter(this.customElements, this.language, this.validatedInlineImages);
27666
27894
  }
27667
27895
  else if (this.contentType === 'html') {
27668
- this.contentConverter = new HTMLConverter(this.customElements);
27896
+ this.contentConverter = new HTMLConverter(this.customElements, this.validatedInlineImages);
27669
27897
  }
27670
27898
  else {
27671
27899
  throw new Error(`Unsupported content type: ${this.contentType}. Only 'markdown' and 'html' are supported.`);
@@ -27698,7 +27926,7 @@ const ProsemirrorAdapter = class {
27698
27926
  if (this.contentType === 'html') {
27699
27927
  nodes = nodes.append(getTableNodes());
27700
27928
  }
27701
- nodes = nodes.append(getImageNode(this.language));
27929
+ nodes = nodes.append(getImageNode(this.language, this.validatedInlineImages));
27702
27930
  return new Schema({
27703
27931
  nodes: nodes,
27704
27932
  marks: schema$1.spec.marks.append({
@@ -27726,7 +27954,7 @@ const ProsemirrorAdapter = class {
27726
27954
  keymap(this.menuCommandFactory.buildKeymap()),
27727
27955
  createTriggerPlugin(this.triggerCharacters, this.contentConverter),
27728
27956
  createLinkPlugin(this.handleNewLinkSelection),
27729
- createImageInserterPlugin(this.imagePasted.emit),
27957
+ createImageInserterPlugin(this.imagePasted.emit, this.validatedInlineImages),
27730
27958
  createImageViewPlugin(this.language),
27731
27959
  createMenuStateTrackingPlugin(types.editorMenuTypesArray, this.menuCommandFactory, this.updateActiveActionBarItems),
27732
27960
  createActionBarInteractionPlugin(this.menuCommandFactory),