@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +665 -543
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
- package/src/components/Editor/Editor.vue +16 -5
- package/src/plugins/imageResizeCrop/ImageTune.ts +900 -0
- package/src/plugins/imageResizeCrop/index.css +234 -0
- package/src/plugins/imageResizeCrop/index.ts +5 -0
- package/src/plugins/imageResizeCrop/types.d.ts +23 -0
- package/src/plugins/imageTool/index.css +1 -1
- package/src/plugins/imageTool/index.ts +30 -11
- package/src/plugins/imageTool/types/types.ts +2 -0
- package/src/plugins/imageTool/ui.ts +3 -2
- package/src/plugins/imageTool/uploader.ts +137 -103
- package/src/plugins/imageTool/utils/index.ts +73 -0
- package/src/utils/AxiosService.ts +87 -0
- package/types/index.d.ts +1 -0
|
@@ -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
|
+
}
|