├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | node_modules/ 4 | .eslintrc 5 | .npmrc 6 | *.log 7 | _index.html 8 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-current, Artur Arseniev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "GrapesJS" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS TOAST UI Image Editor 2 | 3 | Add the [TOAST UI Image Editor](https://ui.toast.com/tui-image-editor/) on Image Components in GrapesJS 4 | 5 | [Demo](http://grapesjs.com/demo.html) 6 | 7 | ![Preview](https://user-images.githubusercontent.com/11614725/52981724-c195f800-33e1-11e9-98a9-f071a2721761.png) 8 | 9 | 10 | ## Summary 11 | 12 | * Plugin name: `grapesjs-tui-image-editor` 13 | * Commands 14 | * `tui-image-editor` - Open the modal with the image editor. 15 | Options: 16 | * `target` - component from which to get and update the image 17 | 18 | 19 | 20 | 21 | 22 | ## Options 23 | 24 | |Option|Description|Default| 25 | |-|-|- 26 | |`config`|TOAST UI's configuration [object](http://nhnent.github.io/tui.image-editor/latest/ImageEditor.html)|`{}`| 27 | |`constructor`|Pass the editor constructor. By default, the `tui.ImageEditor` will be called|``| 28 | |`labelImageEditor`|Label for the image editor (used in the modal)|`Image Editor`| 29 | |`labelApply`|Label used on the apply button|`Apply`| 30 | |`height`|Default editor height|`650px`| 31 | |`width`|Default editor width|`100%`| 32 | |`commandId`|Id to use to create the image editor command|`tui-image-editor`| 33 | |`toolbarIcon`|Icon used in the image component toolbar. Pass an empty string to avoid adding the icon.|` {...}`|`null`| 36 | |`addToAssets`|If no custom `onApply` is passed and this option is `true`, the result image will be added to assets|`true`| 37 | |`upload`|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.)|`false`| 38 | |`onApplyButton`|The apply button (HTMLElement) will be passed as an argument to this function, once created. This will allow you a higher customization.|`null`| 39 | |`script`|Scripts to load dynamically in case no TOAST UI editor constructor is found|`['...tui-code-snippet.js', '...tui-color-picker.js', '...tui-image-editor.min.js']`| 40 | |`style`|In case the `script` is loaded this style will be loaded too|`['...tui-color-picker.css', '...tui-image-editor.css']`| 41 | 42 | 43 | 44 | 45 | 46 | ## Download 47 | 48 | * CDN 49 | * `https://unpkg.com/grapesjs-tui-image-editor` 50 | * NPM 51 | * `npm i grapesjs-tui-image-editor` 52 | * GIT 53 | * `git clone https://github.com/GrapesJS/tui-image-editor.git` 54 | 55 | 56 | 57 | 58 | 59 | ## Usage 60 | 61 | Directly in the browser 62 | ```html 63 | 64 | 65 | 66 | 67 |
68 | 69 | 85 | ``` 86 | 87 | Modern javascript 88 | ```js 89 | import grapesjs from 'grapesjs'; 90 | import plugin from 'grapesjs-tui-image-editor'; 91 | 92 | const editor = grapesjs.init({ 93 | container : '#gjs', 94 | // ... 95 | plugins: [plugin], 96 | pluginsOpts: { 97 | [plugin]: { /* options */ } 98 | } 99 | // or 100 | plugins: [ 101 | editor => plugin(editor, { /* options */ }), 102 | ], 103 | }); 104 | ``` 105 | 106 | 107 | 108 | 109 | 110 | ## Development 111 | 112 | Clone the repository 113 | 114 | ```sh 115 | $ git clone https://github.com/GrapesJS/tui-image-editor.git 116 | $ cd grapesjs-tui-image-editor 117 | ``` 118 | 119 | Install dependencies 120 | 121 | ```sh 122 | $ npm i 123 | ``` 124 | 125 | Start the dev server 126 | 127 | ```sh 128 | $ npm start 129 | ``` 130 | 131 | 132 | 133 | 134 | 135 | ## License 136 | 137 | BSD 3-Clause 138 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS TOAST UI Image Editor 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 |
21 | This is a demo content from index.html. For the development, you shouldn't edit this file, instead you can 22 | copy and rename it to _index.html, on next server start the new file will be served, and it will be ignored by git. 23 |
24 | 25 |
26 | 27 | 28 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-tui-image-editor", 3 | "version": "1.0.2", 4 | "description": "GrapesJS TOAST UI Image Editor", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "build": "grapesjs-cli build", 11 | "start": "grapesjs-cli serve" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/GrapesJS/tui-image-editor.git" 16 | }, 17 | "keywords": [ 18 | "grapesjs", 19 | "plugin", 20 | "image", 21 | "editor" 22 | ], 23 | "author": "Artur Arseniev", 24 | "license": "BSD-3-Clause", 25 | "devDependencies": { 26 | "ajv": "^8.12.0", 27 | "grapesjs": "^0.21.2", 28 | "grapesjs-cli": "^4.1.1", 29 | "tui-image-editor": "^3.15.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Component, Plugin } from 'grapesjs'; 2 | import type tuiImageEditor from 'tui-image-editor'; 3 | 4 | type ImageEditor = tuiImageEditor.ImageEditor; 5 | type IOptions = tuiImageEditor.IOptions; 6 | type Constructor = { new(...any: any): K }; 7 | 8 | export type PluginOptions = { 9 | /** 10 | * TOAST UI's configurations 11 | * https://nhn.github.io/tui.image-editor/latest/ImageEditor 12 | */ 13 | config?: IOptions; 14 | 15 | /** 16 | * Pass the editor constructor. 17 | * By default, the `tui.ImageEditor` will be used. 18 | */ 19 | constructor?: any; 20 | 21 | /** 22 | * Label for the image editor (used in the modal) 23 | * @default 'Image Editor' 24 | */ 25 | labelImageEditor?: string; 26 | 27 | /** 28 | * Label used on the apply button 29 | * @default 'Apply' 30 | */ 31 | labelApply?: string; 32 | 33 | /** 34 | * Default editor height 35 | * @default '650px' 36 | */ 37 | height?: string; 38 | 39 | /** 40 | * Default editor width 41 | * @default '100%' 42 | */ 43 | width?: string; 44 | 45 | /** 46 | * Id to use to create the image editor command 47 | * @default 'tui-image-editor' 48 | */ 49 | commandId?: string; 50 | 51 | /** 52 | * Icon used in the image component toolbar. Pass an empty string to avoid adding the icon. 53 | */ 54 | toolbarIcon?: string; 55 | 56 | /** 57 | * Hide the default editor header 58 | * @default true 59 | */ 60 | hideHeader?: boolean; 61 | 62 | /** 63 | * By default, GrapesJS takes the modified image, adds it to the Asset Manager and update the target. 64 | * If you need some custom logic you can use this custom 'onApply' function. 65 | * @example 66 | * onApply: (imageEditor, imageModel) => { 67 | * const dataUrl = imageEditor.toDataURL(); 68 | * editor.AssetManager.add({ src: dataUrl }); // Add it to Assets 69 | * imageModel.set('src', dataUrl); // Update the image component 70 | * } 71 | */ 72 | onApply?: ((imageEditor: ImageEditor, imageModel: Component) => void) | null; 73 | 74 | /** 75 | * If no custom `onApply` is passed and this option is `true`, the result image will be added to assets 76 | * @default true 77 | */ 78 | addToAssets?: boolean; 79 | 80 | /** 81 | * If no custom `onApply` is passed, on confirm, the edited image, will be passed to the 82 | * AssetManager's uploader and the result (eg. instead of having the dataURL you'll have the URL) 83 | * will be passed to the default `onApply` process (update target, etc.) 84 | */ 85 | upload?: boolean; 86 | 87 | /** 88 | * The apply button (HTMLElement) will be passed as an argument to this function, once created. 89 | * This will allow you a higher customization. 90 | */ 91 | onApplyButton?: (btn: HTMLElement) => void; 92 | 93 | /** 94 | * Scripts to load dynamically in case no TOAST UI editor instance was found 95 | */ 96 | script?: string[]; 97 | 98 | /** 99 | * In case the script is loaded this style will be loaded too 100 | */ 101 | style?: string[]; 102 | }; 103 | 104 | const plugin: Plugin = (editor, options = {}) => { 105 | const opts: Required = { 106 | config: {}, 107 | constructor: '', 108 | labelImageEditor: 'Image Editor', 109 | labelApply: 'Apply', 110 | height: '650px', 111 | width: '100%', 112 | commandId: 'tui-image-editor', 113 | toolbarIcon: ` 114 | 115 | 116 | `, 117 | hideHeader: true, 118 | addToAssets: true, 119 | upload: false, 120 | onApplyButton: () => {}, 121 | onApply: null, 122 | script: [ 123 | 'https://uicdn.toast.com/tui.code-snippet/v1.5.2/tui-code-snippet.min.js', 124 | 'https://uicdn.toast.com/tui-color-picker/v2.2.7/tui-color-picker.min.js', 125 | 'https://uicdn.toast.com/tui-image-editor/v3.15.2/tui-image-editor.min.js' 126 | ], 127 | style: [ 128 | 'https://uicdn.toast.com/tui-color-picker/v2.2.7/tui-color-picker.min.css', 129 | 'https://uicdn.toast.com/tui-image-editor/v3.15.2/tui-image-editor.min.css', 130 | ], 131 | ...options, 132 | }; 133 | 134 | const { script, style, height, width, hideHeader, onApply, upload, addToAssets, commandId } = opts; 135 | const hasWindow = typeof window !== 'undefined'; 136 | 137 | const getConstructor = (): Constructor => { 138 | return opts.constructor || 139 | (hasWindow && (window as any).tui?.ImageEditor); 140 | }; 141 | 142 | let constr = getConstructor(); 143 | 144 | // Dynamic loading of the image editor scripts and styles 145 | if (!constr && script?.length && hasWindow) { 146 | const { head } = document; 147 | const scripts = Array.isArray(script) ? [...script] : [script]; 148 | const styles = (Array.isArray(style) ? [...style] : [style]) as string[]; 149 | const appendStyle = (styles: string[]) => { 150 | if (styles.length) { 151 | const link = document.createElement('link'); 152 | link.href = styles.shift()!; 153 | link.rel = 'stylesheet'; 154 | head.appendChild(link); 155 | appendStyle(styles); 156 | } 157 | } 158 | const appendScript = (scripts: string[]) => { 159 | if (scripts.length) { 160 | const scr = document.createElement('script'); 161 | scr.src = scripts.shift()!; 162 | scr.onerror = scr.onload = appendScript.bind(null, scripts); 163 | head.appendChild(scr); 164 | } else { 165 | constr = getConstructor(); 166 | } 167 | } 168 | appendStyle(styles); 169 | appendScript(scripts); 170 | } 171 | 172 | // Update image component toolbar 173 | if (opts.toolbarIcon) { 174 | editor.Components.addType('image', { 175 | extendFn: ['initToolbar'], 176 | model: { 177 | initToolbar() { 178 | const tb = this.get('toolbar'); 179 | const tbExists = tb?.some(item => item.command === commandId); 180 | 181 | if (!tbExists) { 182 | tb?.unshift({ 183 | command: commandId, 184 | label: opts.toolbarIcon, 185 | }); 186 | this.set('toolbar', tb); 187 | } 188 | } 189 | } 190 | }); 191 | } 192 | 193 | // Add the image editor command 194 | const errorOpts = { level: 'error', ns: commandId }; 195 | editor.Commands.add(commandId, { 196 | imageEditor: null as tuiImageEditor | null, 197 | 198 | run(ed, s, options = {}) { 199 | if (!constr) { 200 | ed.log('TOAST UI Image editor not found', errorOpts); 201 | return ed.stopCommand(commandId); 202 | } 203 | 204 | const target = (options.target || ed.getSelected()) as Component; 205 | 206 | if (!target) { 207 | ed.log('Target not available', errorOpts); 208 | return ed.stopCommand(commandId); 209 | } 210 | 211 | const content = this.createContent(); 212 | const title = opts.labelImageEditor; 213 | const btn = content.children[1] as HTMLElement; 214 | ed.Modal.open({ title, content }).onceClose(() => ed.stopCommand(commandId)) 215 | 216 | const editorConfig = this.getEditorConfig(target.get('src')); 217 | this.imageEditor = new constr(content.children[0], editorConfig); 218 | ed.getModel().setEditing(true); 219 | btn.onclick = () => this.applyChanges(target); 220 | opts.onApplyButton(btn); 221 | }, 222 | 223 | stop(ed) { 224 | (this.imageEditor as tuiImageEditor)?.destroy(); 225 | ed.getModel().setEditing(false); 226 | }, 227 | 228 | getEditorConfig(path: string): IOptions { 229 | const config: IOptions = { ...opts.config }; 230 | 231 | if (!config.includeUI) config.includeUI = {}; 232 | 233 | config.includeUI = { 234 | theme: {}, 235 | ...config.includeUI, 236 | loadImage: { path, name: '1' }, 237 | uiSize: { height, width }, 238 | }; 239 | 240 | if (hideHeader) { 241 | // @ts-ignore 242 | config.includeUI.theme['header.display'] = 'none'; 243 | } 244 | 245 | return config; 246 | }, 247 | 248 | createContent(): HTMLDivElement { 249 | const content = document.createElement('div'); 250 | content.style.position = 'relative'; 251 | content.innerHTML = ` 252 |
253 |