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

kommitters / editorjs-inline-image / 8558467075

04 Apr 2024 05:01PM UTC coverage: 84.87% (+0.6%) from 84.252%
8558467075

push

github

web-flow
Merge pull request #139 from kommitters/v2.1

Release v2.1.0

78 of 93 branches covered (83.87%)

Branch coverage included in aggregate %.

33 of 34 new or added lines in 4 files covered. (97.06%)

1 existing line in 1 file now uncovered.

281 of 330 relevant lines covered (85.15%)

21.95 hits per line

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

90.21
/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;
38✔
23
    this.config = config;
38✔
24
    this.readOnly = readOnly;
38✔
25

26
    this.cssClasses = {
38✔
27
      ...cssClasses,
28
      controlPanel: 'inline-image__control-panel',
29
      tabWrapper: 'inline-image__tab-wrapper',
30
      tab: 'inline-image__tab',
31
      orientationWrapper: 'inline-image__orientation-wrapper',
32
      orientationButton: 'inline-image__orientation-button',
33
      embedButton: 'inline-image__embed-button',
34
      search: 'inline-image__search',
35
      imageGallery: 'inline-image__image-gallery',
36
      noResults: 'inline-image__no-results',
37
      imgWrapper: 'inline-image__img-wrapper',
38
      thumb: 'inline-image__thumb',
39
      landscapeImg: 'landscape-img',
40
      portraitImg: 'portrait-img',
41
      squarishImg: 'squarish-img',
42
      active: 'active',
43
      hidden: 'panel-hidden',
44
      scroll: 'panel-scroll',
45
    };
46

47
    this.onSelectImage = onSelectImage;
38✔
48

49
    this.nodes = {
38✔
50
      loader: null,
51
      embedUrlTab: null,
52
      unsplashTab: null,
53
      embedUrlPanel: null,
54
      unsplashPanel: null,
55
      imageGallery: null,
56
      searchInput: null,
57
      landscapeButton: null,
58
      portraitButton: null,
59
      squarishButton: null,
60
    };
61

62
    this.unsplashClient = new UnsplashClient(this.config.unsplash);
38✔
63
    this.searchTimeout = null;
38✔
64
    this.showEmbedTab = this.config.embed ? this.config.embed.display : true;
38✔
65
    this.queryOrientation = null;
38✔
66
  }
67

68
  /**
69
   * Creates Control Panel components
70
   *
71
   * @returns {HTMLDivElement}
72
   */
73
  render() {
74
    const wrapper = make('div', this.cssClasses.controlPanel);
24✔
75
    const tabWrapper = make('div', this.cssClasses.tabWrapper);
24✔
76
    const embedUrlTab = make('div', [this.cssClasses.tab, this.cssClasses.active], {
24✔
77
      innerHTML: 'Embed URL',
78
      onclick: () => this.showEmbedUrlPanel(),
×
79
    });
80
    const unsplashTab = make('div', [this.cssClasses.tab, this.showEmbedTab ? null : this.cssClasses.active], {
24✔
81
      innerHTML: 'Unsplash',
82
      onclick: () => this.showUnsplashPanel(),
×
83
    });
84

85
    const embedUrlPanel = this.renderEmbedUrlPanel();
24✔
86
    const unsplashPanel = this.renderUnsplashPanel();
24✔
87

88
    if (this.showEmbedTab) { tabWrapper.appendChild(embedUrlTab); }
24✔
89
    tabWrapper.appendChild(unsplashTab);
24✔
90
    wrapper.appendChild(tabWrapper);
24✔
91
    if (this.showEmbedTab) { wrapper.appendChild(embedUrlPanel); }
24✔
92
    wrapper.appendChild(unsplashPanel);
24✔
93

94
    this.nodes.embedUrlPanel = this.showEmbedTab ? embedUrlPanel : null;
24✔
95
    this.nodes.unsplashPanel = unsplashPanel;
24✔
96
    this.nodes.embedUrlTab = this.showEmbedTab ? embedUrlTab : null;
24✔
97
    this.nodes.unsplashTab = unsplashTab;
24✔
98

99
    return wrapper;
24✔
100
  }
101

102
  /**
103
   * Shows "Embed Url" control panel
104
   *
105
   * @returns {void}
106
   */
107
  showEmbedUrlPanel() {
108
    this.nodes.embedUrlTab.classList.add(this.cssClasses.active);
×
109
    this.nodes.unsplashTab.classList.remove(this.cssClasses.active);
×
110
    this.nodes.embedUrlPanel.classList.remove(this.cssClasses.hidden);
×
111
    this.nodes.unsplashPanel.classList.add(this.cssClasses.hidden);
×
112
  }
