├── .gitignore ├── assets └── 2cd4f84f-cb1c-4a15-b281-25af67a9f4e9.jpg ├── simple-image.css ├── example.html ├── README.md └── simple-image.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /assets/2cd4f84f-cb1c-4a15-b281-25af67a9f4e9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-js/simple-image-tutorial/master/assets/2cd4f84f-cb1c-4a15-b281-25af67a9f4e9.jpg -------------------------------------------------------------------------------- /simple-image.css: -------------------------------------------------------------------------------- 1 | .simple-image { 2 | padding: 20px 0; 3 | } 4 | 5 | .simple-image input, 6 | .simple-image [contenteditable] { 7 | width: 100%; 8 | padding: 10px; 9 | border: 1px solid #e4e4e4; 10 | background: #fff; 11 | box-sizing: border-box; 12 | border-radius: 3px; 13 | outline: none; 14 | font-size: 14px; 15 | } 16 | 17 | .simple-image img { 18 | max-width: 100%; 19 | margin-bottom: 15px; 20 | } 21 | 22 | .simple-image.withBorder img { 23 | border: 1px solid #e8e8eb; 24 | } 25 | 26 | .simple-image.withBackground { 27 | background: #eff2f5; 28 | padding: 10px; 29 | } 30 | 31 | .simple-image.withBackground img { 32 | display: block; 33 | max-width: 60%; 34 | margin: 0 auto 15px; 35 | } -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 |

21 | 
22 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Simple Image tutorial project
 2 | Final result of guide series learning [how to create own Block Tool](https://editorjs.io/creating-a-block-tool) for Editor.js
 3 | 
 4 | Simple Image — paste image URL and get the image Block rendered.
 5 | Also supports pasting files and substitutions of `` tags on pasting.
 6 | Does not support uploading images from the device.
 7 | 
 8 | ![](assets/2cd4f84f-cb1c-4a15-b281-25af67a9f4e9.jpg)
 9 | 
10 | ## Chapters
11 | 
12 | - [The first plugin](https://editorjs.io/the-first-plugin)
13 | - [Fill Block with saved data](https://editorjs.io/fill-block-with-saved-data)
14 | - [Saved data validation](https://editorjs.io/saved-data-validation)
15 | - [Changing a view](https://editorjs.io/changing-a-view)
16 | - [Enable Inline Toolbar](https://editorjs.io/enable-inline-toolbar)
17 | - [Making a Block Settings](https://editorjs.io/making-a-block-settings)
18 | - [Access Editor's API](https://editorjs.io/access-api)
19 | - [Paste substitutions](https://editorjs.io/paste-substitutions)
20 | - [Sanitize saved data](https://editorjs.io/sanitize-saved-data)
21 | - [Provide custom configuration](https://editorjs.io/provide-custom-configuration)
22 | 
23 | 
24 | ## Installation
25 | 
26 | **Note.** If you want to use such tool in production, see full-featured [Simple Image](https://github.com/editor-js/simple-image) Tool.
27 | The tutorial code and functionality are simplified for better understanding of basics API usage.
28 | 
29 | ## Configuration
30 | 
31 | This Tool supports following configuration properties:
32 | 
33 | | name | description |
34 | | -- | -- |
35 | | `placeholder` | Custom placeholder for «Paste URL» field |
36 | 
37 | Example of connection:
38 | 
39 | ```js
40 |   /**
41 |    * Initialize the Editor
42 |    */
43 |   const editor = new EditorJS({
44 |     autofocus: true,
45 |     tools: {
46 |       image: {
47 |         class: SimpleImage,
48 |         inlineToolbar: true,
49 |         config: {
50 |           placeholder: 'Paste image URL'
51 |         }
52 |       }
53 |     },
54 |   });
55 | ```
56 | 
57 | ## Output data format
58 | 
59 | This Tool return following data format:
60 | 
61 | ```js
62 | {
63 |   url: 'https://cdn.pixabay.com/photo/2017/09/01/21/53/blue-2705642_1280.jpg'
64 |   caption: 'Image caption example',
65 |   withBorder: false,
66 |   withBackground: false,
67 |   stretched: false
68 | }
69 | ```
70 | 
71 | | field | type | description |
72 | | -- | -- | -- |
73 | | `url` | __string__ | image source URL |
74 | | `caption` | __string__ | image caption |
75 | | `withBorder` | __boolean__ | flag for adding a border |
76 | | `withBackground` | __boolean__ | flag for adding a background |
77 | | `stretched` | __boolean__ | flag for stretching image to the full width of content |


--------------------------------------------------------------------------------
/simple-image.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Tool for creating image Blocks for Editor.js
  3 |  * Made with «Creating a Block Tool» tutorial {@link https://editorjs.io/creating-a-block-tool}
  4 |  *
  5 |  * @typedef {object} ImageToolData — Input/Output data format for our Tool
  6 |  * @property {string} url - image source URL
  7 |  * @property {string} caption - image caption
  8 |  * @property {boolean} withBorder - flag for adding a border
  9 |  * @property {boolean} withBackground - flag for adding a background
 10 |  * @property {boolean} stretched - flag for stretching image to the full width of content
 11 |  *
 12 |  * @typedef {object} ImageToolConfig
 13 |  * @property {string} placeholder — custom placeholder for URL field
 14 |  */
 15 | class SimpleImage {
 16 |   /**
 17 |    * Our tool should be placed at the Toolbox, so describe an icon and title
 18 |    */
 19 |   static get toolbox() {
 20 |     return {
 21 |       title: 'Image',
 22 |       icon: ''
 23 |     };
 24 |   }
 25 | 
 26 |   /**
 27 |    * Allow render Image Blocks by pasting HTML tags, files and URLs
 28 |    * @see {@link https://editorjs.io/paste-substitutions}
 29 |    * @return {{tags: string[], files: {mimeTypes: string[], extensions: string[]}, patterns: {image: RegExp}}}
 30 |    */
 31 |   static get pasteConfig() {
 32 |     return {
 33 |       tags: ['IMG'],
 34 |       files: {
 35 |         mimeTypes: ['image/*'],
 36 |         extensions: ['gif', 'jpg', 'png'] // You can specify extensions instead of mime-types
 37 |       },
 38 |       patterns: {
 39 |         image: /https?:\/\/\S+\.(gif|jpe?g|tiff|png)$/i
 40 |       }
 41 |     }
 42 |   }
 43 | 
 44 |   /**
 45 |    * Automatic sanitize config
 46 |    * @see {@link https://editorjs.io/sanitize-saved-data}
 47 |    */
 48 |   static get sanitize(){
 49 |     return {
 50 |       url: {},
 51 |       caption: {
 52 |         b: true,
 53 |         a: {
 54 |           href: true
 55 |         },
 56 |         i: true
 57 |       }
 58 |     }
 59 |   }
 60 | 
 61 |   /**
 62 |    * Tool class constructor
 63 |    * @param {ImageToolData} data — previously saved data
 64 |    * @param {object} api — Editor.js Core API {@link  https://editorjs.io/api}
 65 |    * @param {ImageToolConfig} config — custom config that we provide to our tool's user
 66 |    */
 67 |   constructor({data, api, config}){
 68 |     this.api = api;
 69 |     this.config = config || {};
 70 |     this.data = {
 71 |       url: data.url || '',
 72 |       caption: data.caption || '',
 73 |       withBorder: data.withBorder !== undefined ? data.withBorder : false,
 74 |       withBackground: data.withBackground !== undefined ? data.withBackground : false,
 75 |       stretched: data.stretched !== undefined ? data.stretched : false,
 76 |     };
 77 | 
 78 |     this.wrapper = undefined;
 79 |     this.settings = [
 80 |       {
 81 |         name: 'withBorder',
 82 |         icon: ``
 83 |       },
 84 |       {
 85 |         name: 'stretched',
 86 |         icon: ``
 87 |       },
 88 |       {
 89 |         name: 'withBackground',
 90 |         icon: ``
 91 |       }
 92 |     ];
 93 |   }
 94 | 
 95 |   /**
 96 |    * Return a Tool's UI
 97 |    * @return {HTMLElement}
 98 |    */
 99 |   render(){
100 |     this.wrapper = document.createElement('div');
101 |     this.wrapper.classList.add('simple-image');
102 | 
103 |     if (this.data && this.data.url){
104 |       this._createImage(this.data.url, this.data.caption);
105 |       return this.wrapper;
106 |     }
107 | 
108 |     const input = document.createElement('input');
109 | 
110 |     input.placeholder = this.config.placeholder || 'Paste an image URL...';
111 |     input.addEventListener('paste', (event) => {
112 |       this._createImage(event.clipboardData.getData('text'));
113 |     });
114 | 
115 |     this.wrapper.appendChild(input);
116 | 
117 |     return this.wrapper;
118 |   }
119 | 
120 |   /**
121 |    * @private
122 |    * Create image with caption field
123 |    * @param {string} url — image source
124 |    * @param {string} captionText — caption value
125 |    */
126 |   _createImage(url, captionText){
127 |     const image = document.createElement('img');
128 |     const caption = document.createElement('div');
129 | 
130 |     image.src = url;
131 |     caption.contentEditable = true;
132 |     caption.innerHTML = captionText || '';
133 | 
134 |     this.wrapper.innerHTML = '';
135 |     this.wrapper.appendChild(image);
136 |     this.wrapper.appendChild(caption);
137 | 
138 |     this._acceptTuneView();
139 |   }
140 | 
141 |   /**
142 |    * Extract data from the UI
143 |    * @param {HTMLElement} blockContent — element returned by render method
144 |    * @return {SimpleImageData}
145 |    */
146 |   save(blockContent){
147 |     const image = blockContent.querySelector('img');
148 |     const caption = blockContent.querySelector('[contenteditable]');
149 | 
150 |     return Object.assign(this.data, {
151 |       url: image.src,
152 |       caption: caption.innerHTML || ''
153 |     });
154 |   }
155 | 
156 |   /**
157 |    * Skip empty blocks
158 |    * @see {@link https://editorjs.io/saved-data-validation}
159 |    * @param {ImageToolConfig} savedData
160 |    * @return {boolean}
161 |    */
162 |   validate(savedData){
163 |     if (!savedData.url.trim()){
164 |       return false;
165 |     }
166 | 
167 |     return true;
168 |   }
169 | 
170 |   /**
171 |    * Making a Block settings: 'add border', 'add background', 'stretch to full width'
172 |    * @see https://editorjs.io/making-a-block-settings — tutorial
173 |    * @see https://editorjs.io/tools-api#rendersettings - API method description
174 |    * @return {HTMLDivElement}
175 |    */
176 |   renderSettings(){
177 |     const wrapper = document.createElement('div');
178 | 
179 |     this.settings.forEach( tune => {
180 |       let button = document.createElement('div');
181 | 
182 |       button.classList.add(this.api.styles.settingsButton);
183 |       button.classList.toggle(this.api.styles.settingsButtonActive, this.data[tune.name]);
184 |       button.innerHTML = tune.icon;
185 |       wrapper.appendChild(button);
186 | 
187 |       button.addEventListener('click', () => {
188 |         this._toggleTune(tune.name);
189 |         button.classList.toggle(this.api.styles.settingsButtonActive);
190 |       });
191 | 
192 |     });
193 | 
194 |     return wrapper;
195 |   }
196 | 
197 |   /**
198 |    * @private
199 |    * Click on the Settings Button
200 |    * @param {string} tune — tune name from this.settings
201 |    */
202 |   _toggleTune(tune) {
203 |     this.data[tune] = !this.data[tune];
204 |     this._acceptTuneView();
205 |   }
206 | 
207 |   /**
208 |    * Add specified class corresponds with activated tunes
209 |    * @private
210 |    */
211 |   _acceptTuneView() {
212 |     this.settings.forEach( tune => {
213 |       this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]);
214 | 
215 |       if (tune.name === 'stretched') {
216 |         this.api.blocks.stretchBlock(this.api.blocks.getCurrentBlockIndex(), !!this.data.stretched);
217 |       }
218 |     });
219 |   }
220 | 
221 |   /**
222 |    * Handle paste event
223 |    * @see https://editorjs.io/tools-api#onpaste - API description
224 |    * @param {CustomEvent }event
225 |    */
226 |   onPaste(event){
227 |     switch (event.type){
228 |       case 'tag':
229 |         const imgTag = event.detail.data;
230 |         this._createImage(imgTag.src);
231 |         break;
232 |       case 'file':
233 |         /* We need to read file here as base64 string */
234 |         const file = event.detail.file;
235 |         const reader = new FileReader();
236 | 
237 |         reader.onload = (loadEvent) => {
238 |           this._createImage(loadEvent.target.result);
239 |         };
240 | 
241 |         reader.readAsDataURL(file);
242 |         break;
243 |       case 'pattern':
244 |         const src = event.detail.data;
245 | 
246 |         this._createImage(src);
247 |         break;
248 |     }
249 |   }
250 | }


--------------------------------------------------------------------------------