Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions etc/lime-elements.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,28 @@ export interface DockMenu {
};
}

// @alpha (undocumented)
export interface EditorImage {
fileInfoId: string;
src: string;
state: EditorImageState;
}

// @alpha (undocumented)
export type EditorImageState = 'loading' | 'failed' | 'success';

// @alpha (undocumented)
export interface EditorLink {
href: string;
text: string;
}

// @alpha
export interface EditorMetadata {
images: EditorImage[];
links: EditorLink[];
}

// @beta
export type EditorTextLink = {
text?: string;
Expand Down Expand Up @@ -986,13 +1008,6 @@ interface Image_2 {
}
export { Image_2 as Image }

// @alpha (undocumented)
export interface ImageInfo {
fileInfoId: string;
src: string;
state: ImageState;
}

// @alpha (undocumented)
export interface ImageInserter {
// (undocumented)
Expand All @@ -1002,16 +1017,6 @@ export interface ImageInserter {
insertThumbnail: () => void;
}

// @alpha (undocumented)
export enum ImageState {
// (undocumented)
FAILED = "failed",
// (undocumented)
LOADING = "loading",
// (undocumented)
SUCCESS = "success"
}

// @public (undocumented)
export interface InfoTileProgress {
displayPercentageColors?: boolean;
Expand Down Expand Up @@ -1721,7 +1726,9 @@ export namespace JSX {
// @alpha
"onImagePasted"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInserter>) => void;
// @alpha
"onImageRemoved"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInfo>) => void;
"onImageRemoved"?: (event: LimelProsemirrorAdapterCustomEvent<EditorImage>) => void;
// @alpha
"onMetadataChange"?: (event: LimelProsemirrorAdapterCustomEvent<EditorMetadata>) => void;
// @alpha
"triggerCharacters"?: TriggerCharacter[];
"ui"?: EditorUiType;
Expand Down Expand Up @@ -1846,8 +1853,10 @@ export namespace JSX {
"onChange"?: (event: LimelTextEditorCustomEvent<string>) => void;
// @alpha
"onImagePasted"?: (event: LimelTextEditorCustomEvent<ImageInserter>) => void;
// @alpha @deprecated
"onImageRemoved"?: (event: LimelTextEditorCustomEvent<EditorImage>) => void;
// @alpha
"onImageRemoved"?: (event: LimelTextEditorCustomEvent<ImageInfo>) => void;
"onMetadataChange"?: (event: LimelTextEditorCustomEvent<EditorMetadata>) => void;
// @alpha
"onTriggerChange"?: (event: LimelTextEditorCustomEvent<TriggerEventDetail>) => void;
// @alpha
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Component, h, State, Host } from '@stencil/core';
import {
ImageInserter,
FileInfo,
ImageInfo,
EditorImage,
LimelTextEditorCustomEvent,
LimelCheckboxCustomEvent,
EditorMetadata,
} from '@limetech/lime-elements';
/**
* Handling inline images (with external file storage)
Expand Down Expand Up @@ -41,14 +42,16 @@ export class TextEditorWithInlineImagesExample {
@State()
private uploadImageFails = false;

private metadata: EditorMetadata = { links: [], images: [] };

public render() {
return (
<Host>
<limel-text-editor
value={this.value}
onChange={this.handleChange}
onImagePasted={this.handleImagePasted}
onImageRemoved={this.handleImageRemoved}
onMetadataChange={this.handleMetadataChange}
/>
<limel-checkbox
label="Upload image fails - insert failed thumbnail"
Expand Down Expand Up @@ -109,19 +112,38 @@ export class TextEditorWithInlineImagesExample {
}
};

private handleImageRemoved = (
event: LimelTextEditorCustomEvent<ImageInfo>,
private handleMetadataChange = (
event: LimelTextEditorCustomEvent<EditorMetadata>,
) => {
const imageInfo = event.detail;
console.log(`Image deleted: ${imageInfo.fileInfoId}`);
const removedImages = this.getRemovedImages(
this.metadata,
event.detail,
);

try {
throw new Error('Not implemented.');
} catch (error) {
console.error(
`Failed to delete image ${imageInfo.fileInfoId}`,
error,
);
}
removedImages.forEach((image) => {
if (image.state === 'success') {
this.removeImage(image);
}
});

this.metadata = event.detail;
};

private getRemovedImages(
oldMetadata: EditorMetadata,
newMetadata: EditorMetadata,
): EditorImage[] {
const newImageIds = new Set(
newMetadata.images.map((image) => image.fileInfoId),
);

return oldMetadata.images.filter(
(image) => !newImageIds.has(image.fileInfoId),
);
}

private removeImage(image: EditorImage) {
// Remove image from external file storage if desired.
console.log(`Image removed: ${image.fileInfoId}`);
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { Plugin, PluginKey, Transaction, StateField } from 'prosemirror-state';
import { Plugin, PluginKey } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { createFileInfo } from '../../../../../util/files';
import { FileInfo } from '../../../../../global/shared-types/file.types';
import {
ImageInserter,
ImageInfo,
ImageState,
} from '../../../text-editor.types';
import { ImageInserter, EditorImageState } from '../../../text-editor.types';
import { Node, Slice, Fragment } from 'prosemirror-model';
import { imageCache } from './node';
import { ImageNodeAttrs } from './node';

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

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

type ImageRemovedCallback = (data: ImageInfo) => CustomEvent<ImageInfo>;

type PluginState = {
insertedImages: Record<string, Node>;
};

export const createImageInserterPlugin = (
imagePastedCallback: ImagePastedCallback,
imageRemovedCallback: ImageRemovedCallback,
) => {
return new Plugin({
key: pluginKey,
Expand All @@ -36,59 +25,9 @@ export const createImageInserterPlugin = (
},
},
},
state: {
init: (): PluginState => {
return { insertedImages: {} };
},
apply: (tr, pluginState): PluginState => {
const newState = { ...pluginState };

newState.insertedImages = getImagesFromTransaction(tr);
findAndHandleRemovedImages(
imageRemovedCallback,
pluginState.insertedImages,
newState.insertedImages,
);

return newState;
},
} as StateField<PluginState>,
});
};

const getImagesFromTransaction = (tr: Transaction): Record<string, Node> => {
const images: Record<string, Node> = {};
tr.doc.descendants((node) => {
if (node.type.name === 'image') {
images[node.attrs.fileInfoId] = node;
}
});

return images;
};

const findAndHandleRemovedImages = (
imageRemovedCallback: ImageRemovedCallback,
previousImages: Record<string, Node>,
newImages: Record<string, Node>,
) => {
const removedKeys = Object.keys(previousImages).filter(
(key) => !(key in newImages),
);

for (const removedKey of removedKeys) {
const removedImage = previousImages[removedKey];
const imageInfo: ImageInfo = {
fileInfoId: removedImage.attrs.fileInfoId,
src: removedImage.attrs.src,
state: removedImage.attrs.state,
};
imageRemovedCallback(imageInfo);

imageCache.delete(removedImage.attrs.fileInfoId);
}
};

export const imageInserterFactory = (
view: EditorView,
base64Data: string,
Expand All @@ -107,12 +46,12 @@ const createThumbnailInserter =
const { state, dispatch } = view;
const { schema } = state;

const placeholderNode = schema.nodes.image.create({
src: base64Data,
alt: fileInfo.filename,
fileInfoId: fileInfo.id,
state: ImageState.LOADING,
});
const imageNodeAttrs = createImageNodeAttrs(
base64Data,
fileInfo,
'loading',
);
const placeholderNode = schema.nodes.image.create(imageNodeAttrs);

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

Expand All @@ -127,12 +66,12 @@ const createImageInserter =
const tr = state.tr;
state.doc.descendants((node, pos) => {
if (node.attrs.fileInfoId === fileInfo.id) {
const imageNode = schema.nodes.image.create({
src: src ? src : node.attrs.src,
alt: fileInfo.filename,
fileInfoId: fileInfo.id,
state: ImageState.SUCCESS,
});
const imageNodeAttrs = createImageNodeAttrs(
src ? src : node.attrs.src,
fileInfo,
'success',
);
const imageNode = schema.nodes.image.create(imageNodeAttrs);

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

Expand All @@ -151,12 +90,13 @@ const createFailedThumbnailInserter =
const tr = state.tr;
state.doc.descendants((node, pos) => {
if (node.attrs.fileInfoId === fileInfo.id) {
const errorPlaceholderNode = schema.nodes.image.create({
src: node.attrs.src,
alt: fileInfo.filename,
fileInfoId: fileInfo.id,
state: ImageState.FAILED,
});
const imageNodeAttrs = createImageNodeAttrs(
node.attrs.src,
fileInfo,
'failed',
);
const errorPlaceholderNode =
schema.nodes.image.create(imageNodeAttrs);

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

Expand All @@ -167,6 +107,19 @@ const createFailedThumbnailInserter =
dispatch(tr);
};

function createImageNodeAttrs(
src: string,
fileInfo: FileInfo,
state: EditorImageState,
): ImageNodeAttrs {
return {
src: src,
alt: fileInfo.filename,
fileInfoId: fileInfo.id,
state: state,
};
}

/**
* Check if a given ProseMirror node or fragment contains any image nodes.
* @param node - The ProseMirror node or fragment to check.
Expand Down
Loading