mikuru 1.0.28 → 1.0.29

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.29 - 2026-05-15
4
+
5
+ - Fixed `MikuruVideoPlayer` and `MikuruAudioPlayer` unmount handling so media events fired while a modal or conditional branch is being removed no longer update component refs recursively.
6
+ - Handled aborted `play()` promises during media player teardown to avoid uncaught browser `AbortError` / `DOMException` noise when closing a sample video quickly.
7
+
3
8
  ## 1.0.28 - 2026-05-15
4
9
 
5
10
  - Added package-exported Mikuru video player, audio player, image viewer, modal, carousel, toast, dropdown, tooltip, progress, and code block components with custom controls, template refs, lifecycle cleanup, keyboard-accessible seeking/navigation, volume, mute, playback rate, stop, fullscreen, close/select/dismiss events, progress states, and copy actions.
@@ -49,7 +49,7 @@
49
49
  </template>
50
50
 
51
51
  <script>
52
- import { computed, onMounted, ref } from "mikuru";
52
+ import { computed, onBeforeUnmount, onMounted, ref } from "mikuru";
53
53
 
54
54
  const {
55
55
  src,
@@ -76,16 +76,23 @@ const initials = computed(() => {
76
76
  const words = title.value.split(" ").filter(Boolean);
77
77
  return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
78
78
  });
79
+ let isDisposed = false;
79
80
 
80
81
  onMounted(() => {
81
82
  applyAudioSettings();
82
83
  });
83
84
 
85
+ onBeforeUnmount(() => {
86
+ isDisposed = true;
87
+ });
88
+
84
89
  function getMedia() {
90
+ if (isDisposed) return null;
85
91
  return mediaEl.value;
86
92
  }
87
93
 
88
94
  function syncMedia() {
95
+ if (isDisposed) return;
89
96
  const media = getMedia();
90
97
  if (!media) return;
91
98
  currentTime.value = media.currentTime || 0;
@@ -95,6 +102,7 @@ function syncMedia() {
95
102
  }
96
103
 
97
104
  function applyAudioSettings() {
105
+ if (isDisposed) return;
98
106
  const media = getMedia();
99
107
  if (!media) return;
100
108
  media.volume = volume.value;
@@ -102,10 +110,12 @@ function applyAudioSettings() {
102
110
  }
103
111
 
104
112
  function markPlaying() {
113
+ if (isDisposed) return;
105
114
  isPlaying.value = true;
106
115
  }
107
116
 
108
117
  function markPaused() {
118
+ if (isDisposed) return;
109
119
  isPlaying.value = false;
110
120
  }
111
121
 
@@ -113,13 +123,20 @@ async function togglePlayback() {
113
123
  const media = getMedia();
114
124
  if (!media) return;
115
125
  if (media.paused) {
116
- await media.play();
126
+ try {
127
+ await media.play();
128
+ } catch (error) {
129
+ if (!isDisposed && error?.name !== "AbortError") {
130
+ throw error;
131
+ }
132
+ }
117
133
  return;
118
134
  }
119
135
  media.pause();
120
136
  }
121
137
 
122
138
  function seek(event) {
139
+ if (isDisposed) return;
123
140
  const media = getMedia();
124
141
  const nextTime = Number(event.target.value);
125
142
  currentTime.value = nextTime;
@@ -129,6 +146,7 @@ function seek(event) {
129
146
  }
130
147
 
131
148
  function setVolume(event) {
149
+ if (isDisposed) return;
132
150
  const nextVolume = Number(event.target.value);
133
151
  volume.value = nextVolume;
134
152
  muted.value = nextVolume === 0;
@@ -136,6 +154,7 @@ function setVolume(event) {
136
154
  }
137
155
 
138
156
  function toggleMute() {
157
+ if (isDisposed) return;
139
158
  muted.value = !muted.value;
140
159
  applyAudioSettings();
141
160
  }
@@ -149,6 +168,7 @@ function skipForward() {
149
168
  }
150
169
 
151
170
  function skipBy(offset) {
171
+ if (isDisposed) return;
152
172
  const media = getMedia();
153
173
  if (!media) return;
154
174
  const nextTime = Math.min(Math.max(media.currentTime + offset, 0), safeDuration.value || media.currentTime + offset);
@@ -92,7 +92,7 @@
92
92
  </template>
93
93
 
94
94
  <script>
95
- import { computed, onMounted, onUnmounted, ref } from "mikuru";
95
+ import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from "mikuru";
96
96
 
97
97
  const {
98
98
  src,
@@ -130,21 +130,30 @@ const seekStyle = computed(() => "--progress: " + seekProgress.value + "%");
130
130
  const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video");
131
131
  const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
132
132
  const rates = [1, 1.25, 1.5, 2, 0.75];
133
+ let isDisposed = false;
133
134
 
134
135
  onMounted(() => {
135
136
  applyAudioSettings();
136
137
  document.addEventListener("fullscreenchange", updateFullscreen);
137
138
  });
138
139
 
140
+ onBeforeUnmount(() => {
141
+ isDisposed = true;
142
+ isSeeking.value = false;
143
+ pointerInside.value = false;
144
+ });
145
+
139
146
  onUnmounted(() => {
140
147
  document.removeEventListener("fullscreenchange", updateFullscreen);
141
148
  });
142
149
 
143
150
  function getMedia() {
151
+ if (isDisposed) return null;
144
152
  return mediaEl.value;
145
153
  }
146
154
 
147
155
  function syncMedia() {
156
+ if (isDisposed) return;
148
157
  const media = getMedia();
149
158
  if (!media) return;
150
159
  currentTime.value = media.currentTime || 0;
@@ -155,6 +164,7 @@ function syncMedia() {
155
164
  }
156
165
 
157
166
  function applyAudioSettings() {
167
+ if (isDisposed) return;
158
168
  const media = getMedia();
159
169
  if (!media) return;
160
170
  media.volume = volume.value;
@@ -163,16 +173,19 @@ function applyAudioSettings() {
163
173
  }
164
174
 
165
175
  function markPlaying() {
176
+ if (isDisposed) return;
166
177
  isPlaying.value = true;
167
178
  controlsVisible.value = pointerInside.value;
168
179
  }
169
180
 
170
181
  function markPaused() {
182
+ if (isDisposed) return;
171
183
  isPlaying.value = false;
172
184
  controlsVisible.value = true;
173
185
  }
174
186
 
175
187
  function updateFullscreen() {
188
+ if (isDisposed) return;
176
189
  isFullscreen.value = document.fullscreenElement === screenEl.value || document.fullscreenElement === mediaEl.value;
177
190
  }
178
191
 
@@ -180,7 +193,13 @@ async function togglePlayback() {
180
193
  const media = getMedia();
181
194
  if (!media) return;
182
195
  if (media.paused) {
183
- await media.play();
196
+ try {
197
+ await media.play();
198
+ } catch (error) {
199
+ if (!isDisposed && error?.name !== "AbortError") {
200
+ throw error;
201
+ }
202
+ }
184
203
  return;
185
204
  }
186
205
  media.pause();
@@ -194,6 +213,7 @@ function getPointerTime(event) {
194
213
  }
195
214
 
196
215
  function seekTo(nextTime) {
216
+ if (isDisposed) return;
197
217
  const media = getMedia();
198
218
  currentTime.value = nextTime;
199
219
  if (media) {
@@ -206,6 +226,7 @@ function seekFromPointer(event) {
206
226
  }
207
227
 
208
228
  function startSeek(event) {
229
+ if (isDisposed) return;
209
230
  isSeeking.value = true;
210
231
  controlsVisible.value = true;
211
232
  event.currentTarget.setPointerCapture?.(event.pointerId);
@@ -213,11 +234,13 @@ function startSeek(event) {
213
234
  }
214
235
 
215
236
  function dragSeek(event) {
237
+ if (isDisposed) return;
216
238
  if (!isSeeking.value) return;
217
239
  seekFromPointer(event);
218
240
  }
219
241
 
220
242
  function endSeek(event) {
243
+ if (isDisposed) return;
221
244
  if (!isSeeking.value) return;
222
245
  isSeeking.value = false;
223
246
  event.currentTarget.releasePointerCapture?.(event.pointerId);
@@ -225,17 +248,20 @@ function endSeek(event) {
225
248
  }
226
249
 
227
250
  function showControls() {
251
+ if (isDisposed) return;
228
252
  pointerInside.value = true;
229
253
  controlsVisible.value = true;
230
254
  }
231
255
 
232
256
  function hideControls() {
257
+ if (isDisposed) return;
233
258
  pointerInside.value = false;
234
259
  if (!isPlaying.value || isSeeking.value) return;
235
260
  controlsVisible.value = false;
236
261
  }
237
262
 
238
263
  function seekWithKeyboard(event) {
264
+ if (isDisposed) return;
239
265
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "Home" && event.key !== "End") {
240
266
  return;
241
267
  }
@@ -256,6 +282,7 @@ function seekWithKeyboard(event) {
256
282
  }
257
283
 
258
284
  function stopPlayback() {
285
+ if (isDisposed) return;
259
286
  const media = getMedia();
260
287
  if (!media) return;
261
288
  media.pause();
@@ -265,6 +292,7 @@ function stopPlayback() {
265
292
  }
266
293
 
267
294
  function setVolume(event) {
295
+ if (isDisposed) return;
268
296
  const nextVolume = Number(event.target.value);
269
297
  volume.value = nextVolume;
270
298
  muted.value = nextVolume === 0;
@@ -272,17 +300,20 @@ function setVolume(event) {
272
300
  }
273
301
 
274
302
  function toggleMute() {
303
+ if (isDisposed) return;
275
304
  muted.value = !muted.value;
276
305
  applyAudioSettings();
277
306
  }
278
307
 
279
308
  function cycleRate() {
309
+ if (isDisposed) return;
280
310
  const currentIndex = rates.indexOf(playbackRate.value);
281
311
  playbackRate.value = rates[(currentIndex + 1) % rates.length];
282
312
  applyAudioSettings();
283
313
  }
284
314
 
285
315
  function toggleFullscreen() {
316
+ if (isDisposed) return;
286
317
  if (document.fullscreenElement) {
287
318
  document.exitFullscreen?.();
288
319
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikuru",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A compile-first JavaScript framework with Vue-like authoring and Svelte-like generated DOM updates.",