• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kommitters / editorjs-inline-image / 8531118904

26 Feb 2024 09:50PM UTC coverage: 84.252% (-0.2%) from 84.409%
8531118904

push

github

web-flow
Merge pull request #127 from kommitters/v2.0

Release v2.0.0

71 of 83 branches covered (85.54%)

Branch coverage included in aggregate %.

3 of 6 new or added lines in 3 files covered. (50.0%)

2 existing lines in 1 file now uncovered.

250 of 298 relevant lines covered (83.89%)

19.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.18
/src/controlPanel.js
1
import { make, isUrl, createImageCredits } from './helpers';
2
import UnsplashClient from './unsplashClient';
3

4
/**
5
 * Renders control panel view
6
 *  - Embed image url
7
 *  - Embed image from Unsplash
8
 */
9
export default class ControlPanel {
10
  /**
11
   * @param {{ api: object, config: object, cssClasses: object,
12
   *  onSelectImage: Function, readOnly: Boolean }}
13
   *  api - Editorjs API
14
   *  config - Tool custom config
15
   *  readOnly - read-only mode flag
16
   *  cssClasses - Css class names
17
   *  onSelectImage - Image selection callback
18
   */
19
  constructor({
20
    api, config, cssClasses, onSelectImage, readOnly,
21
  }) {
22
    this.api = api;
36✔
23
    this.config = config;
36✔
24
    this.readOnly = readOnly;
36✔
25

26
    this.cssClasses = {
36✔
27
      ...cssClasses,
28
      controlPanel: 'inline-image__control-panel',
29
      tabWrapper: 'inline-image__tab-wrapper',
30
      tab: 'inline-image__tab',
31
      embedButton: 'inline-image__embed-button',
32
      search: 'inline-image__search',
33
      imageGallery: 'inline-image__image-gallery',
34
      noResults: 'inline-image__no-results',
35
      imgWrapper: 'inline-image__img-wrapper',
36
      thumb: 'inline-image__thumb',
37
      active: 'active',
38
      hidden: 'panel-hidden',
39
      scroll: 'panel-scroll',
40
    };
41

42
    this.onSelectImage = onSelectImage;
36✔
43

44
    this.nodes = {
36✔
45
      loader: null,
46
      embedUrlTab: null,
47
      unsplashTab: null,
48
      embedUrlPanel: null,
49
      unsplashPanel: null,
50
      imageGallery: null,
51
      searchInput: null,
52
    };
53

54
    this.unsplashClient = new UnsplashClient(this.config.unsplash);
36✔
55
    this.searchTimeout = null;
36✔
56
    this.showEmbedTab = this.config.embed ? this.config.embed.display : true;
36✔
57
  }
58

59
  /**
60
   * Creates Control Panel components
61
   *
62
   * @returns {HTMLDivElement}
63
   */
64
  render() {
65
    const wrapper = make('div', this.cssClasses.controlPanel);
22✔
66
    const tabWrapper = make('div', this.cssClasses.tabWrapper);
22✔
67
    const embedUrlTab = make('div', [this.cssClasses.tab, this.cssClasses.active], {
22✔
68
      innerHTML: 'Embed URL',
69
      onclick: () => this.showEmbedUrlPanel(),
×
70
    });
71
    const unsplashTab = make('div', [this.cssClasses.tab, this.showEmbedTab ? null : this.cssClasses.active], {
22✔
72
      innerHTML: 'Unsplash',
73
      onclick: () => this.showUnsplashPanel(),
×
74
    });
75

76
    const embedUrlPanel = this.renderEmbedUrlPanel();
22✔
77
    const unsplashPanel = this.renderUnsplashPanel();
22✔
78

79
    if (this.showEmbedTab) { tabWrapper.appendChild(embedUrlTab); }
22✔
80
    tabWrapper.appendChild(unsplashTab);
22✔
81
    wrapper.appendChild(tabWrapper);
22✔
82
    if (this.showEmbedTab) { wrapper.appendChild(embedUrlPanel); }
22✔
83
    wrapper.appendChild(unsplashPanel);
22✔
84

85
    this.nodes.embedUrlPanel = this.showEmbedTab ? embedUrlPanel : null;
22✔
86
    this.nodes.unsplashPanel = unsplashPanel;
22✔
87
    this.nodes.embedUrlTab = this.showEmbedTab ? embedUrlTab : null;
22✔
88
    this.nodes.unsplashTab = unsplashTab;
22✔
89

90
    return wrapper;
22✔
91
  }
92

93
  /**
94
   * Shows "Embed Url" control panel
95
   *
96
   * @returns {void}
97
   */
98
  showEmbedUrlPanel() {
99
    this.nodes.embedUrlTab.classList.add(this.cssClasses.active);
×
100
    this.nodes.unsplashTab.classList.remove(this.cssClasses.active);
×
101
    this.nodes.embedUrlPanel.classList.remove(this.cssClasses.hidden);
×
102
    this.nodes.unsplashPanel.classList.add(this.cssClasses.hidden);
×
103
  }
104

105
  /**
106
   * Shows "Unsplash" control panel
107
   *
108
   * @returns {void}
109
   */
110
  showUnsplashPanel() {
111
    this.nodes.unsplashTab.classList.add(this.cssClasses.active);
×
112
    this.nodes.embedUrlTab.classList.remove(this.cssClasses.active);
×
113
    this.nodes.unsplashPanel.classList.remove(this.cssClasses.hidden);
×
114
    this.nodes.embedUrlPanel.classList.add(this.cssClasses.hidden);
×
115
  }
116

117
  /**
118
   * Creates "Embed Url" control panel
119
   *
120
   * @returns {HTMLDivElement}
121
   */
122
  renderEmbedUrlPanel() {
123
    const wrapper = make('div');
22✔
124
    const urlInput = make('div', [this.cssClasses.input, this.cssClasses.caption], {
22✔
125
      id: 'image-url',
126
      contentEditable: !this.readOnly,
127
    });
128
    const embedImageButton = make('div', [this.cssClasses.embedButton, this.cssClasses.input], {
22✔
129
      id: 'embed-button',
130
      innerHTML: 'Embed Image',
131
      onclick: () => this.embedButtonClicked(urlInput.innerHTML),
4✔
132
    });
133

134
    urlInput.dataset.placeholder = 'Enter image url...';
22✔
135

136
    wrapper.appendChild(urlInput);
22✔
137
    wrapper.appendChild(embedImageButton);
22✔
138

139
    return wrapper;
22✔
140
  }
141

142
  /**
143
   * OnClick handler for Embed Image Button
144
   *
145
   * @param {string} imageUrl embedded image url
146
   * @returns {void}
147
   */
148
  embedButtonClicked(imageUrl) {
149
    if (isUrl(imageUrl)) {
4✔
150
      this.onSelectImage({ url: imageUrl });
2✔
151
    } else {
152
      this.api.notifier.show({
2✔
153
        message: 'Please enter a valid url.',
154
        style: 'error',
155
      });
156
    }
157
  }
158

159
  /**
160
   * Creates "Unsplash" control panel
161
   *
162
   * @returns {HTMLDivElement}
163
   */
164
  renderUnsplashPanel() {
165
    const wrapper = make('div', this.showEmbedTab ? this.cssClasses.hidden : null);
22✔
166
    const imageGallery = make('div', this.cssClasses.imageGallery);
22✔
167
    const searchInput = make('div', [this.cssClasses.input, this.cssClasses.caption, this.cssClasses.search], {
22✔
168
      id: 'unsplash-search',
169
      contentEditable: !this.readOnly,
170
      oninput: () => this.searchInputHandler(),
2✔
171
    });
172

173
    searchInput.dataset.placeholder = 'Search for an image...';
22✔
174

175
    wrapper.appendChild(searchInput);
22✔
176
    wrapper.appendChild(imageGallery);
22✔
177

178
    this.nodes.searchInput = searchInput;
22✔
179
    this.nodes.imageGallery = imageGallery;
22✔
180

181
    return wrapper;
22✔
182
  }
183

184
  /**
185
   * OnInput handler for Search input
186
   *
187
   * @returns {void}
188
   */
189
  searchInputHandler() {
190
    this.showLoader();
2✔
191
    this.performSearch();
2✔
192
  }
193

194
  /**
195
   * Shows a loader spinner on image gallery
196
   *
197
   * @returns {void}
198
   */
199
  showLoader() {
200
    this.nodes.imageGallery.innerHTML = '';
2✔
201
    this.nodes.loader = make('div', this.cssClasses.loading);
2✔
202
    this.nodes.imageGallery.appendChild(this.nodes.loader);
2✔
203
  }
204

205
  /**
206
   * Performs image search on user input.
207
   * Defines a timeout for preventing multiple requests
208
   *
209
   * @returns {void}
210
   */
211
  performSearch() {
212
    clearTimeout(this.searchTimeout);
2✔
213
    this.searchTimeout = setTimeout(() => {
2✔
214
      const query = this.nodes.searchInput.innerHTML;
2✔
215
      this.unsplashClient.searchImages(
2✔
216
        query,
NEW
217
        (results) => this.appendImagesToGallery(results),
×
218
      );
219
    }, 1000);
220
  }
221

222
  /**
223
   * Creates the image gallery using Unsplash API results.
224
   *
225
   * @param {Array} results Images from Unsplash API
226
   */
227
  appendImagesToGallery(results) {
228
    this.nodes.imageGallery.innerHTML = '';
8✔
229
    if (results && results.length) {
8✔
230
      this.nodes.unsplashPanel.classList.add(this.cssClasses.scroll);
6✔
231
      results.forEach((image) => {
6✔
232
        this.createThumbImage(image);
8✔
233
      });
234
    } else {
235
      const noResults = make('div', this.cssClasses.noResults, {
2✔
236
        innerHTML: 'No images found',
237
      });
238
      this.nodes.imageGallery.appendChild(noResults);
2✔
239
      this.nodes.unsplashPanel.classList.remove(this.cssClasses.scroll);
2✔
240
    }
241
  }
242

243
  /**
244
   * Creates a thumb image and appends it to the image gallery
245
   *
246
   * @param {Object} image Unsplash image object
247
   * @returns {void}
248
   */
249
  createThumbImage(image) {
250
    const imgWrapper = make('div', this.cssClasses.imgWrapper);
8✔
251
    const img = make('img', this.cssClasses.thumb, {
8✔
252
      src: image.thumb,
253
      onclick: () => this.downloadUnsplashImage(image),
4✔
254
    });
255

256
    const { appName } = this.config.unsplash;
8✔
257
    const imageCredits = createImageCredits({ ...image, appName });
8✔
258

259
    imgWrapper.appendChild(img);
8✔
260
    imgWrapper.appendChild(imageCredits);
8✔
261
    this.nodes.imageGallery.append(imgWrapper);
8✔
262
  }
263

264
  /**
265
   * Handler for embedding Unsplash images.
266
   * Issues a request to Unsplash API
267
   *
268
   * @param {{url: string, author: string, profileLink: string, downloadLocation: string}}
269
   *  url - Image url
270
   *  author - Unsplash image author name
271
   *  profileLink - Unsplash author profile link
272
   *  downloadLocation - Unsplash endpoint for image download
273
   *
274
   * @returns {void}
275
   */
276
  downloadUnsplashImage({
277
    url, author, profileLink, downloadLocation,
278
  }) {
279
    this.onSelectImage({
4✔
280
      url,
281
      unsplash: {
282
        author,
283
        profileLink,
284
      },
285
    });
286
    this.unsplashClient.downloadImage(downloadLocation);
4✔
287
  }
288
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc