@ebl-vue/editor-full 1.0.13 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,900 @@
1
+
2
+ //import Cropper from 'cropperjs';
3
+ //import 'cropperjs/dist/cropper.css';
4
+ import './index.css';
5
+ import type { BlockTune, API, BlockAPI } from '@ebl-vue/editorjs/types'
6
+
7
+
8
+ export interface TuneSetting {
9
+ name: string;
10
+ icon: string;
11
+ label: string;
12
+ group: string;
13
+ }
14
+
15
+ export interface ImageToolTuneData {
16
+ floatLeft: boolean;
17
+ floatRight: boolean;
18
+ center: boolean;
19
+ sizeSmall: boolean;
20
+ sizeMiddle: boolean;
21
+ sizeLarge: boolean;
22
+ resize: boolean;
23
+ resizeSize: number;
24
+ crop: boolean;
25
+ cropperFrameHeight: number;
26
+ cropperFrameWidth: number;
27
+ cropperFrameLeft: number;
28
+ cropperFrameTop: number;
29
+ cropperImageHeight: number;
30
+ cropperImageWidth: number;
31
+ cropperInterface?: any//Cropper;
32
+ }
33
+
34
+ export interface ImageToolTuneConfig {
35
+ resize: boolean;
36
+ crop: boolean;
37
+ }
38
+
39
+ export interface CustomStyles {
40
+ settingsButton: string;
41
+ settingsButtonActive: string;
42
+ settingsButtonModifier?: string;
43
+ settingsButtonModifierActive?: string;
44
+ }
45
+
46
+ export type ImageToolTuneConstructor = {
47
+ api: API;
48
+ data: Partial<ImageToolTuneData>;
49
+ config?: ImageToolTuneConfig;
50
+ block: BlockAPI;
51
+ };
52
+
53
+ export default class ImageToolTune implements BlockTune {
54
+ private settings: TuneSetting[];
55
+ private api: API;
56
+ private block: BlockAPI;
57
+ private data: ImageToolTuneData;
58
+ private wrapper: HTMLElement | undefined;
59
+ private buttons: HTMLElement[];
60
+ private styles: CustomStyles;
61
+
62
+ constructor({ api, data, config, block }: ImageToolTuneConstructor) {
63
+ this.settings = [
64
+ // {
65
+ // name: 'resize',
66
+ // icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M29 30l1 90h36V66h26V30H29zm99 0v36h72V30h-72zm108 0v36h72V30h-72zm108 0v36h72V30h-72zm102 0v78h36V30h-36zm-206 80v36h100.543l-118 118H30v218h218V289.457l118-118V272h36V110H240zm206 34v72h36v-72h-36zM30 156v72h36v-72H30zm416 96v72h36v-72h-36zm0 108v72h36v-72h-36zm-166 86v36h72v-36h-72zm108 0v36h72v-36h-72z"></path></svg>',
67
+ // label: '',
68
+ // group: 'size',
69
+ // },
70
+ // {
71
+ // name: 'crop',
72
+ // icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M21 15h2v2h-2v-2zm0-4h2v2h-2v-2zm2 8h-2v2c1 0 2-1 2-2zM13 3h2v2h-2V3zm8 4h2v2h-2V7zm0-4v2h2c0-1-1-2-2-2zM1 7h2v2H1V7zm16-4h2v2h-2V3zm0 16h2v2h-2v-2zM3 3C2 3 1 4 1 5h2V3zm6 0h2v2H9V3zM5 3h2v2H5V3zm-4 8v8c0 1.1.9 2 2 2h12V11H1zm2 8l2.5-3.21 1.79 2.15 2.5-3.22L13 19H3z"></path></svg>',
73
+ // label: '',
74
+ // group: 'size',
75
+ // },
76
+ ];
77
+
78
+ this.api = api;
79
+ this.block = block;
80
+ this.data = {
81
+ floatLeft: data?.floatLeft ?? false,
82
+ floatRight: data?.floatRight ?? false,
83
+ center: data?.center ?? false,
84
+ sizeSmall: data?.sizeSmall ?? false,
85
+ sizeMiddle: data?.sizeMiddle ?? false,
86
+ sizeLarge: data?.sizeLarge ?? false,
87
+ resize: data?.resize ?? config?.resize ?? false,
88
+ resizeSize: data?.resizeSize ?? 0,
89
+ crop: data?.crop ?? config?.crop ?? false,
90
+ cropperFrameHeight: data?.cropperFrameHeight ?? 0,
91
+ cropperFrameWidth: data?.cropperFrameWidth ?? 0,
92
+ cropperFrameLeft: data?.cropperFrameLeft ?? 0,
93
+ cropperFrameTop: data?.cropperFrameTop ?? 0,
94
+ cropperImageHeight: data?.cropperImageHeight ?? 0,
95
+ cropperImageWidth: data?.cropperImageWidth ?? 0,
96
+ cropperInterface: undefined,
97
+ };
98
+ this.wrapper = undefined;
99
+ this.buttons = [];
100
+ this.styles = {
101
+ settingsButton: 'cdx-settings-button',
102
+ settingsButtonActive: 'cdx-settings-button--active',
103
+ settingsButtonModifier: '',
104
+ settingsButtonModifierActive: '',
105
+ };
106
+ }
107
+
108
+ static get isTune(): boolean {
109
+ return true;
110
+ }
111
+
112
+ static get sanitize(): Record<string, object> {
113
+ return {
114
+ floatLeft: {},
115
+ floatRight: {},
116
+ center: {},
117
+ sizeSmall: {},
118
+ sizeMiddle: {},
119
+ sizeLarge: {},
120
+ resize: {},
121
+ resizeSize: {},
122
+ crop: {},
123
+ cropperFrameHeight: {},
124
+ cropperFrameWidth: {},
125
+ cropperFrameLeft: {},
126
+ cropperFrameTop: {},
127
+ cropperImageHeight: {},
128
+ cropperImageWidth: {},
129
+ cropperInterface: {},
130
+ };
131
+ }
132
+
133
+ /**
134
+ * CSS classes
135
+ * @return {object}
136
+ * @constructor
137
+ * @property {string} CSS.wrapper - wrapper for buttons
138
+ * @property {string} CSS.button - button
139
+ * @property {string} CSS.buttonActive - active button
140
+ * @property {string} CSS.buttonModifier - button with modifier
141
+ * @property {string} CSS.buttonModifierActive - active button with modifier
142
+ */
143
+ get CSS(): Record<string, string> {
144
+ return {
145
+ wrapper: 'cdx-image-tool-tune',
146
+ button: this.styles.settingsButton,
147
+ buttonActive: this.styles.settingsButtonActive,
148
+ buttonModifier: this.styles.settingsButtonModifier || '',
149
+ buttonModifierActive: this.styles.settingsButtonModifierActive || '',
150
+ isFloatLeft: 'cdx-image-tool-tune--floatLeft',
151
+ isFloatRight: 'cdx-image-tool-tune--floatRight',
152
+ isCenter: 'cdx-image-tool-tune--center',
153
+ isSizeSmall: 'cdx-image-tool-tune--sizeSmall',
154
+ isSizeMiddle: 'cdx-image-tool-tune--sizeMiddle',
155
+ isSizeLarge: 'cdx-image-tool-tune--sizeLarge',
156
+ isResize: 'cdx-image-tool-tune--resize',
157
+ isCrop: 'cdx-image-tool-tune--crop',
158
+ };
159
+ }
160
+
161
+ /**
162
+ *
163
+ * @return {HTMLElement}
164
+ * @public
165
+ * @readonly
166
+ * @property {HTMLElement} wrapper - tune buttons wrapper
167
+ */
168
+ get view(): HTMLElement {
169
+ if (!this.wrapper) {
170
+ this.wrapper = this.createView();
171
+ }
172
+
173
+ return this.wrapper;
174
+ }
175
+
176
+ /**
177
+ * Clicks to one of the tunes
178
+ * @param {MouseEvent} e - click
179
+ * @param {HTMLElement} tune - clicked tune button
180
+ * @private
181
+ * @return {void}
182
+ * */
183
+ tuneClicked(e: MouseEvent, tune: HTMLElement): void {
184
+ e.preventDefault();
185
+ e.stopPropagation();
186
+
187
+ const tuneName = tune.dataset.tune || '';
188
+ const tuneGroup = this.settings.find(t => t.name === tuneName)?.group;
189
+
190
+ this.buttons.forEach(button => {
191
+ //if is the same group
192
+ if (
193
+ this.settings.find(t => t.name === button.dataset.tune)?.group ===
194
+ tuneGroup
195
+ ) {
196
+ if (button !== tune) {
197
+ button.classList.remove(this.CSS.buttonActive);
198
+ }
199
+ }
200
+ });
201
+
202
+ tune.classList.toggle(this.CSS.buttonActive);
203
+ this.setTune(tuneName);
204
+ }
205
+
206
+ /**
207
+ * Styles the image with a tune
208
+ * @param {string} tune - tune name
209
+ * @private
210
+ * @return {void}
211
+ * */
212
+ setTune(tune: string): void {
213
+ switch (tune) {
214
+ case 'floatLeft':
215
+ this.data.floatLeft = !this.data.floatLeft;
216
+ this.data.floatRight = false;
217
+ this.data.center = false;
218
+ break;
219
+ case 'floatRight':
220
+ this.data.floatLeft = false;
221
+ this.data.floatRight = !this.data.floatRight;
222
+ this.data.center = false;
223
+ break;
224
+ case 'center':
225
+ this.data.center = !this.data.center;
226
+ this.data.floatLeft = false;
227
+ this.data.floatRight = false;
228
+ break;
229
+ case 'sizeSmall':
230
+ this.data.sizeSmall = !this.data.sizeSmall;
231
+ this.data.sizeMiddle = false;
232
+ this.data.sizeLarge = false;
233
+ this.data.resize = false;
234
+ this.data.crop = false;
235
+ break;
236
+ case 'sizeMiddle':
237
+ this.data.sizeSmall = false;
238
+ this.data.sizeMiddle = !this.data.sizeMiddle;
239
+ this.data.sizeLarge = false;
240
+ this.data.resize = false;
241
+ this.data.crop = false;
242
+ break;
243
+ case 'sizeLarge':
244
+ this.data.sizeSmall = false;
245
+ this.data.sizeMiddle = false;
246
+ this.data.sizeLarge = !this.data.sizeLarge;
247
+ this.data.resize = false;
248
+ this.data.crop = false;
249
+ break;
250
+ case 'resize':
251
+ this.data.sizeSmall = false;
252
+ this.data.sizeMiddle = false;
253
+ this.data.sizeLarge = false;
254
+ this.data.resize = !this.data.resize;
255
+ this.data.crop = false;
256
+ break;
257
+ case 'crop':
258
+ this.data.crop = !this.data.crop;
259
+ this.data.sizeSmall = false;
260
+ this.data.sizeMiddle = false;
261
+ this.data.sizeLarge = false;
262
+ this.data.resize = false;
263
+ this.data.resizeSize = 0;
264
+ break;
265
+ default:
266
+ this.data.floatLeft = false;
267
+ this.data.floatRight = false;
268
+ this.data.sizeSmall = false;
269
+ this.data.sizeMiddle = false;
270
+ this.data.sizeLarge = false;
271
+ this.data.resize = false;
272
+ this.data.crop = false;
273
+ break;
274
+ }
275
+
276
+ if (!this.data.resize) {
277
+ this.data.resizeSize = 0;
278
+ }
279
+
280
+ if (!this.data.crop) {
281
+ this.data.cropperFrameHeight = 0;
282
+ this.data.cropperFrameWidth = 0;
283
+ this.data.cropperFrameLeft = 0;
284
+ this.data.cropperFrameTop = 0;
285
+ this.data.cropperImageHeight = 0;
286
+ this.data.cropperImageWidth = 0;
287
+ }
288
+
289
+ const blockContent = this.block.holder.querySelector(
290
+ '.ce-block__content',
291
+ ) as HTMLElement;
292
+ this.apply(blockContent);
293
+ this.block.dispatchChange();
294
+ }
295
+
296
+ /**
297
+ * Append class to block by tune data
298
+ * @param {HTMLElement} blockContent - wrapper for block content
299
+ * @public
300
+ * @return {void}
301
+ * */
302
+ apply(blockContent: HTMLElement): void {
303
+ if (this.data.floatLeft) {
304
+ blockContent.classList.add(this.CSS.isFloatLeft);
305
+ } else {
306
+ blockContent.classList.remove(this.CSS.isFloatLeft);
307
+ }
308
+
309
+ if (this.data.floatRight) {
310
+ blockContent.classList.add(this.CSS.isFloatRight);
311
+ } else {
312
+ blockContent.classList.remove(this.CSS.isFloatRight);
313
+ }
314
+
315
+ if (this.data.center) {
316
+ blockContent.classList.add(this.CSS.isCenter);
317
+ } else {
318
+ blockContent.classList.remove(this.CSS.isCenter);
319
+ }
320
+
321
+ if (this.data.sizeSmall) {
322
+ blockContent.classList.add(this.CSS.isSizeSmall);
323
+ } else {
324
+ blockContent.classList.remove(this.CSS.isSizeSmall);
325
+ }
326
+
327
+ if (this.data.sizeMiddle) {
328
+ blockContent.classList.add(this.CSS.isSizeMiddle);
329
+ } else {
330
+ blockContent.classList.remove(this.CSS.isSizeMiddle);
331
+ }
332
+
333
+ if (this.data.sizeLarge) {
334
+ blockContent.classList.add(this.CSS.isSizeLarge);
335
+ } else {
336
+ blockContent.classList.remove(this.CSS.isSizeLarge);
337
+ }
338
+
339
+ if (this.data.resize) {
340
+ blockContent.classList.add(this.CSS.isResize);
341
+
342
+ if (this.data.resizeSize > 0) {
343
+ const cdxBlock = blockContent.getElementsByClassName(
344
+ 'cdx-block',
345
+ )[0] as HTMLElement;
346
+ cdxBlock.style.width = this.data.resizeSize + 'px';
347
+ }
348
+
349
+ this.resize(blockContent);
350
+ } else {
351
+ blockContent.classList.remove(this.CSS.isResize);
352
+ this.unresize(blockContent);
353
+ }
354
+
355
+ if (this.data.crop) {
356
+ blockContent.classList.add(this.CSS.isCrop);
357
+
358
+ this.crop(blockContent);
359
+ if (this.data.cropperFrameHeight > 0 && this.data.cropperFrameWidth > 0) {
360
+ this.applyCrop(blockContent);
361
+ }
362
+ } else {
363
+ blockContent.classList.remove(this.CSS.isCrop);
364
+ this.uncrop(blockContent);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Add crop handles to image
370
+ * @param {HTMLElement} blockContent - wrapper for block content
371
+ * @public
372
+ * @return {void}
373
+ */
374
+ crop(blockContent: HTMLElement): void {
375
+ //add append crop button to image-tool__image
376
+ //If editor is readOnly, do not add crop button
377
+ if (this.api.readOnly.isEnabled) return;
378
+
379
+ const image = blockContent.getElementsByClassName(
380
+ 'image-tool__image',
381
+ )[0] as HTMLElement;
382
+ const cropBtn = document.createElement('div');
383
+ cropBtn.classList.add('crop-btn', 'btn-crop-action');
384
+ cropBtn.innerHTML = this.api.i18n.t('Crop');
385
+
386
+ cropBtn.addEventListener('click', () => {
387
+ //remove crop button
388
+ image.removeChild(cropBtn);
389
+ this.appendCrop(blockContent);
390
+ });
391
+
392
+ image.appendChild(cropBtn);
393
+ }
394
+
395
+ appendCrop(blockContent: HTMLElement): void {
396
+ if (this.api.readOnly.isEnabled) return;
397
+
398
+ this.uncrop(blockContent);
399
+ const cdxBlock = blockContent.getElementsByClassName(
400
+ 'cdx-block',
401
+ )[0] as HTMLElement;
402
+ const image = cdxBlock.getElementsByTagName('img')[0] as HTMLImageElement;
403
+ cdxBlock.classList.add('isCropping');
404
+ this.data.cropperInterface = new Cropper(image);
405
+
406
+ //append save crop button
407
+ const cropSaveBtn = document.createElement('div');
408
+ cropSaveBtn.classList.add('crop-save', 'btn-crop-action');
409
+ cropSaveBtn.innerHTML = this.api.i18n.t('Apply');
410
+
411
+ cropSaveBtn.addEventListener('click', () => {
412
+ if (this.data.cropperInterface) {
413
+ this.data.cropperFrameHeight =
414
+ this.data.cropperInterface.getCropBoxData().height;
415
+ this.data.cropperFrameWidth =
416
+ this.data.cropperInterface.getCropBoxData().width;
417
+ this.data.cropperFrameLeft =
418
+ this.data.cropperInterface.getCanvasData().left -
419
+ this.data.cropperInterface.getCropBoxData().left;
420
+ this.data.cropperFrameTop =
421
+ this.data.cropperInterface.getCanvasData().top -
422
+ this.data.cropperInterface.getCropBoxData().top;
423
+ this.data.cropperImageHeight =
424
+ this.data.cropperInterface.getImageData().height;
425
+ this.data.cropperImageWidth =
426
+ this.data.cropperInterface.getImageData().width;
427
+ }
428
+ this.applyCrop(blockContent);
429
+ });
430
+
431
+ const imageToolImage = blockContent.getElementsByClassName(
432
+ 'image-tool__image',
433
+ )[0] as HTMLElement;
434
+ imageToolImage.appendChild(cropSaveBtn);
435
+
436
+ //add temporary style to block content so that it comes in front of every other block
437
+ blockContent.classList.add('isCropping');
438
+ }
439
+
440
+ applyCrop(blockContent: HTMLElement): void {
441
+ //apply data to image and remove cropper interface and save button, add crop button
442
+ const blockEl = blockContent.getElementsByClassName(
443
+ 'cdx-block',
444
+ )[0] as HTMLElement;
445
+ if (blockEl) {
446
+ blockEl.style.minWidth = this.data.cropperFrameWidth + 'px';
447
+ blockEl.style.maxWidth = this.data.cropperFrameWidth + 'px';
448
+
449
+ const image = blockEl.getElementsByTagName('img')[0] as HTMLImageElement;
450
+ image.style.width = this.data.cropperImageWidth + 'px';
451
+ image.style.height = this.data.cropperImageHeight + 'px';
452
+
453
+ const blockImg = blockContent.getElementsByClassName(
454
+ 'image-tool__image',
455
+ )[0] as HTMLElement;
456
+ blockImg.style.width = this.data.cropperFrameWidth + 'px';
457
+ blockImg.style.height = this.data.cropperFrameHeight + 'px';
458
+
459
+ const imageEl = blockImg.getElementsByTagName(
460
+ 'img',
461
+ )[0] as HTMLImageElement;
462
+ if (imageEl) {
463
+ imageEl.style.left = this.data.cropperFrameLeft + 'px';
464
+ imageEl.style.top = this.data.cropperFrameTop + 'px';
465
+ imageEl.classList.add('isCropped');
466
+ }
467
+
468
+ blockEl.classList.remove('isCropping');
469
+
470
+ const cropSaveBtn = blockContent.getElementsByClassName(
471
+ 'btn-crop-action',
472
+ )[0] as HTMLElement;
473
+ if (cropSaveBtn) {
474
+ blockImg.removeChild(cropSaveBtn);
475
+ }
476
+ }
477
+
478
+ //remove cropper interface
479
+ if (this.data.cropperInterface) {
480
+ this.data.cropperInterface.destroy();
481
+ this.data.cropperInterface = undefined;
482
+ }
483
+
484
+ //add crop button
485
+ if (this.api.readOnly.isEnabled) return;
486
+ const cropBtn = document.createElement('div');
487
+ cropBtn.classList.add('crop-btn', 'btn-crop-action');
488
+ cropBtn.innerHTML = this.api.i18n.t('Crop');
489
+
490
+ const imageToolImage = blockContent.getElementsByClassName(
491
+ 'image-tool__image',
492
+ )[0] as HTMLElement;
493
+ if (imageToolImage) {
494
+ cropBtn.addEventListener('click', () => {
495
+ //remove crop button
496
+ imageToolImage.removeChild(cropBtn);
497
+ this.appendCrop(blockContent);
498
+ });
499
+
500
+ imageToolImage.appendChild(cropBtn);
501
+ }
502
+
503
+ blockContent.classList.remove('isCropping');
504
+ this.block.dispatchChange();
505
+ }
506
+
507
+ uncrop(blockContent: HTMLElement): void {
508
+ if (this.api.readOnly.isEnabled) return;
509
+
510
+ const imageEl = blockContent.getElementsByClassName(
511
+ 'image-tool__image',
512
+ )[0] as HTMLElement;
513
+
514
+ //remove crop and save button
515
+ const cropSaveBtn = blockContent.getElementsByClassName(
516
+ 'btn-crop-action',
517
+ )[0] as HTMLElement;
518
+ if (cropSaveBtn && imageEl) {
519
+ imageEl.removeChild(cropSaveBtn);
520
+ }
521
+
522
+ //remove crop button
523
+ const cropBtn = blockContent.getElementsByClassName(
524
+ 'btn-crop-action',
525
+ )[0] as HTMLElement;
526
+ if (cropBtn && imageEl) {
527
+ imageEl.removeChild(cropBtn);
528
+ }
529
+
530
+ //remove isCropped class
531
+ const blockEl = blockContent.getElementsByClassName(
532
+ 'cdx-block',
533
+ )[0] as HTMLElement;
534
+ if (blockEl) {
535
+ const image = blockEl.getElementsByTagName('img')[0] as HTMLImageElement;
536
+ if (image) image.classList.remove('isCropped');
537
+
538
+ //remove isCropping class
539
+ blockEl.classList.remove('isCropping');
540
+
541
+ //remove min and max width
542
+ blockEl.style.minWidth = '';
543
+ blockEl.style.maxWidth = '';
544
+ }
545
+
546
+ if (imageEl) {
547
+ //remove image width and height
548
+ imageEl.style.width = '';
549
+ imageEl.style.height = '';
550
+
551
+ //remove image left and top
552
+ const image = imageEl.getElementsByTagName('img')[0] as HTMLImageElement;
553
+ if (image) {
554
+ image.style.left = '';
555
+ image.style.top = '';
556
+
557
+ //remove image width and height
558
+ image.style.width = '';
559
+ image.style.height = '';
560
+ }
561
+ }
562
+
563
+ blockContent.classList.remove('isCropping');
564
+
565
+ //remove cropper interface
566
+ if (this.data.cropperInterface) {
567
+ this.data.cropperInterface.destroy();
568
+ this.data.cropperInterface = undefined;
569
+ }
570
+
571
+ //remove crop data
572
+ this.data.cropperFrameHeight = 0;
573
+ this.data.cropperFrameWidth = 0;
574
+ this.data.cropperFrameLeft = 0;
575
+ this.data.cropperFrameTop = 0;
576
+ this.data.cropperImageHeight = 0;
577
+ this.data.cropperImageWidth = 0;
578
+ }
579
+
580
+ /**
581
+ * Add resize handles to block
582
+ * @param {HTMLElement} blockContent - wrapper for block content
583
+ * @public
584
+ * @return {void}
585
+ * */
586
+ resize(blockContent: HTMLElement): void {
587
+ if (this.api.readOnly.isEnabled) return;
588
+ const resizable = document.createElement('div');
589
+ resizable.classList.add('resizable');
590
+
591
+ const resizers = document.createElement('div');
592
+ resizers.classList.add('resizers');
593
+
594
+ const resizerTopRight = document.createElement('div');
595
+ resizerTopRight.classList.add('resizer', 'top-right');
596
+ resizerTopRight.addEventListener('mousedown', e => {
597
+ this.resizeClick(
598
+ blockContent.getElementsByClassName('cdx-block')[0] as HTMLElement,
599
+ resizerTopRight,
600
+ e,
601
+ );
602
+ });
603
+
604
+ const resizerBottomRight = document.createElement('div');
605
+ resizerBottomRight.classList.add('resizer', 'bottom-right');
606
+ resizerBottomRight.addEventListener('mousedown', e => {
607
+ this.resizeClick(
608
+ blockContent.getElementsByClassName('cdx-block')[0] as HTMLElement,
609
+ resizerBottomRight,
610
+ e,
611
+ );
612
+ });
613
+
614
+ resizers.appendChild(resizerTopRight);
615
+ resizers.appendChild(resizerBottomRight);
616
+ resizable.appendChild(resizers);
617
+ blockContent.getElementsByClassName('cdx-block')[0].appendChild(resizable);
618
+ }
619
+
620
+ /**
621
+ * click event to resize handles
622
+ * preserve aspect ratio
623
+ * prevent block from moving when dragging resize handles
624
+ * max size = 100%
625
+ * min size = 50px
626
+ * @param {HTMLElement} blockContent - wrapper for block content
627
+ * @param {HTMLElement} handle - resize handle
628
+ * @param {MouseEvent} e - mouse event
629
+ * @public
630
+ * @return {void}
631
+ * */
632
+ resizeClick(blockContent: HTMLElement, _: HTMLElement, e: MouseEvent): void {
633
+ const maxWidth =
634
+ document.getElementsByClassName('codex-editor')[0].clientWidth;
635
+
636
+ let startX = 0;
637
+ let startWidth = 0;
638
+
639
+ const mouseMoveHandler = (e: MouseEvent) => {
640
+ const dx = e.clientX - startX;
641
+ const newWidth = startWidth + dx;
642
+
643
+ if (newWidth > 50 && newWidth < maxWidth) {
644
+ (
645
+ blockContent as HTMLElement & { style: CSSStyleDeclaration }
646
+ ).style.width = newWidth + 'px';
647
+ }
648
+ };
649
+
650
+ const mouseUpHandler = () => {
651
+ const blockWidth = parseInt(
652
+ window.getComputedStyle(blockContent).width,
653
+ 10,
654
+ );
655
+
656
+ if (blockWidth > 0) {
657
+ this.data.resizeSize = blockWidth;
658
+ }
659
+
660
+ document.removeEventListener('mousemove', mouseMoveHandler);
661
+ document.removeEventListener('mouseup', mouseUpHandler);
662
+
663
+ this.block.dispatchChange();
664
+ };
665
+
666
+ document.addEventListener('mousemove', mouseMoveHandler);
667
+ document.addEventListener('mouseup', mouseUpHandler);
668
+
669
+ startX = e.clientX;
670
+ startWidth = parseInt(window.getComputedStyle(blockContent).width, 10);
671
+ }
672
+
673
+ /**
674
+ * Remove resize handles from block
675
+ * @param {HTMLElement} blockContent - wrapper for block content
676
+ * @public
677
+ * @return {void}
678
+ */
679
+ unresize(blockContent: HTMLElement): void {
680
+ const unresizable = blockContent.getElementsByClassName(
681
+ 'resizable',
682
+ )[0] as HTMLElement;
683
+ if (unresizable) {
684
+ blockContent
685
+ .getElementsByClassName('cdx-block')[0]
686
+ .removeChild(unresizable);
687
+ }
688
+
689
+ const block: HTMLElement = blockContent.getElementsByClassName(
690
+ 'cdx-block',
691
+ )[0] as HTMLElement;
692
+ block.style.width = 'auto';
693
+ }
694
+
695
+ /**
696
+ * Remove tunes from block wrapper
697
+ * @param {HTMLElement} blockContent - wrapper for block content
698
+ * @public
699
+ * @return {HTMLElement}
700
+ */
701
+ unwrap(blockContent: HTMLElement): HTMLElement {
702
+ //remove tunes from block
703
+ this.buttons.forEach(button => {
704
+ button.classList.remove(this.CSS.buttonActive);
705
+ });
706
+
707
+ //remove isFloatLeft class
708
+ blockContent.classList.remove(this.CSS.isFloatLeft);
709
+
710
+ //remove isFloatRight class
711
+ blockContent.classList.remove(this.CSS.isFloatRight);
712
+
713
+ //remove isCenter class
714
+ blockContent.classList.remove(this.CSS.isCenter);
715
+
716
+ //remove isSizeSmall class
717
+ blockContent.classList.remove(this.CSS.isSizeSmall);
718
+
719
+ //remove isSizeMiddle class
720
+ blockContent.classList.remove(this.CSS.isSizeMiddle);
721
+
722
+ //remove isSizeLarge class
723
+ blockContent.classList.remove(this.CSS.isSizeLarge);
724
+
725
+ //remove isResize class
726
+ blockContent.classList.remove(this.CSS.isResize);
727
+
728
+ //remove isCrop class
729
+ blockContent.classList.remove(this.CSS.isCrop);
730
+
731
+ //remove isCropped class
732
+ const cdxBlock = blockContent.getElementsByClassName(
733
+ 'cdx-block',
734
+ )[0] as HTMLElement;
735
+ const img = cdxBlock.getElementsByTagName('img')[0] as HTMLImageElement;
736
+ img.classList.remove('isCropped');
737
+
738
+ //remove isCropping class
739
+ cdxBlock.classList.remove('isCropping');
740
+
741
+ //remove min and max width
742
+ cdxBlock.style.minWidth = '';
743
+ cdxBlock.style.maxWidth = '';
744
+
745
+ //remove image width and height
746
+ const imageToolImage = blockContent.getElementsByClassName(
747
+ 'image-tool__image',
748
+ )[0] as HTMLElement;
749
+ imageToolImage.style.width = '';
750
+ imageToolImage.style.height = '';
751
+
752
+ //remove image left and top
753
+ const image = imageToolImage.getElementsByTagName(
754
+ 'img',
755
+ )[0] as HTMLImageElement;
756
+ image.style.left = '';
757
+ image.style.top = '';
758
+
759
+ //remove image width and height
760
+ image.style.width = '';
761
+ image.style.height = '';
762
+
763
+ //remove resize handles
764
+ this.unresize(blockContent);
765
+
766
+ //remove crop handles
767
+ this.uncrop(blockContent);
768
+
769
+ //remove cropper interface
770
+ if (this.data.cropperInterface) {
771
+ this.data.cropperInterface.destroy();
772
+ this.data.cropperInterface = undefined;
773
+ }
774
+
775
+ //remove crop data
776
+ this.data.cropperFrameHeight = 0;
777
+ this.data.cropperFrameWidth = 0;
778
+ this.data.cropperFrameLeft = 0;
779
+ this.data.cropperFrameTop = 0;
780
+ this.data.cropperImageHeight = 0;
781
+ this.data.cropperImageWidth = 0;
782
+
783
+ return blockContent;
784
+ }
785
+
786
+ /**
787
+ * Add tune to block data
788
+ * @private
789
+ * @return {ImageToolTuneData}
790
+ * */
791
+ save(): ImageToolTuneData {
792
+ return this.data;
793
+ }
794
+
795
+ /**
796
+ * Append tunes to block wrapper
797
+ * @param {HTMLElement} blockContent - wrapper for block content
798
+ * @public
799
+ * @return {HTMLElement}
800
+ * */
801
+ wrap(blockContent: HTMLElement): HTMLElement {
802
+ //createview if not exists
803
+ if (!this.wrapper) {
804
+ this.wrapper = this.createView();
805
+ }
806
+
807
+ this.apply(blockContent);
808
+ return blockContent;
809
+ }
810
+
811
+ private tuneNameToI18nKey(tuneName: string): string {
812
+ const translation: Record<string, string> = {
813
+ crop: 'Crop',
814
+ resize: 'Resize',
815
+ };
816
+ return translation[tuneName];
817
+ }
818
+
819
+ /**
820
+ * Creates a view for tunes
821
+ * @return {HTMLElement}
822
+ * @private
823
+ * */
824
+ createView(): HTMLElement {
825
+ this.buttons = this.settings.map(tune => {
826
+ const el = document.createElement('div');
827
+ const buttonIco = document.createElement('span');
828
+ const buttonTxt = document.createElement('span');
829
+ el.classList.add(this.CSS.button);
830
+ buttonTxt.style.fontSize = '8px';
831
+ buttonIco.innerHTML = tune.icon;
832
+ buttonTxt.innerHTML = tune.label;
833
+ el.appendChild(buttonIco);
834
+ el.appendChild(buttonTxt);
835
+ el.dataset.tune = tune.name;
836
+ el.title = tune.label;
837
+
838
+ el.addEventListener('click', e => this.tuneClicked(e, el));
839
+ this.api.tooltip.onHover(
840
+ el,
841
+ this.api.i18n.t(this.tuneNameToI18nKey(tune.name)),
842
+ );
843
+ return el;
844
+ });
845
+ const wrapper = document.createElement('div');
846
+ this.buttons.forEach(button => {
847
+ wrapper.appendChild(button);
848
+ });
849
+ wrapper.classList.add(this.CSS.wrapper);
850
+ return wrapper;
851
+ }
852
+
853
+ /**
854
+ * Checks if tune is active
855
+ * @param {string} tune - tune name
856
+ * @return {boolean}
857
+ * @private
858
+ * */
859
+ isTuneActive(tune: string): boolean {
860
+ return !!this.data[tune as keyof ImageToolTuneData];
861
+ }
862
+
863
+ /**
864
+ * Makes buttons with tunes
865
+ * @return {HTMLElement}
866
+ * @public
867
+ * */
868
+ render(): HTMLElement {
869
+ //when editor is ready
870
+ this.buttons.forEach(button => {
871
+ const tuneName = button.dataset.tune || '';
872
+ button.classList.toggle(
873
+ this.CSS.buttonActive,
874
+ this.isTuneActive(tuneName),
875
+ );
876
+ });
877
+
878
+ return this.view;
879
+ }
880
+
881
+ /**
882
+ * Destroys the plugin
883
+ * @public
884
+ * @return {void}
885
+ * */
886
+ destroy(): void {
887
+ this.wrapper = undefined;
888
+ this.buttons = [];
889
+ }
890
+
891
+ /**
892
+ * Toggle tune
893
+ * @param {string} tuneName - tune name
894
+ * @private
895
+ * @return {void}
896
+ * */
897
+ _toggleTune(tuneName: string): void {
898
+ this.setTune(tuneName);
899
+ }
900
+ }