113

114
  /**
115
   * Shows "Unsplash" control panel
116
   *
117
   * @returns {void}
118
   */
119
  showUnsplashPanel() {
120
    this.nodes.unsplashTab.classList.add(this.cssClasses.active);
×
121
    this.nodes.embedUrlTab.classList.remove(this.cssClasses.active);
×
122
    this.nodes.unsplashPanel.classList.remove(this.cssClasses.hidden);
×
123
    this.nodes.embedUrlPanel.classList.add(this.cssClasses.hidden);
×
124
  }
125

126
  /**
127
   * Creates "Embed Url" control panel
128
   *
129
   * @returns {HTMLDivElement}
130
   */
131
  renderEmbedUrlPanel() {
132
    const wrapper = make('div');
24✔
133
    const urlInput = make('div', [this.cssClasses.input, this.cssClasses.caption], {
24✔
134
      id: 'image-url',
135
      contentEditable: !this.readOnly,
136
    });
137
    const embedImageButton = make('div', [this.cssClasses.embedButton, this.cssClasses.input], {
24✔
138
      id: 'embed-button',
139
      innerHTML: 'Embed Image',
140
      onclick: () => this.embedButtonClicked(urlInput.innerHTML),
4✔
141
    });
142

143
    urlInput.dataset.placeholder = 'Enter image url...';
24✔
144

145
    wrapper.appendChild(urlInput);
24✔
146
    wrapper.appendChild(embedImageButton);
24✔
147

148
    return wrapper;
24✔
149
  }
150

151
  /**
152
   * OnClick handler for Embed Image Button
153
   *
154
   * @param {string} imageUrl embedded image url
155
   * @returns {void}
156
   */
157
  embedButtonClicked(imageUrl) {
158
    if (isUrl(imageUrl)) {
4✔
159
      this.onSelectImage({ url: imageUrl });
2✔
160
    } else {
161
      this.api.notifier.show({
2✔
162
        message: 'Please enter a valid url.',
163
        style: 'error',
164
      });
165
    }
166
  }
167

168
  /**
169
   * Creates "Unsplash" control panel
170
   *
171
   * @returns {HTMLDivElement}
172
   */
173
  renderUnsplashPanel() {
174
    const wrapper = make('div', this.showEmbedTab ? this.cssClasses.hidden : null);
24✔
175
    const imageGallery = make('div', this.cssClasses.imageGallery);
24✔
176
    const searchInput = make('div', [this.cssClasses.input, this.cssClasses.caption, this.cssClasses.search], {
24✔
177
      id: 'unsplash-search',
178
      contentEditable: !this.readOnly,
179
      oninput: () => this.searchInputHandler(),
4✔
180
    });
181
    const orientationWrapper = this.buildOrientationWrapper();
24✔
182

183
    searchInput.dataset.placeholder = 'Search for an image...';
24✔
184

185
    wrapper.appendChild(searchInput);
24✔
186
    wrapper.appendChild(orientationWrapper);
24✔
187
    wrapper.appendChild(imageGallery);
24✔
188

189
    this.nodes.searchInput = searchInput;
24✔
190
    this.nodes.imageGallery = imageGallery;
24✔
191

192
    return wrapper;
24✔
193
  }
194

195
  /**
196
   * OnInput handler for Search input
197
   *
198
   * @returns {void}
199
   */
200
  searchInputHandler() {
201
    this.showLoader();
4✔
202
    this.performSearch();
4✔
203
  }
204

205
  /**
206
   * Shows a loader spinner on image gallery
207
   *
208
   * @returns {void}
209
   */
210
  showLoader() {
211
    this.nodes.imageGallery.innerHTML = '';
6✔
212
    this.nodes.loader = make('div', this.cssClasses.loading);
6✔
213
    this.nodes.imageGallery.appendChild(this.nodes.loader);
6✔
214
  }
215

216
  /**
217
   * Performs image search on user input.
218
   * Defines a timeout for preventing multiple requests
219
   *
220
   * @returns {void}
221
   */
222
  performSearch() {
223
    clearTimeout(this.searchTimeout);
6✔
224
    this.searchTimeout = setTimeout(() => {
6✔
225
      const query = this.nodes.searchInput.innerHTML;
4✔
226
      this.unsplashClient.searchImages(
4✔
227
        query,
228
        this.queryOrientation,
UNCOV
229
        (results) => this.appendImagesToGallery(results),
×
230
      );
231
    }, 1000);
232
  }
233

234
  /**
235
   * Creates the image gallery using Unsplash API results.
236
   *
237
   * @param {Array} results Images from Unsplash API
238
   */
