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