Skip to content

Commit 23bf915

Browse files
committed
Enhance image editor functionality with 'Save as' prompt and refactor button definitions
1 parent 1ede00f commit 23bf915

6 files changed

Lines changed: 128 additions & 26 deletions

File tree

src/core/helpers/utils/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ export function call<T extends any[], R>(
4040
return func(...args);
4141
}
4242

43+
/**
44+
* Call function with parameters
45+
*
46+
* @example
47+
* ```js
48+
* const f = Math.random();
49+
* Jodit.modules.Helpers.call(f > 0.5 ? Math.ceil : Math.floor, f);
50+
* ```
51+
*/
52+
53+
export function callThis<This, T extends any[], R>(
54+
func: (this: This, ...args: T) => R,
55+
thisArg: This,
56+
...args: T
57+
): R {
58+
return func.apply(thisArg, args);
59+
}
60+
4361
/**
4462
* Mark element for debugging
4563
*/

src/modules/file-browser/data-provider.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ import type {
2626
Nullable
2727
} from 'jodit/types';
2828
import { IS_PROD } from 'jodit/core/constants';
29-
import {
30-
abort,
31-
ConfigProto,
32-
error,
33-
isFunction,
34-
normalizeRelativePath,
35-
set
36-
} from 'jodit/core/helpers';
37-
import { Ajax } from 'jodit/core/request';
29+
import { autobind } from 'jodit/core/decorators/autobind/autobind';
30+
import { isFunction } from 'jodit/core/helpers/checker/is-function';
31+
import { normalizeRelativePath } from 'jodit/core/helpers/normalize/normalize-relative-path';
32+
import { ConfigProto } from 'jodit/core/helpers/utils/config-proto';
33+
import { abort, error } from 'jodit/core/helpers/utils/error';
34+
import { set } from 'jodit/core/helpers/utils/set';
35+
import { Ajax } from 'jodit/core/request/ajax';
3836
import { FileBrowserItem } from 'jodit/modules/file-browser/builders/item';
3937

