diff --git a/src/index.ts b/src/index.ts index ad9c76d6..80303aae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,43 +31,49 @@ */ import type { TunesMenuConfig } from "@editorjs/editorjs/types/tools"; -import type { API, ToolboxConfig, PasteConfig, BlockToolConstructorOptions, BlockTool } from '@editorjs/editorjs'; +import type { API, ToolboxConfig, PasteConfig, BlockToolConstructorOptions, BlockTool, BlockAPI } from '@editorjs/editorjs'; import './index.css'; import Ui from './ui'; import Uploader from './uploader'; import { IconAddBorder, IconStretch, IconAddBackground, IconPicture } from '@codexteam/icons'; -import { ActionConfig, UploadResponseFormat, ImageToolData, ImageToolConfig } from './types/types'; +import { ActionConfig, UploadResponseFormat, ImageToolData, ImageConfig } from './types/types'; -type ImageToolConstructorOptions = BlockToolConstructorOptions +type ImageToolConstructorOptions = BlockToolConstructorOptions export default class ImageTool implements BlockTool { /** * Editor.js API instance */ private api: API; + /** * Flag indicating read-only mode */ private readOnly: boolean; + /** * Current Block API instance */ - private block: any; + private block: BlockAPI; + /** * Configuration for the ImageTool */ - private config: ImageToolConfig; + private config: ImageConfig; + /** * Uploader module instance */ private uploader: Uploader; + /** * UI module instance */ private ui: Ui; + /** * Stores current block data internally */ @@ -84,21 +90,21 @@ export default class ImageTool implements BlockTool { constructor({ data, config, api, readOnly, block }: ImageToolConstructorOptions) { this.api = api; this.readOnly = readOnly; - this.block = block; + this.block = block ? block : {} as BlockAPI; /** * Tool's initial config */ this.config = { - endpoints: config? config.endpoints : {}, - additionalRequestData: config? config.additionalRequestData: {}, - additionalRequestHeaders: config? config.additionalRequestHeaders: {}, - field: config? config.field: 'image', - types: config? config.types: 'image/*', - captionPlaceholder: this.api.i18n.t(config && config.captionPlaceholder? config.captionPlaceholder: 'Caption'), - buttonContent: config? config.buttonContent : '', - uploader: config? config.uploader : undefined, - actions: config? config.actions: [], + endpoints: config ? config.endpoints : {}, + additionalRequestData: config ? config.additionalRequestData: {}, + additionalRequestHeaders: config ? config.additionalRequestHeaders: {}, + field: config ? config.field: 'image', + types: config ? config.types: 'image/*', + captionPlaceholder: this.api.i18n.t(config && config.captionPlaceholder ? config.captionPlaceholder: 'Caption'), + buttonContent: config ? config.buttonContent : '', + uploader: config ? config.uploader : undefined, + actions: config ? config.actions: [], }; /** @@ -233,9 +239,9 @@ export default class ImageTool implements BlockTool { * * @public * - * @returns HTMLElement | TunesMenuConfig + * @returns TunesMenuConfig */ - renderSettings(): HTMLElement | TunesMenuConfig { + renderSettings(): TunesMenuConfig { // Merge default tunes with the ones that might be added by user // @see https://github.com/editor-js/image/pull/49 const tunes = ImageTool.tunes.concat(this.config.actions || []); @@ -253,7 +259,7 @@ export default class ImageTool implements BlockTool { return; } - this.tuneToggled(tune.name); + this.tuneToggled(tune.name as keyof ImageToolData); }, })); } @@ -363,7 +369,7 @@ export default class ImageTool implements BlockTool { ImageTool.tunes.forEach(({ name: tune }) => { const value = typeof data[tune as keyof ImageToolData] !== 'undefined' ? data[tune as keyof ImageToolData] === true || data[tune as keyof ImageToolData] === 'true' : false; - this.setTune(tune, value); + this.setTune(tune as keyof ImageToolData, value); }); } @@ -375,7 +381,7 @@ export default class ImageTool implements BlockTool { * @returns {ImageToolData} */ get data(): ImageToolData { - return this._data as ImageToolData; + return this._data; } /** @@ -385,8 +391,8 @@ export default class ImageTool implements BlockTool { * * @param {object} file - uploaded file data */ - set image(file:{ url: string }) { - this._data.file = file || {}; + set image(file: { url: string } | undefined) { + this._data.file = file || {url: ''}; if (file && file.url) { this.ui.fillImage(file.url); @@ -401,7 +407,7 @@ export default class ImageTool implements BlockTool { * @param {UploadResponseFormat} response - uploading server response * @returns {void} */ - onUpload(response: UploadResponseFormat):void { + onUpload(response: UploadResponseFormat): void { if (response.success && response.file) { this.image = response.file; } else { @@ -434,7 +440,7 @@ export default class ImageTool implements BlockTool { * @param {string} tuneName - tune that has been clicked * @returns {void} */ - tuneToggled(tuneName: string): void { + tuneToggled(tuneName: keyof ImageToolData): void { // inverse tune state this.setTune(tuneName, !this._data[tuneName as keyof ImageToolData]); } @@ -446,8 +452,8 @@ export default class ImageTool implements BlockTool { * @param {boolean} value - tune state * @returns {void} */ - setTune(tuneName: string, value: boolean): void { - (this._data[tuneName as keyof ImageToolData] as boolean) = value; + setTune(tuneName: keyof ImageToolData, value: boolean): void { + (this._data[tuneName] as boolean) = value; this.ui.applyTune(tuneName, value); if (tuneName === 'stretched') { diff --git a/src/types/types.ts b/src/types/types.ts index 4fe144fc..90e8de81 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,7 +8,32 @@ export interface UploadOptions { /** * User configuration of Image block tunes. Allows to add custom tunes through the config */ -export interface ActionConfig { name: string; icon: string; title: string; toggle: boolean, action?: Function }; +export interface ActionConfig { + /** + * The name of the tune. + */ + name: string; + + /** + * The icon for the tune. Should be an SVG string. + */ + icon: string; + + /** + * The title of the tune. This will be displayed in the UI. + */ + title: string; + + /** + * A flag indicating whether the tune is a toggle (true) or not (false). + */ + toggle: boolean; + + /** + * An optional action function to be executed when the tune is activated. + */ + action?: Function; +}; /** * UploadResponseFormat interface representing the response format expected from the backend on file uploading. @@ -18,6 +43,7 @@ export interface UploadResponseFormat { * success - 1 for successful uploading, 0 for failure */ success: number; + /** * Object with file data. * 'url' is required, @@ -39,81 +65,102 @@ export interface ImageToolData { * Caption for the image. */ caption: string; + /** * Flag indicating whether the image has a border. */ withBorder: boolean; + /** * Flag indicating whether the image has a background. */ withBackground: boolean; + /** * Flag indicating whether the image is stretched. */ stretched: boolean; + /** * Object containing the URL of the image file. */ file: { url: string; }; + + /** + * Additional actions for the tool. + */ + actions?: ActionConfig[]; } /** * * @description Config supported by Tool */ -export interface ImageToolConfig { +export interface ImageConfig { /** * Endpoints for upload, whether using file or URL. */ endpoints: { + /** * Endpoint for file upload. */ byFile?: string; + /** * Endpoints for URL upload. */ byUrl?: string; }; + /** * Field name for the uploaded image. */ field?: string; + /** * Allowed mime-types for the uploaded image. */ types?: string; + /** * Placeholder text for the caption field. */ captionPlaceholder?: string; + /** * Additional data to send with requests. */ additionalRequestData?: object; + /** * Additional headers to send with requests. */ additionalRequestHeaders?: object; + /** * Custom content for the select file button. */ buttonContent?: string; + /** * Optional custom uploader. */ uploader?: { + /** * Method to upload an image by file. */ uploadByFile?: (file: Blob) => Promise; + /** * Method to upload an image by URL. */ uploadByUrl?: (url: string) => Promise; }; + /** * Additional actions for the tool. */ diff --git a/src/ui.ts b/src/ui.ts index 69c2429b..76da17a4 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,11 +1,25 @@ import { IconPicture } from '@codexteam/icons'; import { make } from './utils/dom'; import type { API } from '@editorjs/editorjs'; -import { ImageToolData, ImageToolConfig } from './types/types'; +import { ImageToolData, ImageConfig } from './types/types'; +/** + * Enumeration representing the different states of the UI. + */ enum UiState { + /** + * The UI is in an empty state, with no image loaded or being uploaded. + */ Empty = "EMPTY", + + /** + * The UI is in an uploading state, indicating an image is currently being uploaded. + */ Uploading = "UPLOADING", + + /** + * The UI is in a filled state, with an image successfully loaded. + */ Filled = "FILLED" }; @@ -17,22 +31,27 @@ interface Nodes { * Wrapper element in the UI. */ wrapper: HTMLElement; + /** * Container for the image element in the UI. */ imageContainer: HTMLElement; + /** * Button for selecting files. */ fileButton: HTMLElement; + /** * Represents the image element in the UI, if one is present; otherwise, it's undefined. */ imageEl?: HTMLElement; + /** * Preloader element for the image. */ imagePreloader: HTMLElement; + /** * Caption element for the image. */ @@ -50,7 +69,7 @@ interface ConstructorParams { /** * Configuration for the image. */ - config: ImageToolConfig; + config: ImageConfig; /** * Callback function for selecting a file. */ @@ -76,7 +95,7 @@ private api: API; /** * Configuration for the image tool. */ -private config: ImageToolConfig; +private config: ImageConfig; /** * Callback function for selecting a file. @@ -95,7 +114,7 @@ public nodes: Nodes; /** * @param {object} ui - image tool Ui module * @param {object} ui.api - Editor.js API - * @param {ImageToolConfig} ui.config - user config + * @param {ImageConfig} ui.config - user config * @param {Function} ui.onSelectFile - callback for clicks on Select file button * @param {boolean} ui.readOnly - read-only mode flag */ diff --git a/src/uploader.ts b/src/uploader.ts index d8a7472b..781f8c69 100644 --- a/src/uploader.ts +++ b/src/uploader.ts @@ -1,7 +1,7 @@ import ajax from '@codexteam/ajax'; import isPromise from './utils/isPromise'; import { UploadOptions } from './types/types'; -import { UploadResponseFormat, ImageToolConfig } from './types/types'; +import { UploadResponseFormat, ImageConfig } from './types/types'; /** * Params interface for Uploader constructor @@ -10,7 +10,7 @@ interface UploaderParams { /** * Configuration for the uploader */ - config: ImageToolConfig; + config: ImageConfig; /** * * @param response: Callback function for successful upload @@ -32,12 +32,12 @@ interface UploaderParams { * 3. Upload by pasting file from Clipboard or by Drag'n'Drop */ export default class Uploader { - private config: ImageToolConfig; + private config: ImageConfig; private onUpload: (response: UploadResponseFormat) => void; private onError: (error: any) => void; /** * @param {object} params - uploader module params - * @param {ImageToolConfig} params.config - image tool config + * @param {ImageConfig} params.config - image tool config * @param {Function} params.onUpload - one callback for all uploading (file, url, d-n-d, pasting) * @param {Function} params.onError - callback for uploading errors */ @@ -71,10 +71,11 @@ export default class Uploader { // custom uploading if (this.config.uploader && typeof this.config.uploader.uploadByFile === 'function') { + const uploadByFile = this.config.uploader.uploadByFile; upload = ajax.selectFiles({ accept: this.config.types || ''}).then((files: File[]) => { preparePreview(files[0]); - const customUpload = this.config.uploader && this.config.uploader.uploadByFile && this.config.uploader.uploadByFile(files[0]); + const customUpload = uploadByFile(files[0]); if (!isPromise(customUpload)) { console.warn('Custom uploader method uploadByFile should return a Promise'); @@ -84,9 +85,9 @@ export default class Uploader { }); // default uploading - } else { + } else if (this.config.endpoints.byFile) { upload = ajax.transport({ - url: this.config.endpoints.byFile!, + url: this.config.endpoints.byFile, data: this.config.additionalRequestData, accept: this.config.types, headers: this.config.additionalRequestHeaders as Record, @@ -95,6 +96,9 @@ export default class Uploader { }, fieldName: this.config.field, }).then((response: any) => response.body); + } else { + this.onError('No valid upload configuration provided.'); + return ; } upload.then((response) => { @@ -174,7 +178,7 @@ export default class Uploader { if (!isPromise(upload)) { console.warn('Custom uploader method uploadByFile should return a Promise'); } - } else { + } else if (this.config.endpoints.byFile) { /** * Default uploading */ @@ -189,11 +193,14 @@ export default class Uploader { } upload = ajax.post({ - url: this.config.endpoints.byFile!, + url: this.config.endpoints.byFile, data: formData, type: ajax.contentType.JSON, headers: this.config.additionalRequestHeaders as Record, }).then((response: any) => response.body); + } else { + this.onError('No valid upload configuration provided.'); + return ; } upload.then((response) => {