• 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

87.18
/src/ui.js
1
import { make, createImageCredits } from './helpers';
2
import Tunes from './tunes';
3
import ControlPanel from './controlPanel';
4
import bgIcon from '../assets/backgroundIcon.svg';
5
import borderIcon from '../assets/borderIcon.svg';
6
import stretchedIcon from '../assets/toolboxIcon.svg';
7
import ModalHandler from './modalHandler';
8

9
/**
10
 * Class for working with UI:
11
 *  - rendering base structure
12
 *  - show/hide preview
13
 *  - apply tune view
14
 */
15
export default class Ui {
16
  /**
17
   * @param {{api: object, config: object, readOnly: Boolean,
18
   *   onAddImageData: Function, onTuneToggled: Function}}
19
   *   api - Editorjs API
20
   *   config - Tool custom config
21
   *   readOnly - read-only mode flag
22
   *   onAddImageData - Callback for adding image data
23
   *   onTuneToggled - Callback for updating tunes data
24
   */
25
  constructor({
26
    api, config, readOnly, onAddImageData, onTuneToggled,
27
  }) {
28
    this.api = api;
14✔
29
    this.config = config;
14✔
30
    this.readOnly = readOnly;
14✔
31
    this.onAddImageData = onAddImageData;
14✔
32
    this.onTuneToggled = onTuneToggled;
14✔
33

34
    this.CSS = {
14✔
35
      baseClass: this.api.styles.block,
36
      loading: this.api.styles.loader,
37
      input: this.api.styles.input,
38
      wrapper: 'inline-image',
39
      imageHolder: 'inline-image__picture',
40
      caption: 'inline-image__caption',
41
    };
42

43
    this.settings = [
14✔
44
      {
45
        name: 'withBorder',
46
        icon: borderIcon,
47
      },
48
      {
49
        name: 'stretched',
50
        icon: stretchedIcon,
51
      },
52
      {
53
        name: 'withBackground',
54
        icon: bgIcon,
55
      },
56
    ];
57

58
    this.controlPanel = new ControlPanel({
14✔
59
      api,
60
      config,
61
      readOnly,
62
      cssClasses: this.CSS,
63
      onSelectImage: (imageData) => this.selectImage(imageData),
×
64
    });
65

66
    this.tunes = new Tunes({
14✔
67
      cssClasses: {
68
        settingsButton: this.api.styles.settingsButton,
69
        settingsButtonActive: this.api.styles.settingsButtonActive,
70
      },
71
      settings: this.settings,
72
      onTuneToggled,
73
    });
74

75
    this.nodes = {
14✔
76
      wrapper: null,
77
      loader: null,
78
      imageHolder: null,
79
      image: null,
80
      caption: null,
81
      credits: null,
82
    };
83

84
    this.modal = new ModalHandler();
14✔
85
  }
86

87
  /**
88
   * Renders tool UI
89
   *
90
   * @param {Object} data Saved tool data
91
   * @returns {HTMLDivElement}
92
   */
93
  render(data) {
94
    const wrapper = make('div', [this.CSS.baseClass, this.CSS.wrapper]);
14✔
95
    const loader = make('div', this.CSS.loading);
14✔
96
    const image = make('img', '', {
14✔
97
      onload: () => this.onImageLoad(),
4✔
98
      onerror: () => this.onImageLoadError(),
2✔
99
    });
100
    const caption = make('div', [this.CSS.input, this.CSS.caption], {
14✔
101
      contentEditable: !this.readOnly,
102
      innerHTML: data.caption || '',
10✔
103
    });
104
    this.nodes.imageHolder = make('div', this.CSS.imageHolder);
14✔
105

106
    caption.dataset.placeholder = 'Enter a caption';
14✔
107

108
    if (data.url) {
14✔
109
      wrapper.appendChild(loader);
10✔
110
      image.src = data.url;
10✔
111
      this.buildImageCredits(data);
10✔
112
    } else {
113
      const controlPanelWrapper = this.controlPanel.render();
4✔
114
      this.nodes.controlPanelWrapper = controlPanelWrapper;
4✔
115
      wrapper.appendChild(controlPanelWrapper);
4✔
116
    }
117

118
    this.nodes.wrapper = wrapper;
14✔
119
    this.nodes.loader = loader;
14✔
120
    this.nodes.image = image;
14✔
121
    this.nodes.caption = caption;
14✔
122

123
    this.applySettings(data);
14✔
124

125
    this.setEvents();
14✔
126

127
    return wrapper;
14✔
128
  }
129

130
  /**
131
   * Builds Unsplash image credits element
132
   *
133
   * @param {Object} imageData Tool data
134
   * @returns {HTMLDivElement}
135
   */
136
  buildImageCredits(imageData) {
137
    const unsplashData = imageData.unsplash;
10✔
138
    if (unsplashData && unsplashData.author && unsplashData.profileLink) {
10✔
139
      const { appName } = this.config.unsplash;
8✔
140
      const credits = createImageCredits({ ...unsplashData, appName });
8✔
141
      this.nodes.imageHolder.appendChild(credits);
8✔
142
      this.nodes.credits = credits;
8✔
143
    }
144
  }
145

146
  /**
147
   * On image load callback
148
   * Shows the embedded image
149
   *
150
   * @returns {void}
151
   */
152
  onImageLoad() {
153
    this.nodes.imageHolder.prepend(this.nodes.image);
4✔
154
    this.nodes.wrapper.appendChild(this.nodes.imageHolder);
4✔
155
    this.nodes.wrapper.appendChild(this.nodes.caption);
4✔
156
    this.nodes.loader.remove();
4✔
157
  }
158

159
  /**
160
   * Callback fired when image fails on load.
161
   * It removes current editor block and notifies error
162
   *
163
   * @returns {void}
164
   */
165
  onImageLoadError() {
166
    this.removeCurrentBlock();
2✔
167
    this.api.notifier.show({
2✔
168
      message: 'Can not load the image, try again!',
169
      style: 'error',
170
    });
171
  }
172

173
  /**
174
   * Removes current block from editor
175
   *
176
   * @returns {void}
177
   */
178
  removeCurrentBlock() {
179
    Promise.resolve().then(() => {
2✔
180
      const blockIndex = this.api.blocks.getCurrentBlockIndex();
2✔
181

182
      this.api.blocks.delete(blockIndex);
2✔
183
    })
184
      .catch((err) => {
185
        // eslint-disable-next-line no-console
UNCOV
186
        console.error(err);
×
187
      });
188
  }
189

190
  /**
191
   * Makes buttons with tunes
192
   *
193
   * @returns {HTMLDivElement}
194
   */
195
  renderSettings(data) {
196
    return this.tunes.render(data);
×
197
  }
198

199
  /**
200
   * Shows a loader spinner
201
   *
202
   * @returns {void}
203
   */
204
  showLoader() {
205
    this.nodes.controlPanelWrapper.remove();
×
206
    this.nodes.wrapper.appendChild(this.nodes.loader);
×
207
  }
208

209
  /**
210
   * Callback fired when an image is embedded
211
   *
212
   * @param {Object} data Tool data
213
   * @returns {void}
214
   */
215
  selectImage(data) {
216
    this.onAddImageData(data);
×
217
    this.showLoader();
×
218
    this.buildImageCredits(data);
×
219
  }
220

221
  /**
222
   * Apply visual representation of activated tune
223
   *
224
   * @param {string} tuneName One of available tunes
225
   * @param {boolean} status True for enable, false for disable
226
   * @returns {void}
227
   */
228
  applyTune(tuneName, status) {
229
    this.nodes.imageHolder.classList.toggle(`${this.CSS.imageHolder}--${tuneName}`, status);
44✔
230

231
    if (tuneName === 'stretched') {
44✔
232
      Promise.resolve().then(() => {
14✔
233
        const blockIndex = this.api.blocks.getCurrentBlockIndex();
14✔
234
        this.api.blocks.stretchBlock(blockIndex, status);
14✔
235
      })
236
        .catch((err) => {
237
          // eslint-disable-next-line no-console
UNCOV
238
          console.error(err);
×
239
        });
240
    }
241
  }
242

243
  /**
244
   * Apply tunes to image from data
245
   *
246
   * @param {Object} data Tool data
247
   * @returns {void}
248
   */
249
  applySettings(data) {
250
    this.settings.forEach((tune) => {
14✔
251
      this.applyTune(tune.name, data[tune.name]);
42✔
252
    });
253
  }
254

255
  /**
256
   * Capture events in the Inline Image block
257
   */
258
  setEvents() {
259
    this.nodes.image.addEventListener('click', () => {
14✔
260
      this.modal.open(this.nodes.image.src);
×
261
    });
262
  }
263
}
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