Skip to content

Commit 7119e61

Browse files
committed
feat: add file upload animation to ProgressBar and update documentation
1 parent dbbf0fa commit 7119e61

7 files changed

Lines changed: 294 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
> - :house: [Internal]
1010
> - :nail_care: [Polish]
1111

12+
## 4.11.5
13+
14+
#### :rocket: New Feature
15+
16+
- `ProgressBar.showFileUploadAnimation(from?, to?)` — animated file icon that flies from a given point and fades out. Coordinates are relative to the editor container. Both `from` and `to` are optional with sensible defaults. The animation is automatically cleaned up on `destruct()`.
17+
```js
18+
const editor = Jodit.make('#editor')
19+
jodit.progressbar.showFileUploadAnimation();
20+
```
21+
1222
## 4.11.4
1323

1424
#### :boom: Breaking Change

src/core/ui/progress-bar/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,43 @@
11
# Progress Bar UI element
2+
3+
Displays a thin progress bar at the top of the editor workplace and provides a file upload animation.
4+
5+
## Usage
6+
7+
```typescript
8+
const bar = jodit.progressbar;
9+
10+
// Show the progress bar
11+
bar.show();
12+
13+
// Update progress (0–100)
14+
bar.progress(45);
15+
16+
// Hide the progress bar
17+
bar.hide();
18+
```
19+
20+
## File upload animation
21+
22+
Animates a file icon that flies from a start point to an end point and fades out.
23+
Coordinates are relative to the editor container. The animation element is rendered
24+
in a separate container (like popups) so it is not clipped by `overflow` on the workplace.
25+
26+
```typescript
27+
// Default: starts at top-center of the editor, flies up-right
28+
bar.showFileUploadAnimation();
29+
30+
// Custom start position (relative to editor container)
31+
bar.showFileUploadAnimation({ x: 100, y: 50 });
32+
33+
// Custom start and end positions
34+
bar.showFileUploadAnimation(
35+
{ x: 100, y: 50 }, // from
36+
{ x: 300, y: -100 } // to
37+
);
38+
```
39+
40+
If `from` is omitted, defaults to `{ x: containerWidth / 2, y: 0 }`.
41+
If `to` is omitted, defaults to `{ x: from.x + 60, y: from.y - 80 }`.
42+
43+
Calling `destruct()` stops and removes any running animation.

