Skip to content

Commit d811891

Browse files
FredrikWallstromBefkadu1
authored andcommitted
feat(text-editor): add metadata event for notifying about metadata changes (images and links)
Deprecated the `imageRemoved`. Use the `metadataChange` event instead to track image removals.
1 parent d6f11f3 commit d811891

File tree

11 files changed

+764
-205
lines changed

11 files changed

+764
-205
lines changed

etc/lime-elements.api.md

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,28 @@ export interface DockMenu {
820820
};
821821
}
822822

823+
// @alpha (undocumented)
824+
export interface EditorImage {
825+
fileInfoId: string;
826+
src: string;
827+
state: EditorImageState;
828+
}
829+
830+
// @alpha (undocumented)
831+
export type EditorImageState = 'loading' | 'failed' | 'success';
832+
833+
// @alpha (undocumented)
834+
export interface EditorLink {
835+
href: string;
836+
text: string;
837+
}
838+
839+
// @alpha
840+
export interface EditorMetadata {
841+
images: EditorImage[];
842+
links: EditorLink[];
843+
}
844+
823845
// @beta
824846
export type EditorTextLink = {
825847
text?: string;
@@ -986,13 +1008,6 @@ interface Image_2 {
9861008
}
9871009
export { Image_2 as Image }
9881010

989-
// @alpha (undocumented)
990-
export interface ImageInfo {
991-
fileInfoId: string;
992-
src: string;
993-
state: ImageState;
994-
}
995-
9961011
// @alpha (undocumented)
9971012
export interface ImageInserter {
9981013
// (undocumented)
@@ -1002,16 +1017,6 @@ export interface ImageInserter {
10021017
insertThumbnail: () => void;
10031018
}
10041019

1005-
// @alpha (undocumented)
1006-
export enum ImageState {
1007-
// (undocumented)
1008-
FAILED = "failed",
1009-
// (undocumented)
1010-
LOADING = "loading",
1011-
// (undocumented)
1012-
SUCCESS = "success"
1013-
}
1014-
10151020
// @public (undocumented)
10161021
export interface InfoTileProgress {
10171022
displayPercentageColors?: boolean;
@@ -1721,7 +1726,9 @@ export namespace JSX {
17211726
// @alpha
17221727
"onImagePasted"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInserter>) => void;
17231728
// @alpha
1724-
"onImageRemoved"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInfo>) => void;
1729+
"onImageRemoved"?: (event: LimelProsemirrorAdapterCustomEvent<EditorImage>) => void;
1730+
// @alpha
1731+
"onMetadataChange"?: (event: LimelProsemirrorAdapterCustomEvent<EditorMetadata>) => void;
17251732
// @alpha
17261733
"triggerCharacters"?: TriggerCharacter[];
17271734
"ui"?: EditorUiType;
@@ -1846,8 +1853,10 @@ export namespace JSX {
18461853
"onChange"?: (event: LimelTextEditorCustomEvent<string>) => void;
18471854
// @alpha
18481855
"onImagePasted"?: (event: LimelTextEditorCustomEvent<ImageInserter>) => void;
1856+
// @alpha @deprecated
1857+
"onImageRemoved"?: (event: LimelTextEditorCustomEvent<EditorImage>) => void;
18491858
// @alpha
1850-
"onImageRemoved"?: (event: LimelTextEditorCustomEvent<ImageInfo>) => void;
1859+
"onMetadataChange"?: (event: LimelTextEditorCustomEvent<EditorMetadata>) => void;
18511860
// @alpha
18521861
"onTriggerChange"?: (event: LimelTextEditorCustomEvent<TriggerEventDetail>) => void;
18531862
// @alpha

src/components/text-editor/examples/text-editor-with-inline-images-file-storage.tsx

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Component, h, State, Host } from '@stencil/core';
22
import {
33
ImageInserter,
44
FileInfo,
5-
ImageInfo,
5+
EditorImage,
66
LimelTextEditorCustomEvent,
77
LimelCheckboxCustomEvent,
8+
EditorMetadata,
89
} from '@limetech/lime-elements';
910
/**
1011
* Handling inline images (with external file storage)
@@ -41,14 +42,16 @@ export class TextEditorWithInlineImagesExample {
4142
@State()
4243
private uploadImageFails = false;
4344

45+
private metadata: EditorMetadata = { links: [], images: [] };
46+
4447
public render() {
4548
return (
4649
<Host>
4750
<limel-text-editor
4851
value={this.value}
4952
onChange={this.handleChange}
5053
onImagePasted={this.handleImagePasted}
51-
onImageRemoved={this.handleImageRemoved}
54+
onMetadataChange={this.handleMetadataChange}
5255
/>
5356
<limel-checkbox
5457
label="Upload image fails - insert failed thumbnail"
@@ -109,19 +112,38 @@ export class TextEditorWithInlineImagesExample {
109112
}
110113
};
111114

112-
private handleImageRemoved = (
113-
event: LimelTextEditorCustomEvent<ImageInfo>,
115+
private handleMetadataChange = (
116+
event: LimelTextEditorCustomEvent<EditorMetadata>,
114117
) => {
115-
const imageInfo = event.detail;
116-
console.log(`Image deleted: ${imageInfo.fileInfoId}`);
118+
const removedImages = this.getRemovedImages(
119+
this.metadata,
120+
event.detail,
121+
);
117122

118-
try {
119-
throw new Error('Not implemented.');
120-
} catch (error) {
121-
console.error(
122-
`Failed to delete image ${imageInfo.fileInfoId}`,
123-
error,
124-
);
125-
}
123+
removedImages.forEach((image) => {
124+
if (image.state === 'success') {
125+
this.removeImage(image);
126+
}
127+
});
128+
129+
this.metadata = event.detail;
126130
};
131+
132+
private getRemovedImages(
133+
oldMetadata: EditorMetadata,
134+
newMetadata: EditorMetadata,
135+
): EditorImage[] {
136+
const newImageIds = new Set(
137+
newMetadata.images.map((image) => image.fileInfoId),
138+
);
139+
140+
return oldMetadata.images.filter(
141+
(image) => !newImageIds.has(image.fileInfoId),
142+
);
143+
}
144+
145+
private removeImage(image: EditorImage) {
146+
// Remove image from external file storage if desired.
147+
console.log(`Image removed: ${image.fileInfoId}`);
148+
}
127149
}

src/components/text-editor/prosemirror-adapter/plugins/image/inserter.ts

Lines changed: 35 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1-
import { Plugin, PluginKey, Transaction, StateField } from 'prosemirror-state';
1+
import { Plugin, PluginKey } from 'prosemirror-state';
22
import { EditorView } from 'prosemirror-view';
33
import { createFileInfo } from '../../../../../util/files';
44
import { FileInfo } from '../../../../../global/shared-types/file.types';
5-
import {
6-
ImageInserter,
7-
ImageInfo,
8-
ImageState,
9-
} from '../../../text-editor.types';
5+
import { ImageInserter, EditorImageState } from '../../../text-editor.types';
106
import { Node, Slice, Fragment } from 'prosemirror-model';
11-
import { imageCache } from './node';
7+
import { ImageNodeAttrs } from './node';
128

139
export const pluginKey = new PluginKey('imageInserterPlugin');
1410

1511
type ImagePastedCallback = (data: ImageInserter) => CustomEvent<ImageInserter>;
1612

17-
type ImageRemovedCallback = (data: ImageInfo) => CustomEvent<ImageInfo>;
18-
19-
type PluginState = {
20-
insertedImages: Record<string, Node>;
21-
};
22-
2313
export const createImageInserterPlugin = (
2414
imagePastedCallback: ImagePastedCallback,
25-
imageRemovedCallback: ImageRemovedCallback,
2615
) => {
2716
return new Plugin({
2817
key: pluginKey,
@@ -36,59 +25,9 @@ export const createImageInserterPlugin = (
3625
},
3726
},
3827
},
39-
state: {
40-
init: (): PluginState => {
41-
return { insertedImages: {} };
42-
},
43-
apply: (tr, pluginState): PluginState => {
44-
const newState = { ...pluginState };
45-
46-
newState.insertedImages = getImagesFromTransaction(tr);
47-
findAndHandleRemovedImages(
48-
imageRemovedCallback,
49-
pluginState.insertedImages,
50-
newState.insertedImages,
51-
);
52-
53-
return newState;
54-
},
55-
} as StateField<PluginState>,
5628
});
5729
};
5830

59-
const getImagesFromTransaction = (tr: Transaction): Record<string, Node> => {
60-
const images: Record<string, Node> = {};
61-
tr.doc.descendants((node) => {
62-
if (node.type.name === 'image') {
63-
images[node.attrs.fileInfoId] = node;
64-
}
65-
});
66-
67-
return images;
68-
};
69-
70-
const findAndHandleRemovedImages = (
71-
imageRemovedCallback: ImageRemovedCallback,
72-
previousImages: Record<string, Node>,
73-
newImages: Record<string, Node>,
74-
) => {
75-
const removedKeys = Object.keys(previousImages).filter(
76-
(key) => !(key in newImages),
77-
);
78-
79-
for (const removedKey of removedKeys) {
80-
const removedImage = previousImages[removedKey];
81-
const imageInfo: ImageInfo = {
82-
fileInfoId: removedImage.attrs.fileInfoId,
83-
src: removedImage.attrs.src,
84-
state: removedImage.attrs.state,
85-
};
86-
imageRemovedCallback(imageInfo);
87-
88-
imageCache.delete(removedImage.attrs.fileInfoId);
89-
}
90-
};
91-
9231
export const imageInserterFactory = (
9332
view: EditorView,
9433
base64Data: string,
@@ -107,12 +46,12 @@ const createThumbnailInserter =
10746
const { state, dispatch } = view;
10847
const { schema } = state;
10948

110-
const placeholderNode = schema.nodes.image.create({
111-
src: base64Data,
112-
alt: fileInfo.filename,
113-
fileInfoId: fileInfo.id,
114-
state: ImageState.LOADING,
115-
});
49+
const imageNodeAttrs = createImageNodeAttrs(
50+
base64Data,
51+
fileInfo,
52+
'loading',
53+
);
54+
const placeholderNode = schema.nodes.image.create(imageNodeAttrs);
11655

11756
const transaction = state.tr.replaceSelectionWith(placeholderNode);
11857

@@ -127,12 +66,12 @@ const createImageInserter =
12766
const tr = state.tr;
12867
state.doc.descendants((node, pos) => {
12968
if (node.attrs.fileInfoId === fileInfo.id) {
130-
const imageNode = schema.nodes.image.create({
131-
src: src ? src : node.attrs.src,
132-
alt: fileInfo.filename,
133-
fileInfoId: fileInfo.id,
134-
state: ImageState.SUCCESS,
135-
});
69+
const imageNodeAttrs = createImageNodeAttrs(
70+
src ? src : node.attrs.src,
71+
fileInfo,
72+
'success',
73+
);
74+
const imageNode = schema.nodes.image.create(imageNodeAttrs);
13675

13776
tr.replaceWith(pos, pos + node.nodeSize, imageNode);
13877

@@ -151,12 +90,13 @@ const createFailedThumbnailInserter =
15190
const tr = state.tr;
15291
state.doc.descendants((node, pos) => {
15392
if (node.attrs.fileInfoId === fileInfo.id) {
154-
const errorPlaceholderNode = schema.nodes.image.create({
155-
src: node.attrs.src,
156-
alt: fileInfo.filename,
157-
fileInfoId: fileInfo.id,
158-
state: ImageState.FAILED,
159-
});
93+
const imageNodeAttrs = createImageNodeAttrs(
94+
node.attrs.src,
95+
fileInfo,
96+
'failed',
97+
);
98+
const errorPlaceholderNode =
99+
schema.nodes.image.create(imageNodeAttrs);
160100

161101
tr.replaceWith(pos, pos + node.nodeSize, errorPlaceholderNode);
162102

@@ -167,6 +107,19 @@ const createFailedThumbnailInserter =
167107
dispatch(tr);
168108
};
169109

110+
function createImageNodeAttrs(
111+
src: string,
112+
fileInfo: FileInfo,
113+
state: EditorImageState,
114+
): ImageNodeAttrs {
115+
return {
116+
src: src,
117+
alt: fileInfo.filename,
118+
fileInfoId: fileInfo.id,
119+
state: state,
120+
};
121+
}
122+
170123
/**
171124
* Check if a given ProseMirror node or fragment contains any image nodes.
172125
* @param node - The ProseMirror node or fragment to check.

0 commit comments

Comments
 (0)