import type { Component, Plugin } from 'grapesjs'; import type tuiImageEditor from 'tui-image-editor'; type ImageEditor = tuiImageEditor.ImageEditor; type IOptions = tuiImageEditor.IOptions; type Constructor = { new(...any: any): K }; export type PluginOptions = { /** * TOAST UI's configurations * https://nhn.github.io/tui.image-editor/latest/ImageEditor */ config?: IOptions; /** * Pass the editor constructor. * By default, the `tui.ImageEditor` will be used. */ constructor?: any; /** * Label for the image editor (used in the modal) * @default 'Image Editor' */ labelImageEditor?: string; /** * Label used on the apply button * @default 'Apply' */ labelApply?: string; /** * Default editor height * @default '650px' */ height?: string; /** * Default editor width * @default '100%' */ width?: string; /** * Id to use to create the image editor command * @default 'tui-image-editor' */ commandId?: string; /** * Icon used in the image component toolbar. Pass an empty string to avoid adding the icon. */ toolbarIcon?: string; /** * Hide the default editor header * @default true */ hideHeader?: boolean; /** * By default, GrapesJS takes the modified image, adds it to the Asset Manager and update the target. * If you need some custom logic you can use this custom 'onApply' function. * @example * onApply: (imageEditor, imageModel) => { * const dataUrl = imageEditor.toDataURL(); * editor.AssetManager.add({ src: dataUrl }); // Add it to Assets * imageModel.set('src', dataUrl); // Update the image component * } */ onApply?: ((imageEditor: ImageEditor, imageModel: Component) => void) | null; /** * If no custom `onApply` is passed and this option is `true`, the result image will be added to assets * @default true */ addToAssets?: boolean; /** * If no custom `onApply` is passed, on confirm, the edited image, will be passed to the * AssetManager's uploader and the result (eg. instead of having the dataURL you'll have the URL) * will be passed to the default `onApply` process (update target, etc.) */ upload?: boolean; /** * The apply button (HTMLElement) will be passed as an argument to this function, once created. * This will allow you a higher customization. */ onApplyButton?: (btn: HTMLElement) => void; /** * Scripts to load dynamically in case no TOAST UI editor instance was found */ script?: string[]; /** * In case the script is loaded this style will be loaded too */ style?: string[]; }; const plugin: Plugin = (editor, options = {}) => { const opts: Required = { config: {}, constructor: '', labelImageEditor: 'Image Editor', labelApply: 'Apply', height: '650px', width: '100%', commandId: 'tui-image-editor', toolbarIcon: ` `, hideHeader: true, addToAssets: true, upload: false, onApplyButton: () => {}, onApply: null, script: [ 'https://uicdn.toast.com/tui.code-snippet/v1.5.2/tui-code-snippet.min.js', 'https://uicdn.toast.com/tui-color-picker/v2.2.7/tui-color-picker.min.js', 'https://uicdn.toast.com/tui-image-editor/v3.15.2/tui-image-editor.min.js' ], style: [ 'https://uicdn.toast.com/tui-color-picker/v2.2.7/tui-color-picker.min.css', 'https://uicdn.toast.com/tui-image-editor/v3.15.2/tui-image-editor.min.css', ], ...options, }; const { script, style, height, width, hideHeader, onApply, upload, addToAssets, commandId } = opts; const hasWindow = typeof window !== 'undefined'; const getConstructor = (): Constructor => { return opts.constructor || (hasWindow && (window as any).tui?.ImageEditor); }; let constr = getConstructor(); // Dynamic loading of the image editor scripts and styles if (!constr && script?.length && hasWindow) { const { head } = document; const scripts = Array.isArray(script) ? [...script] : [script]; const styles = (Array.isArray(style) ? [...style] : [style]) as string[]; const appendStyle = (styles: string[]) => { if (styles.length) { const link = document.createElement('link'); link.href = styles.shift()!; link.rel = 'stylesheet'; head.appendChild(link); appendStyle(styles); } } const appendScript = (scripts: string[]) => { if (scripts.length) { const scr = document.createElement('script'); scr.src = scripts.shift()!; scr.onerror = scr.onload = appendScript.bind(null, scripts); head.appendChild(scr); } else { constr = getConstructor(); } } appendStyle(styles); appendScript(scripts); } // Update image component toolbar if (opts.toolbarIcon) { editor.Components.addType('image', { extendFn: ['initToolbar'], model: { initToolbar() { const tb = this.get('toolbar'); const tbExists = tb?.some(item => item.command === commandId); if (!tbExists) { tb?.unshift({ command: commandId, label: opts.toolbarIcon, }); this.set('toolbar', tb); } } } }); } // Add the image editor command const errorOpts = { level: 'error', ns: commandId }; editor.Commands.add(commandId, { imageEditor: null as tuiImageEditor | null, run(ed, s, options = {}) { if (!constr) { ed.log('TOAST UI Image editor not found', errorOpts); return ed.stopCommand(commandId); } const target = (options.target || ed.getSelected()) as Component; if (!target) { ed.log('Target not available', errorOpts); return ed.stopCommand(commandId); } const content = this.createContent(); const title = opts.labelImageEditor; const btn = content.children[1] as HTMLElement; ed.Modal.open({ title, content }).onceClose(() => ed.stopCommand(commandId)) const editorConfig = this.getEditorConfig(target.get('src')); this.imageEditor = new constr(content.children[0], editorConfig); ed.getModel().setEditing(true); btn.onclick = () => this.applyChanges(target); opts.onApplyButton(btn); }, stop(ed) { (this.imageEditor as tuiImageEditor)?.destroy(); ed.getModel().setEditing(false); }, getEditorConfig(path: string): IOptions { const config: IOptions = { ...opts.config }; if (!config.includeUI) config.includeUI = {}; config.includeUI = { theme: {}, ...config.includeUI, loadImage: { path, name: '1' }, uiSize: { height, width }, }; if (hideHeader) { // @ts-ignore config.includeUI.theme['header.display'] = 'none'; } return config; }, createContent(): HTMLDivElement { const content = document.createElement('div'); content.style.position = 'relative'; content.innerHTML = `