239
  appendImagesToGallery(results) {
240
    this.nodes.imageGallery.innerHTML = '';
8✔
241
    if (results && results.length) {
8✔
242
      this.nodes.unsplashPanel.classList.add(this.cssClasses.scroll);
6✔
243
      results.forEach((image) => {
6✔
244
        this.createThumbImage(image);
8✔
245
      });
246
    } else {
247
      const noResults = make('div', this.cssClasses.noResults, {
2✔
248
        innerHTML: 'No images found',
249
      });
250
      this.nodes.imageGallery.appendChild(noResults);
2✔
251
      this.nodes.unsplashPanel.classList.remove(this.cssClasses.scroll);
2✔
252
    }
253
  }
254

255
  /**
256
   * Creates a thumb image and appends it to the image gallery
257
   *
258
   * @param {Object} image Unsplash image object
259
   * @returns {void}
260
   */
261
  createThumbImage(image) {
262
    const imgWrapper = make('div', this.cssClasses.imgWrapper);
8✔
263
    const imgClasses = [this.cssClasses.thumb];
8✔
264
    if (this.queryOrientation) imgClasses.push(this.cssClasses[`${this.queryOrientation}Img`]);
8✔
265

266
    const img = make('img', imgClasses, {
8✔
267
      src: image.thumb,
268
      onclick: () => this.downloadUnsplashImage(image),
4✔
269
    });
270

271
    const { appName } = this.config.unsplash;
8✔
272
    const imageCredits = createImageCredits({ ...image, appName });
8✔
273

274
    imgWrapper.appendChild(img);
8✔
275
    imgWrapper.appendChild(imageCredits);
8✔
276
    this.nodes.imageGallery.append(imgWrapper);
8✔
277
  }
278

279
  /**
280
   * Handler for embedding Unsplash images.
281
   * Issues a request to Unsplash API
282
   *
283
   * @param {{url: string, author: string, profileLink: string, downloadLocation: string}}
284
   *  url - Image url
285
   *  author - Unsplash image author name
286
   *  profileLink - Unsplash author profile link
287
   *  downloadLocation - Unsplash endpoint for image download
288
   *
289
   * @returns {void}
290
   */
291
  downloadUnsplashImage({
292
    url, author, profileLink, downloadLocation,
293
  }) {
294
    this.onSelectImage({
4✔
295
      url,
296
      unsplash: {
297
        author,
298
        profileLink,
299
      },
300
    });
301
    this.unsplashClient.downloadImage(downloadLocation);
4✔
302
  }
303

304
  /**
305
   * Builds the orientation wrapper that wraps the orientation buttons.
306
   * @returns {HTMLDivElement}
307
   */
308
  buildOrientationWrapper() {
309
    const orientationModes = ['Landscape', 'Portrait', 'Squarish'];
24✔
310
    const orientationButtons = orientationModes.map((orientation) => `${orientation.toLowerCase()}Button`);
72✔
311
    const wrapper = make('div', [this.cssClasses.orientationWrapper]);
24✔
312

313
    orientationModes.forEach((orientation) => {
24✔
314
      const button = make('button', [this.cssClasses.orientationButton], {
72✔
315
        id: `${orientation.toLowerCase()}-button`,
316
        innerHTML: orientation,
317
        onclick: (e) => this.handleOrientationButtonClick(e, orientationButtons),
2✔
318
      });
319
      wrapper.appendChild(button);
72✔
320
      this.nodes[`${orientation.toLowerCase()}Button`] = button;
72✔
321
    });
322

323
    return wrapper;
24✔
324
  }
325

326
  /**
327
   * OnClick handler for orientation buttons
328
   *
329
   * @param {any} event handler event
330
   * @param {Array} orientationButtons orientation HTML button elements.
331
   * @returns {void}
332
   */
333
  handleOrientationButtonClick(event, orientationButtons) {
334
    const isActive = event.target.classList.contains(this.cssClasses.active);
2✔
335
    orientationButtons.forEach((button) => {
2✔
336
      this.nodes[button].classList.remove(this.cssClasses.active);
6✔
337
    });
338

339
    if (!isActive) {
2!
340
      event.target.classList.add(this.cssClasses.active);
2✔
341
      this.queryOrientation = event.target.innerHTML.toLowerCase();
2✔
NEW
342
    } else this.queryOrientation = null;
×
343

344
    const query = this.nodes.searchInput.innerHTML;
2✔
345
    if (query) {
2!
346
      this.showLoader();
2✔
347
      this.performSearch();
2✔
348
    }
349
  }
350
}
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

© 2025 Coveralls, Inc