Skip to content

Commit 6816986

Browse files
authored
Add custom editor tabs (#1136)
- add custom editor tab , to render html elements in tab - handlers to show image, audio, video in editor tab instead of popup - api to use in plugins - some exceptions handled
1 parent 21a4c10 commit 6816986

File tree

13 files changed

+1530
-907
lines changed

13 files changed

+1530
-907
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import "./style.scss";
2+
3+
export default class AudioPlayer {
4+
constructor(container) {
5+
this.container = container;
6+
this.audio = new Audio();
7+
this.isPlaying = false;
8+
this.initializeUI();
9+
this.initializeEvents();
10+
this.cleanup = this.cleanup.bind(this);
11+
}
12+
13+
initializeUI() {
14+
const auidoPlayer = (
15+
<div className="audio-player">
16+
<button className="play-btn" ariaLabel="Play/Pause">
17+
<span className="icon play_arrow"></span>
18+
</button>
19+
20+
<div className="timeline">
21+
<div className="progress"></div>
22+
<div className="progress-handle"></div>
23+
</div>
24+
25+
<div className="time">0:00</div>
26+
27+
<div className="volume-control">
28+
<button className="volume-btn" ariaLabel="Volume"></button>
29+
</div>
30+
</div>
31+
);
32+
33+
this.container.appendChild(auidoPlayer);
34+
35+
this.elements = {
36+
playBtn: this.container.querySelector(".play-btn"),
37+
playIcon: this.container.querySelector(".play-btn .icon"),
38+
timeline: this.container.querySelector(".timeline"),
39+
progress: this.container.querySelector(".progress"),
40+
progressHandle: this.container.querySelector(".progress-handle"),
41+
timeDisplay: this.container.querySelector(".time"),
42+
duration: this.container.querySelector(".duration"),
43+
volumeBtn: this.container.querySelector(".volume-btn"),
44+
};
45+
this.elements.volumeBtn.innerHTML = `<svg viewBox="0 0 24 24">
46+
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
47+
</svg>`;
48+
}
49+
50+
initializeEvents() {
51+
// Play/Pause
52+
this.elements.playBtn.addEventListener("click", () => this.togglePlay());
53+
54+
// Timeline
55+
this.elements.timeline.addEventListener("click", (e) => this.seek(e));
56+
this.elements.timeline.addEventListener("touchstart", (e) => this.seek(e));
57+
58+
// Volume
59+
this.elements.volumeBtn.addEventListener("click", () => this.toggleMute());
60+
61+
// Audio events
62+
this.audio.addEventListener("timeupdate", () => this.updateProgress());
63+
this.audio.addEventListener("ended", () => this.audioEnded());
64+
}
65+
66+
togglePlay() {
67+
if (this.isPlaying) {
68+
this.audio.pause();
69+
this.elements.playIcon.classList.remove("pause");
70+
this.elements.playIcon.classList.add("play_arrow");
71+
} else {
72+
this.audio.play();
73+
this.elements.playIcon.classList.remove("play_arrow");
74+
this.elements.playIcon.classList.add("pause");
75+
}
76+
this.isPlaying = !this.isPlaying;
77+
}
78+
79+
seek(e) {
80+
const rect = this.elements.timeline.getBoundingClientRect();
81+
const pos =
82+
(e.type.includes("touch") ? e.touches[0].clientX : e.clientX) - rect.left;
83+
const percentage = pos / rect.width;
84+
this.audio.currentTime = percentage * this.audio.duration;
85+
}
86+
87+
updateProgress() {
88+
const percentage = (this.audio.currentTime / this.audio.duration) * 100;
89+
this.elements.progress.style.width = `${percentage}%`;
90+
this.elements.progressHandle.style.left = `${percentage}%`;
91+
this.elements.timeDisplay.textContent = this.formatTime(
92+
this.audio.currentTime,
93+
);
94+
}
95+
96+
formatTime(seconds) {
97+
const mins = Math.floor(seconds / 60);
98+
const secs = Math.floor(seconds % 60);
99+
return `${mins}:${secs.toString().padStart(2, "0")}`;
100+
}
101+
102+
toggleMute() {
103+
this.audio.muted = !this.audio.muted;
104+
if (this.audio.muted) {
105+
this.elements.volumeBtn.innerHTML =
106+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/></svg>';
107+
} else {
108+
this.elements.volumeBtn.innerHTML =
109+
'<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>';
110+
}
111+
}
112+
113+
audioEnded() {
114+
this.isPlaying = false;
115+
this.elements.playIcon.classList.remove("pause");
116+
this.elements.playIcon.classList.add("play_arrow");
117+
}
118+
119+
loadTrack(src) {
120+
this.audio.src = src;
121+
this.audio.load();
122+
}
123+
124+
cleanup() {
125+
this.audio.pause();
126+
this.audio.currentTime = 0;
127+
this.isPlaying = false;
128+
129+
this.elements.playBtn.removeEventListener("click", () => this.togglePlay());
130+
this.elements.timeline.removeEventListener("click", (e) => this.seek(e));
131+
this.elements.timeline.removeEventListener("touchstart", (e) =>
132+
this.seek(e),
133+
);
134+
this.elements.volumeBtn.removeEventListener("click", () =>
135+
this.toggleMute(),
136+
);
137+
this.audio.removeEventListener("timeupdate", () => this.updateProgress());
138+
this.audio.removeEventListener("ended", () => this.audioEnded());
139+
140+
const audioSrc = this.audio.src;
141+
this.audio.src = "";
142+
this.audio.load();
143+
if (audioSrc.startsWith("blob:")) {
144+
URL.revokeObjectURL(audioSrc);
145+
}
146+
}
147+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
.audio-player {
2+
background: var(--primary-color, #1e1e1e);
3+
border-radius: 10px;
4+
padding: 15px;
5+
display: flex;
6+
align-items: center;
7+
gap: 15px;
8+
width: 100%;
9+
max-width: 400px;
10+
user-select: none;
11+
12+
.play-btn {
13+
background: transparent;
14+
border: none;
15+
width: 30px;
16+
height: 30px;
17+
border-radius: 50%;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
cursor: pointer;
22+
transition: all 0.2s;
23+
24+
span {
25+
font-size: 20px;
26+
color: var(--primary-text-color);
27+
}
28+
29+
&:hover {
30+
background: color-mix(in srgb, var(--secondary-color) 30%, transparent);
31+
}
32+
}
33+
34+
.timeline {
35+
flex: 1;
36+
height: 4px;
37+
background: color-mix(in srgb, var(--secondary-color) 60%, transparent);
38+
border-radius: 2px;
39+
position: relative;
40+
cursor: pointer;
41+
42+
&:hover .progress-handle {
43+
opacity: 1;
44+
}
45+
}
46+
47+
.progress {
48+
background: var(--primary-text-color, #fff);
49+
width: 0%;
50+
height: 100%;
51+
border-radius: 2px;
52+
transition: width 0.1s linear;
53+
}
54+
55+
.progress-handle {
56+
width: 12px;
57+
height: 12px;
58+
background: var(--primary-text-color, #fff);
59+
border-radius: 50%;
60+
position: absolute;
61+
top: 50%;
62+
transform: translate(-50%, -50%);
63+
pointer-events: none;
64+
opacity: 0;
65+
transition: opacity 0.2s;
66+
}
67+
68+
.time {
69+
color: var(--primary-text-color, #fff);
70+
font-family: monospace;
71+
font-size: 12px;
72+
min-width: 45px;
73+
}
74+
75+
.volume-control {
76+
display: flex;
77+
align-items: center;
78+
gap: 8px;
79+
}
80+
81+
.volume-btn {
82+
background: transparent;
83+
border: none;
84+
cursor: pointer;
85+
86+
svg {
87+
width: 20px;
88+
height: 20px;
89+
fill: var(--primary-text-color, #fff);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)