@rmdes/indiekit-frontend 1.0.0-beta.28 → 1.0.0-beta.30
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.
|
@@ -21,6 +21,8 @@ const paths = {
|
|
|
21
21
|
"M9 2h22v4H9Zm0 12h22v4H9Zm0 12h22v4H9ZM3 7a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",
|
|
22
22
|
"upload-image":
|
|
23
23
|
"M2 0h28a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2C0 .9.9 0 2 0Zm26 24-9-14-7 11-3-4.8L4 24h24Z",
|
|
24
|
+
"browse-media":
|
|
25
|
+
"M1 1h13v13H1V1Zm17 0h13v13H18V1ZM1 18h13v13H1V18Zm17 0h13v13H18V18Z",
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -105,7 +107,17 @@ export const TextareaFieldComponent = class extends HTMLElement {
|
|
|
105
107
|
"table",
|
|
106
108
|
"code",
|
|
107
109
|
"link",
|
|
108
|
-
...(this.editorImageUpload === "false"
|
|
110
|
+
...(this.editorImageUpload === "false"
|
|
111
|
+
? []
|
|
112
|
+
: [
|
|
113
|
+
"upload-image",
|
|
114
|
+
{
|
|
115
|
+
name: "browse-media",
|
|
116
|
+
action: () => this._openMediaBrowser(editor),
|
|
117
|
+
className: "browse-media",
|
|
118
|
+
title: "Browse media",
|
|
119
|
+
},
|
|
120
|
+
]),
|
|
109
121
|
"|",
|
|
110
122
|
"undo",
|
|
111
123
|
"side-by-side",
|
|
@@ -271,6 +283,233 @@ export const TextareaFieldComponent = class extends HTMLElement {
|
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Open media browser modal to browse and insert existing media files
|
|
288
|
+
* @param {EasyMDE} editor - EasyMDE instance
|
|
289
|
+
*/
|
|
290
|
+
_openMediaBrowser(editor) {
|
|
291
|
+
if (this._mediaBrowserOpen) return;
|
|
292
|
+
this._mediaBrowserOpen = true;
|
|
293
|
+
|
|
294
|
+
const endpoint = this.editorEndpoint;
|
|
295
|
+
let allItems = [];
|
|
296
|
+
let afterCursor = null;
|
|
297
|
+
let activeFilter = "all";
|
|
298
|
+
|
|
299
|
+
// Create modal overlay
|
|
300
|
+
const overlay = document.createElement("div");
|
|
301
|
+
overlay.className = "media-browser";
|
|
302
|
+
overlay.setAttribute("role", "dialog");
|
|
303
|
+
overlay.setAttribute("aria-modal", "true");
|
|
304
|
+
overlay.setAttribute("aria-label", "Browse media");
|
|
305
|
+
|
|
306
|
+
const modal = document.createElement("div");
|
|
307
|
+
modal.className = "media-browser__modal";
|
|
308
|
+
|
|
309
|
+
// Header
|
|
310
|
+
const header = document.createElement("div");
|
|
311
|
+
header.className = "media-browser__header";
|
|
312
|
+
|
|
313
|
+
const title = document.createElement("h2");
|
|
314
|
+
title.className = "media-browser__title";
|
|
315
|
+
title.textContent = "Browse media";
|
|
316
|
+
header.append(title);
|
|
317
|
+
|
|
318
|
+
const closeBtn = document.createElement("button");
|
|
319
|
+
closeBtn.type = "button";
|
|
320
|
+
closeBtn.className = "media-browser__close";
|
|
321
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
322
|
+
closeBtn.textContent = "\u00D7";
|
|
323
|
+
closeBtn.addEventListener("click", close);
|
|
324
|
+
header.append(closeBtn);
|
|
325
|
+
|
|
326
|
+
// Filters
|
|
327
|
+
const filters = document.createElement("div");
|
|
328
|
+
filters.className = "media-browser__filters";
|
|
329
|
+
|
|
330
|
+
for (const filter of ["all", "photo", "audio", "video"]) {
|
|
331
|
+
const btn = document.createElement("button");
|
|
332
|
+
btn.type = "button";
|
|
333
|
+
btn.className = "media-browser__filter";
|
|
334
|
+
if (filter === "all") btn.classList.add("is-active");
|
|
335
|
+
btn.textContent = filter.charAt(0).toUpperCase() + filter.slice(1);
|
|
336
|
+
btn.dataset.filter = filter;
|
|
337
|
+
btn.addEventListener("click", () => {
|
|
338
|
+
activeFilter = filter;
|
|
339
|
+
for (const f of filters.querySelectorAll(".media-browser__filter")) {
|
|
340
|
+
f.classList.toggle("is-active", f.dataset.filter === filter);
|
|
341
|
+
}
|
|
342
|
+
renderGrid();
|
|
343
|
+
});
|
|
344
|
+
filters.append(btn);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Grid
|
|
348
|
+
const grid = document.createElement("div");
|
|
349
|
+
grid.className = "media-browser__grid";
|
|
350
|
+
|
|
351
|
+
// Loading
|
|
352
|
+
const loading = document.createElement("div");
|
|
353
|
+
loading.className = "media-browser__loading";
|
|
354
|
+
loading.textContent = "Loading\u2026";
|
|
355
|
+
|
|
356
|
+
// Empty
|
|
357
|
+
const empty = document.createElement("p");
|
|
358
|
+
empty.className = "media-browser__empty";
|
|
359
|
+
empty.textContent = "No media files found.";
|
|
360
|
+
empty.hidden = true;
|
|
361
|
+
|
|
362
|
+
// Load more
|
|
363
|
+
const loadMoreBtn = document.createElement("button");
|
|
364
|
+
loadMoreBtn.type = "button";
|
|
365
|
+
loadMoreBtn.className = "media-browser__load-more";
|
|
366
|
+
loadMoreBtn.textContent = "Load more";
|
|
367
|
+
loadMoreBtn.hidden = true;
|
|
368
|
+
loadMoreBtn.addEventListener("click", () => fetchMedia());
|
|
369
|
+
|
|
370
|
+
modal.append(header, filters, grid, loading, empty, loadMoreBtn);
|
|
371
|
+
overlay.append(modal);
|
|
372
|
+
document.body.append(overlay);
|
|
373
|
+
|
|
374
|
+
// Lock body scroll
|
|
375
|
+
document.body.style.overflow = "hidden";
|
|
376
|
+
|
|
377
|
+
// Close handlers
|
|
378
|
+
overlay.addEventListener("click", (event) => {
|
|
379
|
+
if (event.target === overlay) close();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const onKeyDown = (event) => {
|
|
383
|
+
if (event.key === "Escape") close();
|
|
384
|
+
};
|
|
385
|
+
document.addEventListener("keydown", onKeyDown);
|
|
386
|
+
|
|
387
|
+
function close() {
|
|
388
|
+
overlay.remove();
|
|
389
|
+
document.body.style.overflow = "";
|
|
390
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
391
|
+
// Use arrow-free reference to component
|
|
392
|
+
editor.codemirror.focus();
|
|
393
|
+
// Reset flag on the component instance via closure
|
|
394
|
+
closeBrowser();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const closeBrowser = () => {
|
|
398
|
+
this._mediaBrowserOpen = false;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
function getFilteredItems() {
|
|
402
|
+
if (activeFilter === "all") return allItems;
|
|
403
|
+
return allItems.filter((item) => {
|
|
404
|
+
const type = item["media-type"] || "";
|
|
405
|
+
if (activeFilter === "photo") return type.startsWith("image/");
|
|
406
|
+
if (activeFilter === "audio") return type.startsWith("audio/");
|
|
407
|
+
if (activeFilter === "video") return type.startsWith("video/");
|
|
408
|
+
return true;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function getMediaIcon(mediaType) {
|
|
413
|
+
if (!mediaType) return "\uD83D\uDCC4";
|
|
414
|
+
if (mediaType.startsWith("audio/")) return "\uD83C\uDFB5";
|
|
415
|
+
if (mediaType.startsWith("video/")) return "\uD83C\uDFAC";
|
|
416
|
+
return "\uD83D\uDCC4";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function getFilename(url) {
|
|
420
|
+
try {
|
|
421
|
+
return decodeURIComponent(url.split("/").pop());
|
|
422
|
+
} catch {
|
|
423
|
+
return url.split("/").pop();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderGrid() {
|
|
428
|
+
grid.replaceChildren();
|
|
429
|
+
const filtered = getFilteredItems();
|
|
430
|
+
empty.hidden = filtered.length > 0;
|
|
431
|
+
|
|
432
|
+
for (const item of filtered) {
|
|
433
|
+
const url = item.url;
|
|
434
|
+
const mediaType = item["media-type"] || "";
|
|
435
|
+
const isImage = mediaType.startsWith("image/");
|
|
436
|
+
const filename = getFilename(url);
|
|
437
|
+
|
|
438
|
+
const tile = document.createElement("button");
|
|
439
|
+
tile.type = "button";
|
|
440
|
+
tile.className = "media-browser__item";
|
|
441
|
+
tile.title = filename;
|
|
442
|
+
tile.addEventListener("click", () => {
|
|
443
|
+
insertMedia(url, filename, isImage);
|
|
444
|
+
close();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (isImage) {
|
|
448
|
+
const img = document.createElement("img");
|
|
449
|
+
img.src = url;
|
|
450
|
+
img.alt = filename;
|
|
451
|
+
img.loading = "lazy";
|
|
452
|
+
img.className = "media-browser__thumbnail";
|
|
453
|
+
tile.append(img);
|
|
454
|
+
} else {
|
|
455
|
+
const icon = document.createElement("span");
|
|
456
|
+
icon.className = "media-browser__icon";
|
|
457
|
+
icon.textContent = getMediaIcon(mediaType);
|
|
458
|
+
tile.append(icon);
|
|
459
|
+
|
|
460
|
+
const name = document.createElement("span");
|
|
461
|
+
name.className = "media-browser__filename";
|
|
462
|
+
name.textContent = filename;
|
|
463
|
+
tile.append(name);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
grid.append(tile);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function insertMedia(url, filename, isImage) {
|
|
471
|
+
const cm = editor.codemirror;
|
|
472
|
+
const cursor = cm.getCursor();
|
|
473
|
+
const text = isImage ? `` : `[${filename}](${url})`;
|
|
474
|
+
cm.replaceRange(text, cursor);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function fetchMedia() {
|
|
478
|
+
loading.hidden = false;
|
|
479
|
+
loadMoreBtn.hidden = true;
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const url = new URL(endpoint, globalThis.location.origin);
|
|
483
|
+
url.searchParams.set("q", "source");
|
|
484
|
+
url.searchParams.set("limit", "20");
|
|
485
|
+
if (afterCursor) url.searchParams.set("after", afterCursor);
|
|
486
|
+
|
|
487
|
+
const response = await fetch(url.href, { credentials: "same-origin" });
|
|
488
|
+
if (!response.ok) throw new Error(response.statusText);
|
|
489
|
+
|
|
490
|
+
const data = await response.json();
|
|
491
|
+
const items = data.items || [];
|
|
492
|
+
allItems = allItems.concat(items);
|
|
493
|
+
|
|
494
|
+
afterCursor =
|
|
495
|
+
data.paging && data.paging.after ? data.paging.after : null;
|
|
496
|
+
loadMoreBtn.hidden = !afterCursor;
|
|
497
|
+
|
|
498
|
+
renderGrid();
|
|
499
|
+
} catch (error) {
|
|
500
|
+
const errorMsg = document.createElement("p");
|
|
501
|
+
errorMsg.className = "media-browser__error";
|
|
502
|
+
errorMsg.textContent = `Error loading media: ${error.message}`;
|
|
503
|
+
grid.replaceChildren(errorMsg);
|
|
504
|
+
} finally {
|
|
505
|
+
loading.hidden = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Initial fetch
|
|
510
|
+
fetchMedia();
|
|
511
|
+
}
|
|
512
|
+
|
|
274
513
|
/**
|
|
275
514
|
* Upload file
|
|
276
515
|
* @param {object} file - File
|
package/package.json
CHANGED
|
@@ -217,3 +217,161 @@ textarea-field[editor] {
|
|
|
217
217
|
background: rgb(255 255 255 / 25%);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
/* Media browser modal */
|
|
222
|
+
.media-browser {
|
|
223
|
+
background: rgb(0 0 0 / 50%);
|
|
224
|
+
display: flex;
|
|
225
|
+
inset: 0;
|
|
226
|
+
place-content: center;
|
|
227
|
+
place-items: center;
|
|
228
|
+
position: fixed;
|
|
229
|
+
z-index: 1100;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.media-browser__modal {
|
|
233
|
+
background: var(--color-background);
|
|
234
|
+
border-radius: var(--border-radius-small);
|
|
235
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 30%);
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
inline-size: min(56rem, calc(100% - 2rem));
|
|
239
|
+
max-block-size: 80vh;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.media-browser__header {
|
|
243
|
+
align-items: center;
|
|
244
|
+
border-block-end: var(--border-hairline);
|
|
245
|
+
display: flex;
|
|
246
|
+
justify-content: space-between;
|
|
247
|
+
padding: var(--space-s) var(--space-m);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.media-browser__title {
|
|
251
|
+
font: var(--font-subhead);
|
|
252
|
+
margin: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.media-browser__close {
|
|
256
|
+
background: none;
|
|
257
|
+
block-size: 2rem;
|
|
258
|
+
border: 0;
|
|
259
|
+
border-radius: var(--border-radius-small);
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
font-size: 1.5rem;
|
|
262
|
+
inline-size: 2rem;
|
|
263
|
+
line-height: 1;
|
|
264
|
+
|
|
265
|
+
&:hover {
|
|
266
|
+
background: var(--color-offset);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.media-browser__filters {
|
|
271
|
+
border-block-end: var(--border-hairline);
|
|
272
|
+
display: flex;
|
|
273
|
+
gap: var(--space-xs);
|
|
274
|
+
padding-block: 0;
|
|
275
|
+
padding-inline: var(--space-m);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.media-browser__filter {
|
|
279
|
+
background: none;
|
|
280
|
+
border: 0;
|
|
281
|
+
border-block-end: 2px solid transparent;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
font: var(--font-caption);
|
|
284
|
+
padding: var(--space-xs) var(--space-s);
|
|
285
|
+
|
|
286
|
+
&:hover {
|
|
287
|
+
color: var(--color-primary);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
&.is-active {
|
|
291
|
+
border-block-end-color: var(--color-primary);
|
|
292
|
+
color: var(--color-primary);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.media-browser__grid {
|
|
297
|
+
display: grid;
|
|
298
|
+
flex: 1;
|
|
299
|
+
gap: var(--space-xs);
|
|
300
|
+
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
|
301
|
+
overflow-y: auto;
|
|
302
|
+
padding: var(--space-m);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.media-browser__item {
|
|
306
|
+
aspect-ratio: 1;
|
|
307
|
+
background: var(--color-offset);
|
|
308
|
+
border: 2px solid transparent;
|
|
309
|
+
border-radius: var(--border-radius-small);
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-direction: column;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
place-content: center;
|
|
315
|
+
place-items: center;
|
|
316
|
+
|
|
317
|
+
&:hover,
|
|
318
|
+
&:focus-visible {
|
|
319
|
+
border-color: var(--color-primary);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.media-browser__thumbnail {
|
|
324
|
+
block-size: 100%;
|
|
325
|
+
inline-size: 100%;
|
|
326
|
+
object-fit: cover;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.media-browser__icon {
|
|
330
|
+
font-size: 2rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.media-browser__filename {
|
|
334
|
+
font: var(--font-caption);
|
|
335
|
+
max-inline-size: 100%;
|
|
336
|
+
overflow: hidden;
|
|
337
|
+
padding-inline: var(--space-2xs);
|
|
338
|
+
text-overflow: ellipsis;
|
|
339
|
+
white-space: nowrap;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.media-browser__loading {
|
|
343
|
+
font: var(--font-caption);
|
|
344
|
+
padding: var(--space-m);
|
|
345
|
+
text-align: center;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.media-browser__empty {
|
|
349
|
+
color: var(--color-on-offset);
|
|
350
|
+
font: var(--font-caption);
|
|
351
|
+
grid-column: 1 / -1;
|
|
352
|
+
padding: var(--space-l);
|
|
353
|
+
text-align: center;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.media-browser__error {
|
|
357
|
+
color: var(--color-error, #c00);
|
|
358
|
+
font: var(--font-caption);
|
|
359
|
+
grid-column: 1 / -1;
|
|
360
|
+
padding: var(--space-l);
|
|
361
|
+
text-align: center;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.media-browser__load-more {
|
|
365
|
+
background: var(--color-offset);
|
|
366
|
+
block-size: 2.5rem;
|
|
367
|
+
border: var(--border-hairline);
|
|
368
|
+
border-radius: var(--border-radius-small);
|
|
369
|
+
cursor: pointer;
|
|
370
|
+
font: var(--font-caption);
|
|
371
|
+
margin-block: 0 var(--space-m);
|
|
372
|
+
margin-inline: var(--space-m);
|
|
373
|
+
|
|
374
|
+
&:hover {
|
|
375
|
+
background: var(--color-offset-variant);
|
|
376
|
+
}
|
|
377
|
+
}
|