4038
export const DEFAULT_SOURCE_NAME = 'default';
@@ -127,13 +125,15 @@ export default class DataProvider implements IFileBrowserDataProvider {
127125

128126
private progressHandler = (ignore: number): void => {};
129127

128+
@autobind
130129
onProgress(callback: (percentage: number) => void): void {
131130
this.progressHandler = callback;
132131
}
133132

134133
/**
135134
* Load permissions for path and source
136135
*/
136+
@autobind
137137
async permissions(
138138
path: string,
139139
source: string
@@ -182,6 +182,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
182182
return null;
183183
}
184184

185+
@autobind
185186
canI(action: string): boolean {
186187
const rule: keyof IPermissions = 'allow' + action;
187188

@@ -238,19 +239,21 @@ export default class DataProvider implements IFileBrowserDataProvider {
238239
/**
239240
* Load items list by path and source
240241
*/
242+
@autobind
241243
items(
242244
path: string,
243245
source: string,
244246
mods: IFileBrowserDataProviderItemsMods = {}
245247
): Promise<IFileBrowserItem[]> {
246248
return this.__items(path, source, mods, resp =>
247-
this.generateItemsList(resp.data.sources, mods)
249+
this.__generateItemsList(resp.data.sources, mods)
248250
);
249251
}
250252

251253
/**
252254
* Load items list by path and source
253255
*/
256+
@autobind
254257
itemsEx(
255258
path: string,
256259
source: string,
@@ -260,12 +263,12 @@ export default class DataProvider implements IFileBrowserDataProvider {
260263
sources.reduce((acc, source) => acc + source.files.length, 0);
261264

262265
return this.__items(path, source, mods, resp => ({
263-
items: this.generateItemsList(resp.data.sources, mods),
266+
items: this.__generateItemsList(resp.data.sources, mods),
264267
loadedTotal: calcTotal(resp.data.sources)
265268
}));
266269
}
267270

268-
private generateItemsList(
271+
private __generateItemsList(
269272
sources: ISourcesFiles,
270273
mods: IFileBrowserDataProviderItemsMods = {}
271274
): IFileBrowserItem[] {
@@ -306,6 +309,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
306309
return elements;
307310
}
308311

312+
@autobind
309313
async tree(path: string, source: string): Promise<ISourcesFiles> {
310314
path = normalizeRelativePath(path);
311315

@@ -338,6 +342,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
338342
/**
339343
* Get path by url. You can use this method in another modules
340344
*/
345+
@autobind
341346
getPathByUrl(url: string): Promise<any> {
342347
set('options.getLocalFileByUrl.data.url', url, this);
343348

@@ -357,6 +362,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
357362
* @param path - Relative directory in which you want create a folder
358363
* @param source - Server source key
359364
*/
365+
@autobind
360366
createFolder(name: string, path: string, source: string): Promise<boolean> {
361367
const { create } = this.o;
362368

@@ -383,6 +389,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
383389
* @param filepath - The relative path to the file / folder source
384390
* @param path - Relative to the directory where you want to move the file / folder
385391
*/
392+
@autobind
386393
move(
387394
filepath: string,
388395
path: string,
@@ -419,7 +426,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
419426
* @param file - The filename
420427
* @param source - Source
421428
*/
422-
private remove(
429+
private __remove(
423430
action: 'fileRemove' | 'folderRemove',
424431
path: string,
425432
file: string,
@@ -451,8 +458,9 @@ export default class DataProvider implements IFileBrowserDataProvider {
451458
* @param file - The filename
452459
* @param source - Source
453460
*/
461+
@autobind
454462
fileRemove(path: string, file: string, source: string): Promise<string> {
455-
return this.remove('fileRemove', path, file, source);
463+
return this.__remove('fileRemove', path, file, source);
456464
}
457465

458466
/**
@@ -462,8 +470,9 @@ export default class DataProvider implements IFileBrowserDataProvider {
462470
* @param file - The filename
463471
* @param source - Source
464472
*/
473+
@autobind
465474
folderRemove(path: string, file: string, source: string): Promise<string> {
466-
return this.remove('folderRemove', path, file, source);
475+
return this.__remove('folderRemove', path, file, source);
467476
}
468477

469478
/**
@@ -474,7 +483,7 @@ export default class DataProvider implements IFileBrowserDataProvider {
474483
* @param newname - New name
475484
* @param source - Source
476485
*/
477-
private rename(
486+
private __rename(
478487
action: 'fileRename' | 'folderRename',
479488
path: string,
480489
name: string,
@@ -504,28 +513,30 @@ export default class DataProvider implements IFileBrowserDataProvider {
504513
/**
505514
* Rename folder
506515
*/
516+
@autobind
507517
folderRename(
508518
path: string,
509519
name: string,
510520
newname: string,
511521
source: string
512522
): Promise<string> {
513-
return this.rename('folderRename', path, name, newname, source);
523+
return this.__rename('folderRename', path, name, newname, source);
514524
}
515525

516526
/**
517527
* Rename file
518528
*/
529+
@autobind
519530
fileRename(
520531
path: string,
521532
name: string,
522533
newname: string,
523534
source: string
524535
): Promise<string> {
525-
return this.rename('fileRename', path, name, newname, source);
536+
return this.__rename('fileRename', path, name, newname, source);
526537
}
527538

528-
private changeImage(
539+
private __changeImage(
529540
type: 'resize' | 'crop',
530541
path: string,
531542
source: string,
@@ -565,37 +576,42 @@ export default class DataProvider implements IFileBrowserDataProvider {
565576
/**
566577
* Send command to server to crop image
567578
*/
579+
@autobind
568580
crop(
569581
path: string,
570582
source: string,
571583
name: string,
572584
newname: string | void,
573585
box: ImageBox | void
574586
): Promise<boolean> {
575-
return this.changeImage('crop', path, source, name, newname, box);
587+
return this.__changeImage('crop', path, source, name, newname, box);
576588
}
577589

578590
/**
579591
* Send command to server to resize image
580592
*/
593+
@autobind
581594
resize(
582595
path: string,
583596
source: string,
584597
name: string,
585598
newname: string | void,
586599
box: ImageBox | void
587600
): Promise<boolean> {
588-
return this.changeImage('resize', path, source, name, newname, box);
601+
return this.__changeImage('resize', path, source, name, newname, box);
589602
}
590603

604+
@autobind
591605
getMessage(resp: IFileBrowserAnswer): string {
592606
return this.options.getMessage(resp);
593607
}
594608

609+
@autobind
595610
isSuccess(resp: IFileBrowserAnswer): boolean {
596611
return this.options.isSuccess(resp);
597612
}
598613

614+
@autobind
599615
destruct(): any {
600616
this.__ajaxInstances.forEach(a => a.destruct());
601617
this.__ajaxInstances.clear();

src/modules/image-editor/image-editor.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,50 @@ describe('Image editor', () => {
240240
});
241241

242242
describe('Resize mode', () => {
243+
describe('Save as', () => {
244+
it('Should shoe prompt dialog', async () => {
245+
const editor = getJodit({
246+
uploader: {
247+
url: 'https://xdsoft.net/jodit/connector/index.php?action=upload'
248+
},
249+
filebrowser: {
250+
ajax: {
251+
url: 'https://xdsoft.net/jodit/connector/index.php'
252+
}
253+
}
254+
});
255+
256+
editor.value =
257+
'<p><img alt="artio" src="tests/artio.jpg"/></p>';
258+
259+
simulateEvent('dblclick', editor.editor.querySelector('img'));
260+
261+
const dialog = getOpenedDialog(editor);
262+
263+
const form = getForm(dialog);
264+
simulateEvent('click', form.getElm('editImage'));
265+
await new Promise(resolve =>
266+
editor.filebrowser.events.one('afterImageEditor', resolve)
267+
);
268+
269+
const imageEditor = getOpenedDialog(editor);
270+
expect(imageEditor).is.not.null;
271+
272+
clickButton('save-as', imageEditor);
273+
274+
const prompt = getOpenedDialog(editor);
275+
expect(prompt).is.not.null;
276+
expect(prompt).is.not.equals(dialog);
277+
const input = prompt.querySelector('input');
278+
expect(input).is.not.null;
279+
input.value = 'new-name';
280+
clickButton('ok', prompt);
281+
await editor.async.requestIdlePromise();
282+
expect(getOpenedDialog(editor)).eq(dialog);
283+
clickButton('ok', dialog);
284+
}).timeout(7000);
285+
});
286+
243287
describe('Enable ratio', () => {
244288
it('Should deny resize image without ratio', async () => {
245289
const editor = getJodit({

src/modules/image-editor/image-editor.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import type {
2525
import { ViewComponent } from 'jodit/core/component';
2626
import { autobind, component, debounce, throttle } from 'jodit/core/decorators';
2727
import { Dom } from 'jodit/core/dom';
28-
import { $$, attr, call, css, refs, toArray, trim } from 'jodit/core/helpers';
28+
import { toArray } from 'jodit/core/helpers/array/to-array';
29+
import { trim } from 'jodit/core/helpers/string/trim';
30+
import { attr } from 'jodit/core/helpers/utils/attr';
31+
import { css } from 'jodit/core/helpers/utils/css';
32+
import { $$, refs } from 'jodit/core/helpers/utils/selector';
33+
import { call } from 'jodit/core/helpers/utils/utils';
2934
import { Button } from 'jodit/core/ui/button';
3035
import { Config } from 'jodit/config';
3136

@@ -738,7 +743,13 @@ export class ImageEditor extends ViewComponent<IViewWithToolbar & IDlgs> {
738743
this.buttons = {
739744
reset: Button(this.j, 'update', 'Reset'),
740745
save: Button(this.j, 'save', 'Save'),
741-
saveas: Button(this.j, 'save', 'Save as ...')
746+
saveas: Button(this.j, {
747+
icon: {
748+
name: 'save'
749+
},
750+
name: 'save-as',
751+
text: 'Save as ...'
752+
})
742753
};
743754

744755
this.activeTab = o.resize ? TABS.resize : TABS.crop;

src/plugins/search/search.test.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test.describe('Search screenshot testing', () => {
3131

3232
test.describe('Replace popup', () => {
3333
test('works', async function ({ page }) {
34-
await page.click('[data-ref="find"] [role="trigger"]');
34+
await page.click('[data-ref="find"] [aria-haspopup="true"]');
3535
await page.click('[data-ref="replace"]');
3636
await page.evaluate(() => {
3737
(document.activeElement as HTMLInputElement).blur();

test/bootstrap.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,13 @@ function mockAjax() {
328328
break;
329329
}
330330

331+
case 'imageResize':
332+
resolve({
333+
success: true,
334+
time: '2020-08-04 19:03:23',
335+
data: { code: 220 }
336+
});
337+
break;
331338
case 'folderRemove':
332339
case 'folderRename': {
333340
temp.folderName = ajax.options.data.newname;
@@ -1144,8 +1151,14 @@ function getButton(buttonName, joditOrElement, role, last) {
11441151
let button;
11451152

11461153
if (!/\s/.test(buttonName)) {
1147-
for (const className of classes) {
1148-
const prefix = `.${className}.${className}_${buttonName}${last ? ':last-child' : ''}`;
1154+
const variants = classes
1155+
.map(c => [
1156+
`.${c}.${c}_${buttonName}`,
1157+
`.${c}.${c}_${buttonName.replace(/-/g, '_')}`
1158+
])
1159+
.flat();
1160+
for (const className of variants) {
1161+
const prefix = `${className}${last ? ':last-child' : ''}`;
11491162
button = elm.querySelector(`${prefix} ${roleSelector}`);
11501163

11511164
if (button) {

0 commit comments

Comments
 (0)