src/core/ui/progress-bar/progress-bar.less

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
.jodit-progress-bar {
2424
position: absolute;
25-
z-index: 2147483647;
25+
z-index: var(--z-index-tooltip);
2626
top: 0;
2727
left: 0;
2828
height: 2px;
@@ -63,4 +63,25 @@
6363
clip: rect(-6px, 22px, 14px, var(--padding-default));
6464
}
6565
}
66+
67+
&__file-animation {
68+
position: fixed;
69+
width: 32px;
70+
height: 32px;
71+
opacity: 0.8;
72+
pointer-events: none;
73+
z-index: 2147483647;
74+
transform: scale(1);
75+
transition:
76+
left 800ms ease-in,
77+
top 800ms ease-in,
78+
opacity 800ms ease-in,
79+
transform 800ms ease-in;
80+
81+
svg {
82+
width: 100%;
83+
height: 100%;
84+
fill: var(--color-background-progress);
85+
}
86+
}
6687
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*!
2+
* Jodit Editor (https://xdsoft.net/jodit/)
3+
* Released under MIT see LICENSE.txt in the project root for license information.
4+
* Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5+
*/
6+
7+
describe('Test ProgressBar', () => {
8+
const { ProgressBar } = Jodit.modules;
9+
10+
let editor;
11+
beforeEach(() => {
12+
editor = getJodit();
13+
});
14+
15+
describe('show', () => {
16+
it('should append progress bar to workplace', () => {
17+
const bar = new ProgressBar(editor);
18+
bar.show();
19+
expect(editor.workplace.contains(bar.container)).to.be.true;
20+
bar.destruct();
21+
});
22+
});
23+
24+
describe('hide', () => {
25+
it('should remove progress bar from DOM', () => {
26+
const bar = new ProgressBar(editor);
27+
bar.show();
28+
expect(editor.workplace.contains(bar.container)).to.be.true;
29+
bar.hide();
30+
expect(editor.workplace.contains(bar.container)).to.be.false;
31+
});
32+
});
33+
34+
describe('progress', () => {
35+
it('should set container width as percentage', () => {
36+
const bar = new ProgressBar(editor);
37+
bar.show();
38+
bar.progress(50);
39+
expect(bar.container.style.width).eq('50%');
40+
bar.destruct();
41+
});
42+
43+
it('should return self for chaining', () => {
44+
const bar = new ProgressBar(editor);
45+
expect(bar.progress(10)).eq(bar);
46+
bar.destruct();
47+
});
48+
});
49+
50+
describe('destruct', () => {
51+
it('should remove progress bar from DOM on destruct', () => {
52+
const bar = new ProgressBar(editor);
53+
bar.show();
54+
expect(editor.workplace.contains(bar.container)).to.be.true;
55+
bar.destruct();
56+
expect(editor.workplace.contains(bar.container)).to.be.false;
57+
});
58+
});
59+
60+
describe('showFileUploadAnimation', () => {
61+
const sel = '.jodit-progress-bar__file-animation';
62+
63+
it('should create animation element in DOM', () => {
64+
const bar = new ProgressBar(editor);
65+
bar.showFileUploadAnimation();
66+
67+
expect(document.querySelector(sel)).is.not.null;
68+
bar.destruct();
69+
});
70+
71+
it('should contain an SVG icon inside animation element', () => {
72+
const bar = new ProgressBar(editor);
73+
bar.showFileUploadAnimation();
74+
75+
const svg = document.querySelector(sel + ' svg');
76+
expect(svg).is.not.null;
77+
bar.destruct();
78+
});
79+
80+
it('should apply position based on editor container rect', () => {
81+
const bar = new ProgressBar(editor);
82+
const rect = editor.container.getBoundingClientRect();
83+
bar.showFileUploadAnimation({ x: 100, y: 50 });
84+
85+
const animEl = document.querySelector(sel);
86+
// End position = rect offset + to (default to = from + 60, from - 80)
87+
expect(animEl.style.left).eq(rect.left + 160 + 'px');
88+
expect(animEl.style.top).eq(rect.top + -30 + 'px');
89+
bar.destruct();
90+
});
91+
92+
it('should use custom from and to positions', () => {
93+
const bar = new ProgressBar(editor);
94+
const rect = editor.container.getBoundingClientRect();
95+
bar.showFileUploadAnimation({ x: 10, y: 20 }, { x: 200, y: 300 });
96+
97+
const animEl = document.querySelector(sel);
98+
expect(animEl.style.left).eq(rect.left + 200 + 'px');
99+
expect(animEl.style.top).eq(rect.top + 300 + 'px');
100+
bar.destruct();
101+
});
102+
103+
it('should set opacity to 0 for fade out', () => {
104+
const bar = new ProgressBar(editor);
105+
bar.showFileUploadAnimation();
106+
107+
const animEl = document.querySelector(sel);
108+
expect(animEl.style.opacity).eq('0');
109+
bar.destruct();
110+
});
111+
112+
it('should remove animation element on destruct', () => {
113+
const bar = new ProgressBar(editor);
114+
bar.showFileUploadAnimation();
115+
116+
expect(document.querySelector(sel)).is.not.null;
117+
bar.destruct();
118+
expect(document.querySelector(sel)).is.null;
119+
});
120+
121+
it('should remove previous animation if called again', () => {
122+
const bar = new ProgressBar(editor);
123+
bar.showFileUploadAnimation();
124+
bar.showFileUploadAnimation();
125+
126+
expect(document.querySelectorAll(sel).length).eq(1);
127+
bar.destruct();
128+
});
129+
130+
it('should remove element after transitionend event', () => {
131+
const bar = new ProgressBar(editor);
132+
bar.showFileUploadAnimation();
133+
134+
const animEl = document.querySelector(sel);
135+
expect(animEl).is.not.null;
136+
137+
animEl.dispatchEvent(new Event('transitionend'));
138+
139+
expect(document.querySelector(sel)).is.null;
140+
bar.destruct();
141+
});
142+
});
143+
});

src/core/ui/progress-bar/progress-bar.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
* @module ui/progress-bar
1111
*/
1212

13-
import type { IJodit, IProgressBar } from 'jodit/types';
13+
import type { IJodit, IProgressBar, Nullable } from 'jodit/types';
1414
import { Dom } from 'jodit/core/dom/dom';
15+
import { getContainer } from 'jodit/core/global';
16+
import { position } from 'jodit/core/helpers/size/position';
17+
import { css } from 'jodit/core/helpers/utils/css';
1518
import { UIElement } from 'jodit/core/ui/element';
19+
import { Icon } from 'jodit/core/ui/icon';
1620

1721
import './progress-bar.less';
1822

@@ -46,7 +50,70 @@ export class ProgressBar extends UIElement implements IProgressBar {
4650
return this;
4751
}
4852

53+
private __animationElement: Nullable<HTMLElement> = null;
54+
55+
showFileUploadAnimation(
56+
from?: { x: number; y: number },
57+
to?: { x: number; y: number }
58+
): void {
59+
this.__cleanUpAnimation();
60+
61+
const box = getContainer(this.j, ProgressBar);
62+
const pos = position(this.j.container, this.j);
63+
64+
const el = this.j.c.div(this.getFullElName('file-animation'));
65+
66+
const iconSvg = Icon.get('file', '');
67+
if (iconSvg) {
68+
el.innerHTML = iconSvg;
69+
}
70+
71+
const start = from ?? {
72+
x: pos.width / 2,
73+
y: 0
74+
};
75+
76+
const end = to ?? {
77+
x: start.x + 60,
78+
y: start.y - 80
79+
};
80+
81+
css(el, {
82+
left: pos.left + start.x,
83+
top: pos.top + start.y
84+
});
85+
86+
box.appendChild(el);
87+
this.__animationElement = el;
88+
89+
// Force reflow before starting transition
90+
// eslint-disable-next-line no-unused-expressions
91+
el.offsetWidth;
92+
93+
css(el, {
94+
left: pos.left + end.x,
95+
top: pos.top + end.y,
96+
opacity: 0,
97+
transform: 'scale(0.4)'
98+
});
99+
100+
const onEnd = (): void => {
101+
el.removeEventListener('transitionend', onEnd);
102+
this.__cleanUpAnimation();
103+
};
104+
105+
el.addEventListener('transitionend', onEnd);
106+
}
107+
108+
private __cleanUpAnimation(): void {
109+
if (this.__animationElement) {
110+
Dom.safeRemove(this.__animationElement);
111+
this.__animationElement = null;
112+
}
113+
}
114+
49115
override destruct(): any {
116+
this.__cleanUpAnimation();
50117
this.hide();
51118
return super.destruct();
52119
}

src/types/toolbar.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,12 @@ export interface IProgressBar extends IUIElement {
367367
hide(): IProgressBar;
368368

369369
progress(percentage: number): IProgressBar;
370+
371+
/**
372+
* Show file upload animation - file icon flies from `from` to `to` and fades out
373+
*/
374+
showFileUploadAnimation(
375+
from?: { x: number; y: number },
376+
to?: { x: number; y: number }
377+
): void;
370378
}

test/loader.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)