@webx-ui/module-media 0.4.2 → 0.5.0
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/dist/index.js +132 -128
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
import { defineComponent as S, useModel as A, ref as M, watch as q, openBlock as b, createBlock as $, unref as e, mergeModels as F, computed as D, resolveDirective as Pe, withCtx as w, createElementBlock as W, Fragment as Q, renderList as Z, withDirectives as Oe, createCommentVNode as U, createVNode as h, createTextVNode as P, toDisplayString as L, isRef as te, useTemplateRef as ee, onMounted as he, nextTick as qe, createElementVNode as _, withKeys as Ne, normalizeClass as Ae, normalizeStyle as me } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useTranslate as B, useAdmin as ae } from "@webx-ui/module-admin";
|
|
3
|
+
import { WxTree as He, WxSelectionArea as Ie, WxFileCard as Xe, WxEmpty as Ke, toast as N, WxAction as T, WxInput as j, WxDropdown as pe, WxDropdownItem as fe, WxActions as G, useModal as le, WxDialog as ie, WxSelect as je, WxSpace as be, WxButton as I, useElementWidth as Ge, createModal as Y, WxPagination as Ye, WxDrawer as Je, confirm as ve, openImageEditor as Qe, WxImage as Ze, WxPopover as et, WxPopconfirm as tt, WxCard as at } from "@webx-ui/core";
|
|
4
|
+
function we(a) {
|
|
5
|
+
const d = `${a.apiPath}/media`, m = (l) => l.data;
|
|
6
|
+
return {
|
|
7
|
+
directories: () => a.http.get(`${d}/directories`).then(m),
|
|
8
|
+
createDirectory: (l, v) => a.http.post(`${d}/directories`, { parent_id: l, title: v }).then(m),
|
|
9
|
+
renameDirectory: (l, v) => a.http.patch(`${d}/directories/${l}`, { title: v }).then(m),
|
|
10
|
+
moveDirectory: (l, v) => a.http.patch(`${d}/directories/${l}/move`, { parent_id: v }).then(m),
|
|
11
|
+
deleteDirectory: (l, v = !1) => a.http.delete(`${d}/directories/${l}`, {
|
|
12
|
+
query: { force: v ? 1 : void 0 }
|
|
13
|
+
}),
|
|
14
|
+
files: (l = {}) => a.http.get(`${d}/files`, {
|
|
15
|
+
query: {
|
|
16
|
+
directory_id: l.directory_id ?? void 0,
|
|
17
|
+
q: l.q || void 0,
|
|
18
|
+
type: l.type ?? void 0,
|
|
19
|
+
sort: l.sort,
|
|
20
|
+
page: l.page,
|
|
21
|
+
per_page: l.per_page
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
file: (l) => a.http.get(`${d}/files/${l}`).then(m),
|
|
25
|
+
fileByPath: (l) => a.http.get(`${d}/files/by-path?path=${encodeURIComponent(l)}`).then(m).catch(() => null),
|
|
26
|
+
async upload(l, v, i) {
|
|
27
|
+
const t = new FormData();
|
|
28
|
+
return t.append("directory_id", String(l)), v.forEach((p) => t.append("files[]", p)), new Promise((p, k) => {
|
|
29
|
+
const g = new XMLHttpRequest();
|
|
30
|
+
g.open("POST", `${d}/files`), g.responseType = "json", g.withCredentials = !0, g.setRequestHeader("Accept", "application/json"), g.setRequestHeader("X-Requested-With", "XMLHttpRequest"), g.setRequestHeader("X-Webx-Locale", a.i18n.state.locale);
|
|
31
|
+
const u = lt();
|
|
32
|
+
u && g.setRequestHeader("X-XSRF-TOKEN", u), g.upload.addEventListener("progress", (n) => {
|
|
33
|
+
n.lengthComputable && i?.(Math.round(n.loaded / n.total * 100));
|
|
34
|
+
}), g.addEventListener("load", () => {
|
|
35
|
+
const n = g.response;
|
|
36
|
+
if (g.status >= 200 && g.status < 300 && n?.data) {
|
|
37
|
+
p(n.data);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
k(
|
|
41
|
+
Object.assign(new Error(n?.message ?? "Upload failed"), {
|
|
42
|
+
status: g.status,
|
|
43
|
+
body: n
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}), g.addEventListener("error", () => k(new Error("Upload failed"))), g.send(t);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
rename: (l, v) => a.http.patch(`${d}/files/${l}`, { name: v }).then(m),
|
|
50
|
+
move: (l, v) => a.http.post(`${d}/files/move`, { ids: l, directory_id: v }).then((i) => i.data.moved),
|
|
51
|
+
// A POST for the batch, not a DELETE: the panel's own client sends no body on DELETE, and
|
|
52
|
+
// a list of ids in a query string is a worse answer than an honest verb.
|
|
53
|
+
remove: (l) => a.http.post(`${d}/files/delete`, { ids: l }).then((v) => v.data.deleted),
|
|
54
|
+
removeOne: (l) => a.http.delete(`${d}/files/${l}`),
|
|
55
|
+
edit: (l, v) => a.http.post(`${d}/files/${l}/edit`, v).then(m),
|
|
56
|
+
copy: (l) => a.http.post(`${d}/files/${l}/copy`).then(m),
|
|
57
|
+
restoreOriginal: (l) => a.http.post(`${d}/files/${l}/restore-original`).then(m),
|
|
58
|
+
thumb(l, v, i, t = "cover") {
|
|
59
|
+
if (!l.thumb)
|
|
60
|
+
return null;
|
|
61
|
+
const p = new URL(l.thumb, window.location.origin);
|
|
62
|
+
return p.searchParams.set("w", String(v)), i && p.searchParams.set("h", String(i)), p.searchParams.set("fit", t), p.pathname + p.search;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function lt() {
|
|
67
|
+
const a = document.cookie.split("; ").find((d) => d.startsWith("XSRF-TOKEN="));
|
|
68
|
+
return a ? decodeURIComponent(a.slice(11)) : null;
|
|
69
|
+
}
|
|
4
70
|
const ge = /* @__PURE__ */ S({
|
|
5
71
|
__name: "DirectoryTree",
|
|
6
72
|
props: /* @__PURE__ */ F({
|
|
@@ -47,7 +113,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
47
113
|
onDrop: u
|
|
48
114
|
}, null, 8, ["selected", "expanded", "modelValue"]));
|
|
49
115
|
}
|
|
50
|
-
}),
|
|
116
|
+
}), it = /* @__PURE__ */ S({
|
|
51
117
|
__name: "FileGrid",
|
|
52
118
|
props: /* @__PURE__ */ F({
|
|
53
119
|
files: {},
|
|
@@ -128,7 +194,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
128
194
|
}, null, 8, ["description"]));
|
|
129
195
|
};
|
|
130
196
|
}
|
|
131
|
-
}),
|
|
197
|
+
}), ot = { class: "wx-media-toolbar" }, nt = /* @__PURE__ */ S({
|
|
132
198
|
__name: "MediaToolbar",
|
|
133
199
|
props: /* @__PURE__ */ F({
|
|
134
200
|
canUpload: { type: Boolean },
|
|
@@ -158,7 +224,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
158
224
|
{ value: "name", label: p("manager.sort-name") },
|
|
159
225
|
{ value: "-size", label: p("manager.sort-size") }
|
|
160
226
|
]), u = D(() => k.value.find((f) => f.value === v.value)?.label ?? ""), n = D(() => g.value.find((f) => f.value === i.value)?.label ?? "");
|
|
161
|
-
return (f, c) => (b(), W("header",
|
|
227
|
+
return (f, c) => (b(), W("header", ot, [
|
|
162
228
|
a.compact ? (b(), $(e(T), {
|
|
163
229
|
key: 0,
|
|
164
230
|
icon: "folder",
|
|
@@ -247,7 +313,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
247
313
|
})) : U("", !0)
|
|
248
314
|
]));
|
|
249
315
|
}
|
|
250
|
-
}),
|
|
316
|
+
}), rt = /* @__PURE__ */ S({
|
|
251
317
|
__name: "MoveDialog",
|
|
252
318
|
props: {
|
|
253
319
|
directories: {},
|
|
@@ -255,7 +321,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
255
321
|
count: {}
|
|
256
322
|
},
|
|
257
323
|
setup(a) {
|
|
258
|
-
const d = a, { open: m, resolve: l, dismiss: v } =
|
|
324
|
+
const d = a, { open: m, resolve: l, dismiss: v } = le(), i = B("webx-media"), t = D(
|
|
259
325
|
() => k(d.directories).filter((g) => g.value !== d.from)
|
|
260
326
|
), p = M(t.value[0]?.value ?? null);
|
|
261
327
|
function k(g) {
|
|
@@ -268,7 +334,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
268
334
|
...k(u.children ?? [])
|
|
269
335
|
]);
|
|
270
336
|
}
|
|
271
|
-
return (g, u) => (b(), $(e(
|
|
337
|
+
return (g, u) => (b(), $(e(ie), {
|
|
272
338
|
open: e(m),
|
|
273
339
|
"onUpdate:open": u[3] || (u[3] = (n) => te(m) ? m.value = n : null),
|
|
274
340
|
title: e(i)("manager.move-to"),
|
|
@@ -310,7 +376,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
310
376
|
_: 1
|
|
311
377
|
}, 8, ["open", "title"]));
|
|
312
378
|
}
|
|
313
|
-
}),
|
|
379
|
+
}), st = /* @__PURE__ */ S({
|
|
314
380
|
__name: "NameDialog",
|
|
315
381
|
props: {
|
|
316
382
|
title: {},
|
|
@@ -319,7 +385,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
319
385
|
confirmText: { default: void 0 }
|
|
320
386
|
},
|
|
321
387
|
setup(a) {
|
|
322
|
-
const d = a, { open: m, resolve: l, dismiss: v } =
|
|
388
|
+
const d = a, { open: m, resolve: l, dismiss: v } = le(), i = B("webx-media"), t = M(d.value), p = ee("input");
|
|
323
389
|
he(async () => {
|
|
324
390
|
await qe(), p.value?.querySelector("input")?.focus();
|
|
325
391
|
});
|
|
@@ -327,7 +393,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
327
393
|
const g = t.value.trim();
|
|
328
394
|
g !== "" && l(g);
|
|
329
395
|
}
|
|
330
|
-
return (g, u) => (b(), $(e(
|
|
396
|
+
return (g, u) => (b(), $(e(ie), {
|
|
331
397
|
open: e(m),
|
|
332
398
|
"onUpdate:open": u[2] || (u[2] = (n) => te(m) ? m.value = n : null),
|
|
333
399
|
title: a.title,
|
|
@@ -376,74 +442,7 @@ const ge = /* @__PURE__ */ S({
|
|
|
376
442
|
_: 1
|
|
377
443
|
}, 8, ["open", "title"]));
|
|
378
444
|
}
|
|
379
|
-
})
|
|
380
|
-
function we(a) {
|
|
381
|
-
const d = `${a.apiPath}/media`, m = (l) => l.data;
|
|
382
|
-
return {
|
|
383
|
-
directories: () => a.http.get(`${d}/directories`).then(m),
|
|
384
|
-
createDirectory: (l, v) => a.http.post(`${d}/directories`, { parent_id: l, title: v }).then(m),
|
|
385
|
-
renameDirectory: (l, v) => a.http.patch(`${d}/directories/${l}`, { title: v }).then(m),
|
|
386
|
-
moveDirectory: (l, v) => a.http.patch(`${d}/directories/${l}/move`, { parent_id: v }).then(m),
|
|
387
|
-
deleteDirectory: (l, v = !1) => a.http.delete(`${d}/directories/${l}`, {
|
|
388
|
-
query: { force: v ? 1 : void 0 }
|
|
389
|
-
}),
|
|
390
|
-
files: (l = {}) => a.http.get(`${d}/files`, {
|
|
391
|
-
query: {
|
|
392
|
-
directory_id: l.directory_id ?? void 0,
|
|
393
|
-
q: l.q || void 0,
|
|
394
|
-
type: l.type ?? void 0,
|
|
395
|
-
sort: l.sort,
|
|
396
|
-
page: l.page,
|
|
397
|
-
per_page: l.per_page
|
|
398
|
-
}
|
|
399
|
-
}),
|
|
400
|
-
file: (l) => a.http.get(`${d}/files/${l}`).then(m),
|
|
401
|
-
fileByPath: (l) => a.http.get(`${d}/files/by-path?path=${encodeURIComponent(l)}`).then(m).catch(() => null),
|
|
402
|
-
async upload(l, v, i) {
|
|
403
|
-
const t = new FormData();
|
|
404
|
-
return t.append("directory_id", String(l)), v.forEach((p) => t.append("files[]", p)), new Promise((p, k) => {
|
|
405
|
-
const g = new XMLHttpRequest();
|
|
406
|
-
g.open("POST", `${d}/files`), g.responseType = "json", g.withCredentials = !0, g.setRequestHeader("Accept", "application/json"), g.setRequestHeader("X-Requested-With", "XMLHttpRequest"), g.setRequestHeader("X-Webx-Locale", a.i18n.state.locale);
|
|
407
|
-
const u = st();
|
|
408
|
-
u && g.setRequestHeader("X-XSRF-TOKEN", u), g.upload.addEventListener("progress", (n) => {
|
|
409
|
-
n.lengthComputable && i?.(Math.round(n.loaded / n.total * 100));
|
|
410
|
-
}), g.addEventListener("load", () => {
|
|
411
|
-
const n = g.response;
|
|
412
|
-
if (g.status >= 200 && g.status < 300 && n?.data) {
|
|
413
|
-
p(n.data);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
k(
|
|
417
|
-
Object.assign(new Error(n?.message ?? "Upload failed"), {
|
|
418
|
-
status: g.status,
|
|
419
|
-
body: n
|
|
420
|
-
})
|
|
421
|
-
);
|
|
422
|
-
}), g.addEventListener("error", () => k(new Error("Upload failed"))), g.send(t);
|
|
423
|
-
});
|
|
424
|
-
},
|
|
425
|
-
rename: (l, v) => a.http.patch(`${d}/files/${l}`, { name: v }).then(m),
|
|
426
|
-
move: (l, v) => a.http.post(`${d}/files/move`, { ids: l, directory_id: v }).then((i) => i.data.moved),
|
|
427
|
-
// A POST for the batch, not a DELETE: the panel's own client sends no body on DELETE, and
|
|
428
|
-
// a list of ids in a query string is a worse answer than an honest verb.
|
|
429
|
-
remove: (l) => a.http.post(`${d}/files/delete`, { ids: l }).then((v) => v.data.deleted),
|
|
430
|
-
removeOne: (l) => a.http.delete(`${d}/files/${l}`),
|
|
431
|
-
edit: (l, v) => a.http.post(`${d}/files/${l}/edit`, v).then(m),
|
|
432
|
-
copy: (l) => a.http.post(`${d}/files/${l}/copy`).then(m),
|
|
433
|
-
restoreOriginal: (l) => a.http.post(`${d}/files/${l}/restore-original`).then(m),
|
|
434
|
-
thumb(l, v, i, t = "cover") {
|
|
435
|
-
if (!l.thumb)
|
|
436
|
-
return null;
|
|
437
|
-
const p = new URL(l.thumb, window.location.origin);
|
|
438
|
-
return p.searchParams.set("w", String(v)), i && p.searchParams.set("h", String(i)), p.searchParams.set("fit", t), p.pathname + p.search;
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
function st() {
|
|
443
|
-
const a = document.cookie.split("; ").find((d) => d.startsWith("XSRF-TOKEN="));
|
|
444
|
-
return a ? decodeURIComponent(a.slice(11)) : null;
|
|
445
|
-
}
|
|
446
|
-
const dt = {
|
|
445
|
+
}), dt = {
|
|
447
446
|
module: {
|
|
448
447
|
title: "Files"
|
|
449
448
|
},
|
|
@@ -551,7 +550,7 @@ const dt = {
|
|
|
551
550
|
}, ye = /* @__PURE__ */ new WeakSet();
|
|
552
551
|
function xe() {
|
|
553
552
|
try {
|
|
554
|
-
const { i18n: a } =
|
|
553
|
+
const { i18n: a } = ae();
|
|
555
554
|
ye.has(a) || (a.defaults("webx-media", dt), ye.add(a));
|
|
556
555
|
} catch {
|
|
557
556
|
}
|
|
@@ -567,7 +566,7 @@ const ut = {
|
|
|
567
566
|
},
|
|
568
567
|
emits: ["pick"],
|
|
569
568
|
setup(a, { emit: d }) {
|
|
570
|
-
const m = a, l = d, v =
|
|
569
|
+
const m = a, l = d, v = ae(), i = we(v);
|
|
571
570
|
xe();
|
|
572
571
|
const t = B("webx-media");
|
|
573
572
|
function p() {
|
|
@@ -635,7 +634,7 @@ const ut = {
|
|
|
635
634
|
N.danger(ce(C) ?? t("errors.upload"));
|
|
636
635
|
}
|
|
637
636
|
}
|
|
638
|
-
const ne = Y(
|
|
637
|
+
const ne = Y(st);
|
|
639
638
|
async function re() {
|
|
640
639
|
const r = await ne({
|
|
641
640
|
title: t("manager.new-folder"),
|
|
@@ -684,7 +683,7 @@ const ut = {
|
|
|
684
683
|
}
|
|
685
684
|
await E();
|
|
686
685
|
}
|
|
687
|
-
const ze = Y(
|
|
686
|
+
const ze = Y(rt);
|
|
688
687
|
async function We() {
|
|
689
688
|
const r = await ze({
|
|
690
689
|
directories: n.value,
|
|
@@ -801,7 +800,7 @@ const ut = {
|
|
|
801
800
|
})) : U("", !0)
|
|
802
801
|
])),
|
|
803
802
|
_("section", ct, [
|
|
804
|
-
h(
|
|
803
|
+
h(nt, {
|
|
805
804
|
search: x.value,
|
|
806
805
|
"onUpdate:search": o[1] || (o[1] = (y) => x.value = y),
|
|
807
806
|
type: z.value,
|
|
@@ -827,7 +826,7 @@ const ut = {
|
|
|
827
826
|
accept: a.accept === null ? void 0 : `${a.accept}/*`,
|
|
828
827
|
onChange: Te
|
|
829
828
|
}, null, 40, mt),
|
|
830
|
-
h(
|
|
829
|
+
h(it, {
|
|
831
830
|
selected: s.value,
|
|
832
831
|
"onUpdate:selected": o[5] || (o[5] = (y) => s.value = y),
|
|
833
832
|
files: _e.value,
|
|
@@ -905,20 +904,7 @@ const ut = {
|
|
|
905
904
|
}, 8, ["open", "title"])
|
|
906
905
|
], 2));
|
|
907
906
|
}
|
|
908
|
-
}), gt = /* @__PURE__ */ S({
|
|
909
|
-
__name: "MediaPage",
|
|
910
|
-
setup(a) {
|
|
911
|
-
return (d, m) => (b(), $(e(Ze), {
|
|
912
|
-
class: "wx-media-page",
|
|
913
|
-
padding: "md"
|
|
914
|
-
}, {
|
|
915
|
-
default: w(() => [
|
|
916
|
-
h(ke)
|
|
917
|
-
]),
|
|
918
|
-
_: 1
|
|
919
|
-
}));
|
|
920
|
-
}
|
|
921
|
-
}), yt = { class: "wx-media-picker" }, $e = /* @__PURE__ */ S({
|
|
907
|
+
}), gt = { class: "wx-media-picker" }, $e = /* @__PURE__ */ S({
|
|
922
908
|
__name: "MediaPicker",
|
|
923
909
|
props: {
|
|
924
910
|
accept: { default: "image" },
|
|
@@ -926,15 +912,15 @@ const ut = {
|
|
|
926
912
|
manage: { type: Boolean, default: !1 }
|
|
927
913
|
},
|
|
928
914
|
setup(a) {
|
|
929
|
-
const { open: d, resolve: m } =
|
|
930
|
-
return (v, i) => (b(), $(e(
|
|
915
|
+
const { open: d, resolve: m } = le(), l = B("webx-media");
|
|
916
|
+
return (v, i) => (b(), $(e(ie), {
|
|
931
917
|
open: e(d),
|
|
932
918
|
"onUpdate:open": i[1] || (i[1] = (t) => te(d) ? d.value = t : null),
|
|
933
919
|
title: a.title ?? e(l)("module.title"),
|
|
934
920
|
width: 960
|
|
935
921
|
}, {
|
|
936
922
|
default: w(() => [
|
|
937
|
-
_("div",
|
|
923
|
+
_("div", gt, [
|
|
938
924
|
h(ke, {
|
|
939
925
|
picking: !a.manage,
|
|
940
926
|
accept: a.manage ? null : a.accept,
|
|
@@ -945,19 +931,19 @@ const ut = {
|
|
|
945
931
|
_: 1
|
|
946
932
|
}, 8, ["open", "title"]));
|
|
947
933
|
}
|
|
948
|
-
}),
|
|
934
|
+
}), yt = Y($e), Rt = Y(
|
|
949
935
|
$e,
|
|
950
936
|
{ props: { manage: !0 } }
|
|
951
|
-
),
|
|
937
|
+
), ht = { class: "wx-media-field" }, bt = {
|
|
952
938
|
key: 0,
|
|
953
939
|
class: "wx-media-field__label"
|
|
954
|
-
},
|
|
940
|
+
}, wt = ["disabled"], xt = ["disabled", "title"], kt = {
|
|
955
941
|
key: 1,
|
|
956
942
|
class: "wx-media-field__missing"
|
|
957
|
-
},
|
|
943
|
+
}, $t = {
|
|
958
944
|
key: 0,
|
|
959
945
|
class: "wx-media-field__bar"
|
|
960
|
-
},
|
|
946
|
+
}, _t = { class: "wx-media-field__captions" }, Ct = { class: "wx-media-field__caption" }, Mt = { class: "wx-media-field__caption" }, Tt = { class: "wx-media-field__captions-footer" }, zt = /* @__PURE__ */ S({
|
|
961
947
|
__name: "MediaField",
|
|
962
948
|
props: /* @__PURE__ */ F({
|
|
963
949
|
label: { default: void 0 },
|
|
@@ -974,7 +960,7 @@ const ut = {
|
|
|
974
960
|
setup(a) {
|
|
975
961
|
const d = a, m = A(a, "modelValue");
|
|
976
962
|
xe();
|
|
977
|
-
const l = B("webx-media"), v = we(
|
|
963
|
+
const l = B("webx-media"), v = we(ae()), i = {
|
|
978
964
|
"16/9": "16 / 9",
|
|
979
965
|
"4/3": "4 / 3",
|
|
980
966
|
"1/1": "1 / 1"
|
|
@@ -1002,14 +988,14 @@ const ut = {
|
|
|
1002
988
|
}
|
|
1003
989
|
async function f() {
|
|
1004
990
|
if (d.disabled) return;
|
|
1005
|
-
const s = await
|
|
991
|
+
const s = await yt({ accept: d.accept });
|
|
1006
992
|
s && (m.value = { ...m.value, path: s.path, url: s.url });
|
|
1007
993
|
}
|
|
1008
994
|
function c() {
|
|
1009
995
|
m.value = null, t.value = !1;
|
|
1010
996
|
}
|
|
1011
|
-
return (s, x) => (b(), W("div",
|
|
1012
|
-
a.label ? (b(), W("span",
|
|
997
|
+
return (s, x) => (b(), W("div", ht, [
|
|
998
|
+
a.label ? (b(), W("span", bt, L(a.label), 1)) : U("", !0),
|
|
1013
999
|
m.value ? (b(), W("div", {
|
|
1014
1000
|
key: 2,
|
|
1015
1001
|
class: "wx-media-field__frame",
|
|
@@ -1022,18 +1008,18 @@ const ut = {
|
|
|
1022
1008
|
title: e(l)("field.replace"),
|
|
1023
1009
|
onClick: f
|
|
1024
1010
|
}, [
|
|
1025
|
-
k.value ? (b(), $(e(
|
|
1011
|
+
k.value ? (b(), $(e(Ze), {
|
|
1026
1012
|
key: 0,
|
|
1027
1013
|
src: k.value,
|
|
1028
1014
|
fit: "contain",
|
|
1029
1015
|
width: "100%",
|
|
1030
1016
|
height: "100%"
|
|
1031
|
-
}, null, 8, ["src"])) : (b(), W("span",
|
|
1032
|
-
], 8,
|
|
1033
|
-
a.disabled ? U("", !0) : (b(), W("div",
|
|
1017
|
+
}, null, 8, ["src"])) : (b(), W("span", kt, L(e(l)("field.no-preview")), 1))
|
|
1018
|
+
], 8, xt),
|
|
1019
|
+
a.disabled ? U("", !0) : (b(), W("div", $t, [
|
|
1034
1020
|
h(e(G), { size: "sm" }, {
|
|
1035
1021
|
default: w(() => [
|
|
1036
|
-
a.captions ? (b(), $(e(
|
|
1022
|
+
a.captions ? (b(), $(e(et), {
|
|
1037
1023
|
key: 0,
|
|
1038
1024
|
open: t.value,
|
|
1039
1025
|
"onUpdate:open": x[3] || (x[3] = (z) => t.value = z),
|
|
@@ -1049,8 +1035,8 @@ const ut = {
|
|
|
1049
1035
|
}, null, 8, ["title"])
|
|
1050
1036
|
]),
|
|
1051
1037
|
default: w(() => [
|
|
1052
|
-
_("div",
|
|
1053
|
-
_("div",
|
|
1038
|
+
_("div", _t, [
|
|
1039
|
+
_("div", Ct, [
|
|
1054
1040
|
x[4] || (x[4] = _("label", {
|
|
1055
1041
|
class: "wx-media-field__caption-label",
|
|
1056
1042
|
for: "wx-media-alt"
|
|
@@ -1063,7 +1049,7 @@ const ut = {
|
|
|
1063
1049
|
"onUpdate:modelValue": x[0] || (x[0] = (z) => n("alt", z))
|
|
1064
1050
|
}, null, 8, ["model-value", "placeholder"])
|
|
1065
1051
|
]),
|
|
1066
|
-
_("div",
|
|
1052
|
+
_("div", Mt, [
|
|
1067
1053
|
x[5] || (x[5] = _("label", {
|
|
1068
1054
|
class: "wx-media-field__caption-label",
|
|
1069
1055
|
for: "wx-media-title"
|
|
@@ -1076,7 +1062,7 @@ const ut = {
|
|
|
1076
1062
|
"onUpdate:modelValue": x[1] || (x[1] = (z) => n("title", z))
|
|
1077
1063
|
}, null, 8, ["model-value", "placeholder"])
|
|
1078
1064
|
]),
|
|
1079
|
-
_("div",
|
|
1065
|
+
_("div", Tt, [
|
|
1080
1066
|
h(e(I), {
|
|
1081
1067
|
size: "sm",
|
|
1082
1068
|
type: "primary",
|
|
@@ -1092,7 +1078,7 @@ const ut = {
|
|
|
1092
1078
|
]),
|
|
1093
1079
|
_: 1
|
|
1094
1080
|
}, 8, ["open", "title"])) : U("", !0),
|
|
1095
|
-
h(e(
|
|
1081
|
+
h(e(tt), {
|
|
1096
1082
|
title: e(l)("field.clear-title"),
|
|
1097
1083
|
description: e(l)("field.clear-text"),
|
|
1098
1084
|
"confirm-text": e(l)("field.clear-confirm"),
|
|
@@ -1123,10 +1109,23 @@ const ut = {
|
|
|
1123
1109
|
class: "wx-media-field__pick",
|
|
1124
1110
|
disabled: a.disabled,
|
|
1125
1111
|
onClick: f
|
|
1126
|
-
}, L(e(l)("field.select")), 9,
|
|
1112
|
+
}, L(e(l)("field.select")), 9, wt)
|
|
1127
1113
|
], 4))
|
|
1128
1114
|
]));
|
|
1129
1115
|
}
|
|
1116
|
+
}), Wt = /* @__PURE__ */ S({
|
|
1117
|
+
__name: "MediaPage",
|
|
1118
|
+
setup(a) {
|
|
1119
|
+
return (d, m) => (b(), $(e(at), {
|
|
1120
|
+
class: "wx-media-page",
|
|
1121
|
+
padding: "md"
|
|
1122
|
+
}, {
|
|
1123
|
+
default: w(() => [
|
|
1124
|
+
h(ke)
|
|
1125
|
+
]),
|
|
1126
|
+
_: 1
|
|
1127
|
+
}));
|
|
1128
|
+
}
|
|
1130
1129
|
});
|
|
1131
1130
|
function Vt(a = {}) {
|
|
1132
1131
|
const d = a.path ?? "/media";
|
|
@@ -1137,20 +1136,25 @@ function Vt(a = {}) {
|
|
|
1137
1136
|
{
|
|
1138
1137
|
path: d,
|
|
1139
1138
|
name: "webx.media",
|
|
1140
|
-
component:
|
|
1139
|
+
component: Wt
|
|
1141
1140
|
}
|
|
1142
|
-
]
|
|
1141
|
+
],
|
|
1142
|
+
// What a screen means by `wx-media`: the field over the library. The server registers
|
|
1143
|
+
// the same name for the value it stores.
|
|
1144
|
+
types: {
|
|
1145
|
+
"wx-media": { component: zt, kind: "field" }
|
|
1146
|
+
}
|
|
1143
1147
|
};
|
|
1144
1148
|
}
|
|
1145
1149
|
export {
|
|
1146
|
-
|
|
1150
|
+
zt as WxMediaField,
|
|
1147
1151
|
ke as WxMediaManager,
|
|
1148
|
-
|
|
1152
|
+
Wt as WxMediaPage,
|
|
1149
1153
|
$e as WxMediaPicker,
|
|
1150
1154
|
we as createMediaApi,
|
|
1151
1155
|
Vt as media,
|
|
1152
1156
|
dt as mediaMessages,
|
|
1153
|
-
|
|
1154
|
-
|
|
1157
|
+
Rt as openMediaLibrary,
|
|
1158
|
+
yt as openMediaPicker
|
|
1155
1159
|
};
|
|
1156
1160
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/DirectoryTree.vue","../src/FileGrid.vue","../src/MediaToolbar.vue","../src/MoveDialog.vue","../src/NameDialog.vue","../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/MediaManager.vue","../src/MediaPage.vue","../src/MediaPicker.vue","../src/openMediaPicker.ts","../src/MediaField.vue","../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { WxTree, type TreeDropEvent } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * The folders, on the left.\n *\n * The root carries a translated label rather than the title it was given in the database: it is\n * the library itself, and an editor should not have to know it is a row.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n}>()\n\nconst selected = defineModel<number | null>('selected', { default: null })\n\nconst emit = defineEmits<{\n move: [id: number, parentId: number]\n}>()\n\nconst t = useTranslate('webx-media')\n\ntype Node = MediaDirectory & { label: string }\n\n// The tree rearranges this list itself when something is dropped, and then says so; the answer\n// from the server replaces it a moment later. A computed would have nothing to rearrange.\nconst nodes = ref<Node[]>([])\nconst expanded = ref<number[]>([])\n\nwatch(\n () => props.directories,\n (directories) => {\n nodes.value = directories.map(label)\n expanded.value = keysOf(directories)\n },\n { immediate: true, deep: true },\n)\n\nfunction label(directory: MediaDirectory): Node {\n return {\n ...directory,\n label: directory.is_root ? t('manager.root') : directory.title,\n children: (directory.children ?? []).map(label),\n }\n}\n\nfunction keysOf(directories: MediaDirectory[]): number[] {\n return directories.flatMap((directory) => [directory.id, ...keysOf(directory.children ?? [])])\n}\n\nfunction onDrop(event: TreeDropEvent<Node>): void {\n const parent = event.zone === 'inside' ? event.target : event.parent\n\n if (parent) {\n emit('move', event.node.id, parent.id)\n }\n}\n</script>\n\n<template>\n <wx-tree\n v-model:selected=\"selected\"\n v-model:expanded=\"expanded\"\n v-model=\"nodes\"\n class=\"wx-media-tree\"\n node-key=\"id\"\n draggable\n show-lines\n @drop=\"onDrop\"\n />\n</template>\n\n<style>\n.wx-media-tree {\n min-width: 0;\n overflow: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { toast, WxEmpty, WxFileCard, WxSelectionArea } from '@webx-ui/core'\nimport type { MediaApi } from './api'\nimport type { MediaFile } from './types'\n\n/**\n * The files, as cards, with a rubber band over them.\n *\n * Previews come from the server's thumbnail endpoint rather than the files themselves: a grid of\n * forty photographs is forty full-size images otherwise, and on a phone that is the difference\n * between a screen and a wait.\n */\nconst props = defineProps<{\n files: MediaFile[]\n api: MediaApi\n query?: string\n /** A picker takes one file; the manager selects to act on a batch. */\n single?: boolean\n}>()\n\nconst selected = defineModel<number[]>('selected', { default: () => [] })\n\nconst emit = defineEmits<{\n open: [file: MediaFile]\n rename: [file: MediaFile, name: string]\n edit: [file: MediaFile]\n remove: [file: MediaFile]\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst empty = computed(() =>\n props.query ? t('manager.empty-search', { query: props.query }) : t('manager.empty'),\n)\n\nfunction copied(): void {\n toast.success(t('manager.link-copied'))\n}\n\n/**\n * The clipboard is not always there: it is missing outside a secure context, and an iframe has\n * to be granted it. Rather than let the card fail silently, try the old way — and if that is\n * refused too, put the address on screen, where it can at least be copied by hand.\n */\nfunction copyFailed(file: MediaFile): void {\n const area = document.createElement('textarea')\n area.value = file.url\n area.setAttribute('readonly', '')\n area.style.position = 'fixed'\n area.style.opacity = '0'\n document.body.appendChild(area)\n area.select()\n\n let done = false\n\n try {\n done = document.execCommand('copy')\n } catch {\n done = false\n }\n\n area.remove()\n\n if (done) {\n copied()\n\n return\n }\n\n toast.danger(`${t('errors.copy')} ${file.url}`, { duration: 0 })\n}\n</script>\n\n<template>\n <wx-selection-area\n v-if=\"files.length > 0\"\n v-slot=\"{ isSelected }\"\n v-model=\"selected\"\n class=\"wx-media-grid\"\n :multiple=\"!single\"\n >\n <wx-file-card\n v-for=\"file in files\"\n :key=\"file.id\"\n v-wx-select=\"file.id\"\n class=\"wx-media-grid__card\"\n :name=\"file.name\"\n :url=\"file.url\"\n :thumbnail=\"api.thumb(file, 320, 320) ?? undefined\"\n :type=\"file.mime\"\n :selected=\"isSelected(file.id)\"\n renamable\n :editable=\"file.editable\"\n removable\n copyable\n :rename-label=\"t('manager.rename')\"\n :edit-label=\"t('manager.edit')\"\n :remove-label=\"t('manager.delete')\"\n :remove-confirm-text=\"t('dialogs.delete-file', { name: file.name })\"\n :cancel-label=\"t('manager.cancel')\"\n :save-label=\"t('manager.save')\"\n :copy-label=\"t('manager.copy-link')\"\n :copied-label=\"t('manager.link-copied')\"\n @rename=\"(name) => emit('rename', file, name)\"\n @edit=\"emit('edit', file)\"\n @remove=\"emit('remove', file)\"\n @copy=\"copied\"\n @copy-error=\"copyFailed(file)\"\n @dblclick=\"emit('open', file)\"\n />\n </wx-selection-area>\n\n <wx-empty v-else :description=\"empty\" />\n</template>\n\n<style>\n.wx-media-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: var(--wx-space-12);\n align-content: start;\n min-height: 0;\n overflow: auto;\n padding: var(--wx-space-2);\n /* A rubber band over names would otherwise select the names. */\n user-select: none;\n}\n\n.wx-media--compact .wx-media-grid {\n grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n gap: var(--wx-space-8);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { WxAction, WxActions, WxDropdown, WxDropdownItem, WxInput } from '@webx-ui/core'\nimport type { MediaKind } from './types'\n\n/**\n * One row of icons over the files.\n *\n * Actions rather than labelled buttons: the row has to survive a phone and a picker dialog half\n * the width of a window, and a row of words does not. What each icon means is a tooltip away,\n * and what is chosen is said in the icon's own label.\n */\nconst props = defineProps<{\n canUpload: boolean\n canManage: boolean\n selected: number\n /** The caller asked for one kind of file, so there is nothing to filter by. */\n fixedType?: boolean\n /** Narrow screens get the folders behind a button instead of beside the files. */\n compact: boolean\n}>()\n\nconst search = defineModel<string>('search', { default: '' })\nconst type = defineModel<MediaKind | 'all'>('type', { default: 'all' })\nconst sort = defineModel<string>('sort', { default: '-created_at' })\n\nconst emit = defineEmits<{\n upload: []\n move: []\n remove: []\n folders: []\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst types = computed<{ value: MediaKind | 'all'; label: string }[]>(() => [\n { value: 'all', label: t('manager.all-types') },\n ...(['image', 'video', 'audio', 'document', 'other'] as MediaKind[]).map((kind) => ({\n value: kind,\n label: t(`manager.${kind}`),\n })),\n])\n\nconst sorts = computed(() => [\n { value: '-created_at', label: t('manager.sort-newest') },\n { value: 'created_at', label: t('manager.sort-oldest') },\n { value: 'name', label: t('manager.sort-name') },\n { value: '-size', label: t('manager.sort-size') },\n])\n\nconst typeLabel = computed(() => types.value.find((one) => one.value === type.value)?.label ?? '')\nconst sortLabel = computed(() => sorts.value.find((one) => one.value === sort.value)?.label ?? '')\n</script>\n\n<template>\n <header class=\"wx-media-toolbar\">\n <wx-action\n v-if=\"compact\"\n icon=\"folder\"\n :title=\"t('manager.folders')\"\n size=\"sm\"\n @click=\"emit('folders')\"\n />\n\n <wx-input\n v-model=\"search\"\n class=\"wx-media-toolbar__search\"\n :placeholder=\"t('manager.search')\"\n clearable\n size=\"sm\"\n />\n\n <wx-dropdown v-if=\"!fixedType\">\n <template #trigger>\n <wx-action icon=\"filter\" :title=\"`${t('manager.all-types')}: ${typeLabel}`\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in types\"\n :key=\"String(option.value)\"\n :active=\"option.value === type\"\n @click=\"type = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-dropdown>\n <template #trigger>\n <!-- Not `type=\"sort\"`: in this set that means reordering by hand, and its icon is a drag handle. -->\n <wx-action icon=\"arrow-down\" :title=\"sortLabel\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in sorts\"\n :key=\"option.value\"\n :active=\"option.value === sort\"\n @click=\"sort = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-action\n v-if=\"canUpload\"\n type=\"upload\"\n :title=\"t('manager.upload')\"\n size=\"sm\"\n @click=\"emit('upload')\"\n />\n\n <!-- Kept in the row rather than added to it: the icons are always here and simply cannot\n be used while nothing is selected, so the grid never moves down a line. -->\n <wx-actions v-if=\"canManage\" size=\"sm\">\n <wx-action\n icon=\"folder\"\n :title=\"t('manager.move')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('move')\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('remove')\"\n />\n </wx-actions>\n </header>\n</template>\n\n<style>\n.wx-media-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-6);\n}\n\n.wx-media-toolbar__search {\n flex: 1 1 180px;\n min-width: 120px;\n}\n\n.wx-media-toolbar__count {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n margin-inline-start: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxSelect, WxSpace } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * Where to put the selected files.\n *\n * A list of folders rather than a box to type in: an editor knows a folder by its name and its\n * place in the tree, and has no reason to ever learn that it also has a number.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n /** The folder they are in now, which is not worth offering. */\n from?: number | null\n count: number\n}>()\n\nconst { open, resolve, dismiss } = useModal<number>()\n\nconst t = useTranslate('webx-media')\n\nconst options = computed(() =>\n flatten(props.directories).filter((option) => option.value !== props.from),\n)\nconst target = ref<number | null>(options.value[0]?.value ?? null)\n\nfunction flatten(directories: MediaDirectory[]): { value: number; label: string }[] {\n return directories.flatMap((directory) => [\n {\n value: directory.id,\n // Indented, so the tree is still readable once it is a flat list.\n label:\n ' '.repeat(directory.depth * 3) + (directory.is_root ? t('manager.root') : directory.title),\n },\n ...flatten(directory.children ?? []),\n ])\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('manager.move-to')\" :width=\"420\">\n <wx-select v-model=\"target\" :options=\"options\" />\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"target === null\" @click=\"resolve(target!)\">\n {{ t('manager.move') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxInput, WxSpace } from '@webx-ui/core'\n\n/**\n * A name, asked for properly.\n *\n * `window.prompt` is not an option: a browser is free to refuse it — an embedded frame usually\n * does — and then the button simply does nothing, which is exactly how \"new folder\" looked.\n */\nconst props = withDefaults(\n defineProps<{\n title: string\n label?: string\n value?: string\n confirmText?: string\n }>(),\n { label: undefined, value: '', confirmText: undefined },\n)\n\nconst { open, resolve, dismiss } = useModal<string>()\n\nconst t = useTranslate('webx-media')\n\nconst name = ref(props.value)\nconst input = useTemplateRef<HTMLElement>('input')\n\nonMounted(async () => {\n await nextTick()\n input.value?.querySelector('input')?.focus()\n})\n\nfunction submit(): void {\n const value = name.value.trim()\n\n if (value !== '') {\n resolve(value)\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title\" :width=\"420\">\n <div ref=\"input\">\n <!-- The dialog's title says what is being named, so the field carries the same words as a\n placeholder and as its accessible name rather than repeating them above itself. -->\n <wx-input\n v-model=\"name\"\n :placeholder=\"label ?? t('manager.folder-name')\"\n :aria-label=\"label ?? t('manager.folder-name')\"\n @keyup.enter=\"submit\"\n />\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"name.trim() === ''\" @click=\"submit\">\n {{ confirmText ?? t('manager.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","import type { AdminContext } from '@webx-ui/module-admin'\nimport type { EditOperations, FileQuery, MediaDirectory, MediaFile, MediaPage } from './types'\n\nexport interface MediaApi {\n directories(): Promise<MediaDirectory[]>\n createDirectory(parentId: number, title: string): Promise<MediaDirectory>\n renameDirectory(id: number, title: string): Promise<MediaDirectory>\n moveDirectory(id: number, parentId: number): Promise<MediaDirectory>\n /** Without `force` the server refuses a folder that holds anything, and says what it holds. */\n deleteDirectory(id: number, force?: boolean): Promise<void>\n\n files(query?: FileQuery): Promise<MediaPage>\n file(id: number): Promise<MediaFile>\n /**\n * The file behind a key.\n *\n * An entity stores the key and nothing else, so this is how a form draws the picture it\n * saved last time. `null` when the file is gone from the library.\n */\n fileByPath(path: string): Promise<MediaFile | null>\n upload(\n directoryId: number,\n files: File[],\n onProgress?: (percent: number) => void,\n ): Promise<MediaFile[]>\n rename(id: number, name: string): Promise<MediaFile>\n move(ids: number[], directoryId: number): Promise<number>\n remove(ids: number[]): Promise<number>\n removeOne(id: number): Promise<void>\n\n edit(id: number, operations: EditOperations): Promise<MediaFile>\n copy(id: number): Promise<MediaFile>\n restoreOriginal(id: number): Promise<MediaFile>\n\n /** The address of a preview at a size the server allows. */\n thumb(file: MediaFile, width: number, height?: number, fit?: 'cover' | 'contain'): string | null\n}\n\nexport function createMediaApi(admin: AdminContext): MediaApi {\n const base = `${admin.apiPath}/media`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n directories: () => admin.http.get<{ data: MediaDirectory[] }>(`${base}/directories`).then(data),\n\n createDirectory: (parentId, title) =>\n admin.http\n .post<{ data: MediaDirectory }>(`${base}/directories`, { parent_id: parentId, title })\n .then(data),\n\n renameDirectory: (id, title) =>\n admin.http.patch<{ data: MediaDirectory }>(`${base}/directories/${id}`, { title }).then(data),\n\n moveDirectory: (id, parentId) =>\n admin.http\n .patch<{ data: MediaDirectory }>(`${base}/directories/${id}/move`, { parent_id: parentId })\n .then(data),\n\n deleteDirectory: (id, force = false) =>\n admin.http.delete<void>(`${base}/directories/${id}`, {\n query: { force: force ? 1 : undefined },\n }),\n\n files: (query = {}) =>\n admin.http.get<MediaPage>(`${base}/files`, {\n query: {\n directory_id: query.directory_id ?? undefined,\n q: query.q || undefined,\n type: query.type ?? undefined,\n sort: query.sort,\n page: query.page,\n per_page: query.per_page,\n },\n }),\n\n file: (id) => admin.http.get<{ data: MediaFile }>(`${base}/files/${id}`).then(data),\n\n fileByPath: (path) =>\n admin.http\n .get<{ data: MediaFile }>(`${base}/files/by-path?path=${encodeURIComponent(path)}`)\n .then(data)\n .catch(() => null),\n\n async upload(directoryId, files, onProgress) {\n const body = new FormData()\n body.append('directory_id', String(directoryId))\n files.forEach((file) => body.append('files[]', file))\n\n // XHR rather than fetch, for the one thing fetch still cannot do: say how far an upload\n // has got. A person watching a forty-megabyte video wants a bar, not a spinner.\n return new Promise<MediaFile[]>((resolve, reject) => {\n const request = new XMLHttpRequest()\n\n request.open('POST', `${base}/files`)\n request.responseType = 'json'\n request.withCredentials = true\n request.setRequestHeader('Accept', 'application/json')\n request.setRequestHeader('X-Requested-With', 'XMLHttpRequest')\n // Without this the refusal comes back in English while the panel is in Russian: the\n // panel's own client sends it on every request, and this one is not that client.\n request.setRequestHeader('X-Webx-Locale', admin.i18n.state.locale)\n\n const token = csrfToken()\n if (token) {\n request.setRequestHeader('X-XSRF-TOKEN', token)\n }\n\n request.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * 100))\n }\n })\n\n request.addEventListener('load', () => {\n const answer = request.response as { data?: MediaFile[]; message?: string } | null\n\n if (request.status >= 200 && request.status < 300 && answer?.data) {\n resolve(answer.data)\n\n return\n }\n\n reject(\n Object.assign(new Error(answer?.message ?? 'Upload failed'), {\n status: request.status,\n body: answer,\n }),\n )\n })\n\n request.addEventListener('error', () => reject(new Error('Upload failed')))\n request.send(body)\n })\n },\n\n rename: (id, name) =>\n admin.http.patch<{ data: MediaFile }>(`${base}/files/${id}`, { name }).then(data),\n\n move: (ids, directoryId) =>\n admin.http\n .post<{ data: { moved: number } }>(`${base}/files/move`, { ids, directory_id: directoryId })\n .then((body) => body.data.moved),\n\n // A POST for the batch, not a DELETE: the panel's own client sends no body on DELETE, and\n // a list of ids in a query string is a worse answer than an honest verb.\n remove: (ids) =>\n admin.http\n .post<{ data: { deleted: number } }>(`${base}/files/delete`, { ids })\n .then((body) => body.data.deleted),\n\n removeOne: (id) => admin.http.delete<void>(`${base}/files/${id}`),\n\n edit: (id, operations) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/edit`, operations).then(data),\n\n copy: (id) => admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/copy`).then(data),\n\n restoreOriginal: (id) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/restore-original`).then(data),\n\n thumb(file, width, height, fit = 'cover') {\n if (!file.thumb) {\n return null\n }\n\n const url = new URL(file.thumb, window.location.origin)\n url.searchParams.set('w', String(width))\n if (height) {\n url.searchParams.set('h', String(height))\n }\n url.searchParams.set('fit', fit)\n\n return url.pathname + url.search\n },\n }\n}\n\nfunction csrfToken(): string | null {\n const cookie = document.cookie.split('; ').find((part) => part.startsWith('XSRF-TOKEN='))\n\n return cookie ? decodeURIComponent(cookie.slice('XSRF-TOKEN='.length)) : null\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-media` ships as `lang/en/*.php`, and the same ones every other\n * language there has. Kept here so a manager placed by hand, with no WebX UI server behind it,\n * still has words — and so a key never reaches the screen when a translation is missing.\n */\nexport const mediaMessages: Record<string, Messages> = {\n module: {\n title: 'Files',\n },\n manager: {\n root: 'Library',\n search: 'Search files',\n 'new-folder': 'New folder',\n 'folder-name': 'Folder name',\n rename: 'Rename',\n move: 'Move',\n 'move-to': 'Move to…',\n delete: 'Delete',\n upload: 'Upload',\n 'upload-hint': 'Drop files here or choose them',\n select: 'Select',\n selected: ':count selected',\n empty: 'This folder is empty',\n 'empty-search': 'Nothing matches :query',\n 'all-types': 'All types',\n image: 'Images',\n video: 'Video',\n audio: 'Audio',\n document: 'Documents',\n other: 'Other',\n 'sort-newest': 'Newest first',\n 'sort-oldest': 'Oldest first',\n 'sort-name': 'By name',\n 'sort-size': 'Largest first',\n 'duplicate-added': 'That file was already in this folder',\n uploaded: ':count file(s) added',\n cancel: 'Cancel',\n save: 'Save',\n folders: 'Folders',\n edit: 'Edit',\n 'copy-link': 'Copy the link',\n 'link-copied': 'Link copied',\n 'status-files': 'Files: :count',\n 'status-selected': 'selected: :count',\n 'status-size': 'size: :size',\n },\n // The picture field on a form, which is the module used from outside its own section.\n field: {\n select: 'Choose a picture',\n edit: 'Captions',\n replace: 'Replace',\n captions: 'Captions',\n 'alt-hint': 'What is on the picture, for a reader who cannot see it',\n 'title-hint': 'A caption shown on hover',\n 'no-preview': 'No preview',\n 'clear-title': 'Clear the field?',\n 'clear-text': 'The file stays in the library — only this record stops using it.',\n 'clear-confirm': 'Clear',\n },\n // The image editor is a component of the design system, so its words are props rather than\n // lines in the dictionary — the manager hands it these when it opens it.\n editor: {\n title: 'Edit picture',\n save: 'Save',\n cancel: 'Cancel',\n reset: 'Reset',\n 'rotate-left': 'Turn left',\n 'rotate-right': 'Turn right',\n 'flip-horizontal': 'Mirror across',\n 'flip-vertical': 'Mirror down',\n ratio: 'Ratio',\n crop: 'Crop',\n output: 'Output',\n 'output-hint': 'The size of the picture you will get',\n width: 'Width',\n height: 'Height',\n free: 'Free',\n original: 'Original',\n error: 'This picture could not be loaded',\n },\n dialogs: {\n 'delete-files-title': 'Delete :count file(s)?',\n 'delete-files-text': 'Anywhere they are already used, they will stop opening.',\n 'delete-folder-title': 'Delete the folder :title?',\n 'delete-folder-contents': 'It holds :files file(s) and :directories folder(s).',\n 'delete-folder-warning':\n 'Everything inside goes too, and pictures already placed in content will stop opening.',\n confirm: 'Delete',\n 'delete-file': 'Delete :name?',\n },\n errors: {\n 'directory-not-empty': 'This folder is not empty.',\n 'root-immutable': 'The library root cannot be moved or deleted.',\n 'directory-into-itself': 'A folder cannot be moved into itself.',\n 'not-an-image': 'This file is not an image.',\n 'image-too-large': 'This image is larger than :megapixels megapixels.',\n upload: 'That file was not accepted.',\n copy: 'The clipboard is not available here. The address:',\n 'unsupported-type': 'Only these can be uploaded: :types.',\n 'file-too-large': 'The file is larger than :size MB.',\n 'too-many-files': 'No more than :count files at a time.',\n },\n files: {\n copy: ':name (copy)',\n },\n validation: {\n title: 'name',\n parent_id: 'parent folder',\n directory_id: 'folder',\n files: 'files',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { mediaMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/**\n * Put this package's English under the panel's dictionary, once per panel.\n *\n * The server's translations land on top of it when they arrive. What this prevents is the half\n * second — or the whole session, in a panel assembled without a server — where a screen shows\n * `manager.upload` instead of a word.\n */\nexport function useMediaMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-media', mediaMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n openImageEditor,\n toast,\n useElementWidth,\n WxAction,\n WxActions,\n WxDrawer,\n WxPagination,\n} from '@webx-ui/core'\nimport DirectoryTree from './DirectoryTree.vue'\nimport FileGrid from './FileGrid.vue'\nimport MediaToolbar from './MediaToolbar.vue'\nimport MoveDialog from './MoveDialog.vue'\nimport NameDialog from './NameDialog.vue'\nimport { createMediaApi } from './api'\nimport { useMediaMessages } from './i18n'\nimport type { MediaDirectory, MediaFile, MediaKind, MediaPage } from './types'\n\n/**\n * The library: folders on the left, files on the right, a row of icons over both.\n *\n * Everything the screen does is one request and a reload of the part that changed — there is no\n * cache here on purpose. A library is edited by several people, and a panel that believes its\n * own copy shows a folder that was emptied an hour ago.\n *\n * The width that decides the layout is this component's own, not the window's: the same manager\n * is a page, half of a picker dialog, and a phone.\n */\nconst props = withDefaults(\n defineProps<{\n /** A picker hands one file back instead of managing the library. */\n picking?: boolean\n /** Only these kinds can be chosen, when picking. */\n accept?: MediaKind | null\n }>(),\n { picking: false, accept: null },\n)\n\nconst emit = defineEmits<{ pick: [file: MediaFile] }>()\n\nconst admin = useAdmin()\nconst api = createMediaApi(admin)\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\n/**\n * The image editor's words, in the language the panel is being read in.\n *\n * Read when the editor is opened rather than kept in a computed: the dialog is mounted outside\n * the app, so it is handed plain strings once and nothing re-renders it afterwards.\n */\nfunction editorLabels(): Record<string, string> {\n return {\n title: t('editor.title'),\n saveLabel: t('editor.save'),\n cancelLabel: t('editor.cancel'),\n resetLabel: t('editor.reset'),\n rotateLeftLabel: t('editor.rotate-left'),\n rotateRightLabel: t('editor.rotate-right'),\n flipHorizontalLabel: t('editor.flip-horizontal'),\n flipVerticalLabel: t('editor.flip-vertical'),\n ratioLabel: t('editor.ratio'),\n cropLabel: t('editor.crop'),\n outputLabel: t('editor.output'),\n outputHint: t('editor.output-hint'),\n widthLabel: t('editor.width'),\n heightLabel: t('editor.height'),\n freeLabel: t('editor.free'),\n originalLabel: t('editor.original'),\n errorText: t('editor.error'),\n }\n}\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst width = useElementWidth(root)\nconst compact = computed(() => width.value > 0 && width.value < 640)\n\nconst directories = ref<MediaDirectory[]>([])\nconst current = ref<number | null>(null)\nconst page = ref<MediaPage | null>(null)\nconst selected = ref<number[]>([])\nconst search = ref('')\n/*\n * What the caller asked for is not a default the person can put back: a field that wants a\n * picture would otherwise be handed a PDF, and find out when the page renders.\n */\nconst type = ref<MediaKind | 'all'>(props.accept ?? 'all')\n\nwatch(\n () => props.accept,\n (kind) => {\n type.value = kind ?? 'all'\n },\n)\nconst sort = ref('-created_at')\nconst busy = ref(false)\nconst foldersOpen = ref(false)\nconst picker = useTemplateRef<HTMLInputElement>('picker')\n\nconst rows = computed(() => page.value?.data ?? [])\nconst canManage = computed(() => admin.can('media.manage'))\nconst canUpload = computed(() => admin.can('media.upload') || canManage.value)\nconst folder = computed(() => find(current.value))\n\nonMounted(load)\n\nwatch([current, type, sort], () => loadFiles(1))\nwatch(\n search,\n debounce(() => loadFiles(1), 300),\n)\nwatch(compact, (narrow) => {\n if (!narrow) {\n foldersOpen.value = false\n }\n})\n\nasync function load(): Promise<void> {\n directories.value = await api.directories()\n current.value ??= directories.value[0]?.id ?? null\n await loadFiles(1)\n}\n\nasync function loadFiles(to = page.value?.meta.current_page ?? 1): Promise<void> {\n busy.value = true\n\n try {\n page.value = await api.files({\n directory_id: search.value ? null : current.value,\n q: search.value,\n type: type.value === 'all' ? null : type.value,\n sort: sort.value,\n page: to,\n })\n selected.value = []\n } finally {\n busy.value = false\n }\n}\n\nfunction choose(): void {\n picker.value?.click()\n}\n\n/**\n * Uploading says what happened in a toast and nowhere else.\n *\n * A list of what was just added under the toolbar is a second place to look and a thing to\n * dismiss; the files themselves appear in the grid a moment later, which is the answer.\n */\nasync function upload(event: Event): Promise<void> {\n const input = event.target as HTMLInputElement\n const chosen = Array.from(input.files ?? [])\n\n // Cleared straight away, so choosing the same file twice in a row still counts as a change.\n input.value = ''\n\n if (chosen.length === 0 || current.value === null) {\n return\n }\n\n try {\n const stored = await api.upload(current.value, chosen)\n const duplicates = stored.filter((file) => file.duplicate).length\n\n if (stored.length > duplicates) {\n toast.success(t('manager.uploaded', { count: stored.length - duplicates }))\n }\n\n if (duplicates > 0) {\n toast.info(t('manager.duplicate-added'))\n }\n\n await Promise.all([load(), loadFiles(1)])\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.upload'))\n }\n}\n\nconst askName = createModal<\n string,\n { title: string; label?: string; value?: string; confirmText?: string }\n>(NameDialog)\n\nasync function createFolder(): Promise<void> {\n const title = await askName({\n title: t('manager.new-folder'),\n confirmText: t('manager.new-folder'),\n })\n\n if (title === undefined || current.value === null) {\n return\n }\n\n await api.createDirectory(current.value, title)\n await load()\n}\n\nasync function renameFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n const title = await askName({\n title: t('manager.rename'),\n value: target.title,\n confirmText: t('manager.save'),\n })\n\n if (title !== undefined) {\n await api.renameDirectory(target.id, title)\n await load()\n }\n}\n\nasync function deleteFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n // Asked twice on purpose, and the second question carries the counts the server sent back:\n // what goes with the folder is somebody's article illustrations.\n try {\n await api.deleteDirectory(target.id)\n } catch (error) {\n const counts = countsOf(error)\n\n if (!counts) {\n throw error\n }\n\n const agreed = await confirm({\n title: t('dialogs.delete-folder-title', { title: target.title }),\n message: `${t('dialogs.delete-folder-contents', counts)} ${t('dialogs.delete-folder-warning')}`,\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) {\n return\n }\n\n await api.deleteDirectory(target.id, true)\n }\n\n current.value = null\n await load()\n}\n\nasync function moveFolder(id: number, parentId: number): Promise<void> {\n try {\n await api.moveDirectory(id, parentId)\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.directory-into-itself'))\n }\n\n await load()\n}\n\nconst askWhereTo = createModal<\n number,\n { directories: MediaDirectory[]; from: number | null; count: number }\n>(MoveDialog)\n\nasync function moveSelected(): Promise<void> {\n const to = await askWhereTo({\n directories: directories.value,\n from: current.value,\n count: selected.value.length,\n })\n\n if (to === undefined) {\n return\n }\n\n await api.move([...selected.value], to)\n await Promise.all([load(), loadFiles()])\n}\n\nasync function removeSelected(): Promise<void> {\n const ids = [...selected.value]\n\n const agreed = await confirm({\n title: t('dialogs.delete-files-title', { count: ids.length }),\n message: t('dialogs.delete-files-text'),\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (agreed) {\n await api.remove(ids)\n await Promise.all([load(), loadFiles()])\n }\n}\n\nasync function rename(file: MediaFile, name: string): Promise<void> {\n await api.rename(file.id, name)\n await loadFiles()\n}\n\nasync function remove(file: MediaFile): Promise<void> {\n await api.removeOne(file.id)\n await Promise.all([load(), loadFiles()])\n}\n\n/**\n * The editor works on a preview and hands back a blob; the server is told the operations\n * instead and applies them to the original, at full size.\n *\n * Filters and adjustments are switched off for the same reason: they cannot be said as an\n * operation, and a picture that is quietly smaller than it was is worse than a button that is\n * not there.\n */\nasync function edit(file: MediaFile): Promise<void> {\n // `source`, not `url`: the editor draws the picture onto a canvas and writes that canvas out,\n // and a browser refuses to do that for bytes from another origin without CORS headers — which\n // is exactly what a CDN in front of the library is. `url` stays what everything that only\n // looks at the picture uses.\n const result = await openImageEditor({\n src: file.source ?? file.url,\n fileName: file.file_name,\n filters: false,\n // The editor is a component of the design system: its words are props with English\n // defaults, not lines in the panel's dictionary. Whoever opens it is the one who knows\n // what language the panel is being read in.\n ...editorLabels(),\n })\n\n if (!result) {\n return\n }\n\n await api.edit(file.id, {\n // The editor's frame is read against the picture as it is on screen — already turned — and\n // the server applies the operations in that order for the same reason.\n crop: result.crop,\n rotate: (((result.rotation % 360) + 360) % 360) as 0 | 90 | 180 | 270,\n flip: result.flipX ? 'horizontal' : result.flipY ? 'vertical' : undefined,\n resize: { width: result.width, height: result.height },\n })\n\n // Both addresses — the picture and its preview — carry the new version, so what is on screen\n // a moment later is what was just saved rather than what the browser kept.\n await loadFiles()\n}\n\nfunction find(id: number | null): MediaDirectory | null {\n const walk = (nodes: MediaDirectory[]): MediaDirectory | null => {\n for (const node of nodes) {\n if (node.id === id) {\n return node\n }\n\n const deeper = walk(node.children ?? [])\n\n if (deeper) {\n return deeper\n }\n }\n\n return null\n }\n\n return id === null ? null : walk(directories.value)\n}\n\nfunction countsOf(error: unknown): { files: number; directories: number } | null {\n const body = (\n error as { body?: { code?: string; counts?: { files: number; directories: number } } }\n )?.body\n\n return body?.code === 'directory_not_empty' ? (body.counts ?? null) : null\n}\n\nfunction messageOf(error: unknown): string | null {\n return (error as { message?: string })?.message ?? null\n}\n\n/** Bytes as somebody would say them: 32 MB, not 33 554 432. */\nfunction readable(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let size = bytes\n let unit = 0\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024\n unit++\n }\n\n return `${size >= 10 || unit === 0 ? Math.round(size) : size.toFixed(1)} ${units[unit]}`\n}\n\nfunction debounce(run: () => void, wait: number): () => void {\n let timer: ReturnType<typeof setTimeout> | undefined\n\n return () => {\n clearTimeout(timer)\n timer = setTimeout(run, wait)\n }\n}\n</script>\n\n<template>\n <div ref=\"root\" class=\"wx-media\" :class=\"{ 'wx-media--compact': compact }\">\n <aside v-if=\"!compact\" class=\"wx-media__folders\">\n <directory-tree v-model:selected=\"current\" :directories=\"directories\" @move=\"moveFolder\" />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </aside>\n\n <section class=\"wx-media__files\">\n <!--\n Uploading is offered while picking too. Most of the time the picture somebody is looking\n for is the one on their desk, and a picker that can only choose from what is already\n there sends them off to the library, to the upload, and back to a form they then have to\n find again.\n -->\n <media-toolbar\n v-model:search=\"search\"\n v-model:type=\"type\"\n v-model:sort=\"sort\"\n :can-upload=\"canUpload\"\n :fixed-type=\"accept !== null\"\n :can-manage=\"canManage\"\n :selected=\"selected.length\"\n :compact=\"compact\"\n @upload=\"choose\"\n @move=\"moveSelected\"\n @remove=\"removeSelected\"\n @folders=\"foldersOpen = true\"\n />\n\n <!--\n `accept` here is a hint to the file dialog, not a rule: what may be uploaded at all is\n the server's list of extensions, and it says so in a language the person reads.\n -->\n <input\n ref=\"picker\"\n type=\"file\"\n multiple\n hidden\n :accept=\"accept === null ? undefined : `${accept}/*`\"\n @change=\"upload\"\n />\n\n <file-grid\n v-model:selected=\"selected\"\n :files=\"rows\"\n :api=\"api\"\n :query=\"search\"\n :single=\"picking\"\n @rename=\"rename\"\n @edit=\"edit\"\n @remove=\"remove\"\n @open=\"(file) => emit('pick', file)\"\n />\n\n <footer class=\"wx-media__status\">\n <span>{{ t('manager.status-files', { count: page?.stats.files ?? 0 }) }}</span>\n <span v-if=\"selected.length > 0\">\n {{ t('manager.status-selected', { count: selected.length }) }}\n </span>\n <span>{{ t('manager.status-size', { size: readable(page?.stats.size ?? 0) }) }}</span>\n </footer>\n\n <wx-pagination\n v-if=\"page && page.meta.last_page > 1\"\n :page=\"page.meta.current_page\"\n :per-page=\"page.meta.per_page\"\n :total=\"page.meta.total\"\n :last-page=\"page.meta.last_page\"\n :disabled=\"busy\"\n size=\"sm\"\n @change=\"({ page: to }) => loadFiles(to)\"\n />\n </section>\n\n <!-- On a narrow screen the folders are a drawer: a tree beside a grid leaves room for\n neither, and an editor on a phone is looking for one thing at a time. -->\n <wx-drawer v-model:open=\"foldersOpen\" side=\"left\" :title=\"t('manager.folders')\" :size=\"280\">\n <div class=\"wx-media__drawer\">\n <directory-tree\n v-model:selected=\"current\"\n :directories=\"directories\"\n @move=\"moveFolder\"\n @update:selected=\"foldersOpen = false\"\n />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </div>\n </wx-drawer>\n </div>\n</template>\n\n<style>\n.wx-media {\n display: grid;\n grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);\n gap: var(--wx-space-16);\n min-height: 0;\n height: 100%;\n}\n\n.wx-media--compact {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--wx-space-8);\n}\n\n.wx-media__folders {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-8);\n min-height: 0;\n border-right: 1px solid var(--wx-color-border);\n padding-right: var(--wx-space-12);\n}\n\n/* The drawer hands its content the edge of the screen; the tree and its actions want air. */\n.wx-media__drawer {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n padding-block: var(--wx-space-4);\n}\n\n.wx-media__status {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n border-top: 1px solid var(--wx-color-border);\n padding-top: var(--wx-space-6);\n}\n\n.wx-media__files {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n min-height: 0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { WxCard } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\n\n/**\n * The library as a page of the panel.\n *\n * On its own, the manager sits straight on the panel's background, which reads as something\n * that has not finished loading. In a dialog it already has walls; here it needs its own.\n */\n</script>\n\n<template>\n <wx-card class=\"wx-media-page\" padding=\"md\">\n <media-manager />\n </wx-card>\n</template>\n\n<style>\n.wx-media-page {\n height: 100%;\n min-height: 0;\n}\n\n/* The card is the page: its body is what scrolls, not the page behind it. */\n.wx-media-page > .wx-card__body {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxDialog } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\nimport type { MediaFile, MediaKind } from './types'\n\n/**\n * The library as a dialog, for a form that needs a picture.\n *\n * The same manager, not a second one: a picker that browses differently from the manager is a\n * second thing to learn and a second thing to keep working.\n */\nwithDefaults(\n defineProps<{\n accept?: MediaKind | null\n title?: string\n /** Opened to manage the library rather than to choose from it. */\n manage?: boolean\n }>(),\n { accept: 'image', title: undefined, manage: false },\n)\n\nconst { open, resolve } = useModal<MediaFile>()\n\nconst t = useTranslate('webx-media')\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title ?? t('module.title')\" :width=\"960\">\n <div class=\"wx-media-picker\">\n <media-manager\n :picking=\"!manage\"\n :accept=\"manage ? null : accept\"\n @pick=\"(file) => resolve(file)\"\n />\n </div>\n </wx-dialog>\n</template>\n\n<style>\n.wx-media-picker {\n height: min(70vh, 560px);\n min-height: 0;\n}\n</style>\n","import { createModal } from '@webx-ui/core'\nimport MediaPicker from './MediaPicker.vue'\nimport type { MediaFile, MediaKind } from './types'\n\nexport interface MediaPickerOptions {\n /** Narrow what can be chosen; `null` offers everything. */\n accept?: MediaKind | null\n title?: string\n}\n\n/**\n * The library, opened from code, resolving with the file that was chosen.\n *\n * A form does not need to own a dialog for this — `const file = await openMediaPicker()` is the\n * whole integration, and `undefined` means the person closed it.\n */\nexport const openMediaPicker = createModal<MediaFile, MediaPickerOptions>(MediaPicker)\n\n/**\n * The whole library as a dialog, from anywhere.\n *\n * The same manager, the same folders, the same uploads — for a screen that needs to put files in\n * order without navigating away from what it was doing. It resolves when a file is opened, and\n * with `undefined` when the dialog is simply closed.\n */\nexport const openMediaLibrary = createModal<MediaFile, Omit<MediaPickerOptions, 'accept'>>(\n MediaPicker,\n { props: { manage: true } },\n)\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n WxAction,\n WxActions,\n WxButton,\n WxImage,\n WxInput,\n WxPopconfirm,\n WxPopover,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport { createMediaApi } from './api'\nimport { openMediaPicker } from './openMediaPicker'\nimport { useMediaMessages } from './i18n'\nimport type { MediaAspect, MediaKind, MediaValue } from './types'\n\n/**\n * A picture on a form, with the words this entity uses for it.\n *\n * Two states and nothing in between: an empty frame that opens the library, and the picture\n * with what can be done to it. `alt` and `title` live here rather than on the file, because one\n * picture used by two articles needs two captions — and they are localized for the same reason\n * everything else on a form is.\n *\n * What the field stores is the key and those words. The address is worked out when the page is\n * drawn, so moving the library to another disk changes nothing written before.\n */\nconst props = withDefaults(\n defineProps<{\n label?: string\n accept?: MediaKind | null\n /** Off for a decorative picture, where a caption is noise. */\n captions?: boolean\n /**\n * Shape of the frame: one of the presets, any `width / height`, or `null` for a fixed\n * height instead.\n *\n * A field that holds a cover and a field that holds an avatar are not the same shape, and\n * the frame should say which before anything is in it.\n */\n aspect?: MediaAspect | null\n /** Height of the frame when there is no aspect — a number in pixels, or any CSS length. */\n height?: number | string\n disabled?: boolean\n }>(),\n {\n label: undefined,\n accept: 'image',\n captions: true,\n aspect: '16/9',\n height: 220,\n disabled: false,\n },\n)\n\nconst value = defineModel<MediaValue | null>({ default: null })\n\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\nconst api = createMediaApi(useAdmin())\n\n/** The presets, spelled the way CSS wants them. Anything else is passed through as given. */\nconst RATIOS: Record<string, string> = {\n '16/9': '16 / 9',\n '4/3': '4 / 3',\n '1/1': '1 / 1',\n}\n\nconst editing = ref(false)\n\n/*\n * What a record stores is the key. The address beside it is only there when the picture was\n * chosen in this session, so a form opened on something saved last week has a key and nothing\n * to draw — and the library is the only thing that knows where that key currently lives.\n */\nconst resolved = ref<Record<string, string>>({})\n\nwatch(\n () => value.value?.path,\n async (path) => {\n if (!path || value.value?.url || resolved.value[path]) return\n\n const file = await api.fileByPath(path)\n\n if (file) resolved.value = { ...resolved.value, [path]: file.url }\n },\n { immediate: true },\n)\n\nconst preview = computed(() => {\n const current = value.value\n\n if (!current) return null\n\n return current.url ?? resolved.value[current.path] ?? null\n})\n\nconst frameStyle = computed(() =>\n props.aspect\n ? { aspectRatio: RATIOS[props.aspect] ?? props.aspect }\n : { height: typeof props.height === 'number' ? `${props.height}px` : props.height },\n)\n\n/**\n * `alt` and `title` are edited through the field, so they have to be readable as a record even\n * when what was stored is the plain string a column held before the site had a second language.\n */\nfunction words(key: 'alt' | 'title'): LocalizedValue | string {\n return value.value?.[key] ?? {}\n}\n\nfunction setWords(key: 'alt' | 'title', next: unknown): void {\n if (!value.value) return\n\n value.value = { ...value.value, [key]: next as LocalizedValue }\n}\n\nasync function choose(): Promise<void> {\n if (props.disabled) return\n\n const file = await openMediaPicker({ accept: props.accept })\n\n if (!file) return\n\n // The captions belong to the entity, so picking a different picture keeps them: usually the\n // words are still right and retyping them in three languages is the punishment for a swap.\n value.value = { ...value.value, path: file.path, url: file.url }\n}\n\n/** Clears the field. The file stays in the library — it is very likely used somewhere else. */\nfunction clear(): void {\n value.value = null\n editing.value = false\n}\n</script>\n\n<template>\n <div class=\"wx-media-field\">\n <span v-if=\"label\" class=\"wx-media-field__label\">{{ label }}</span>\n\n <div v-if=\"!value\" class=\"wx-media-field__frame is-empty\" :style=\"frameStyle\">\n <button type=\"button\" class=\"wx-media-field__pick\" :disabled=\"disabled\" @click=\"choose\">\n {{ t('field.select') }}\n </button>\n </div>\n\n <div v-else class=\"wx-media-field__frame\" :style=\"frameStyle\">\n <!--\n The picture and the buttons are wrapped in elements of our own rather than styled\n through their class: a component of the design system carries scoped rules, and a scoped\n rule is a class plus an attribute, so a single class from outside loses to it — silently,\n and only in a browser.\n\n The picture is also the way to swap it. A button labelled \"replace\" inside the captions\n popover is a second place to look for something the picture itself already offers.\n -->\n <button\n type=\"button\"\n class=\"wx-media-field__picture\"\n :disabled=\"disabled\"\n :title=\"t('field.replace')\"\n @click=\"choose\"\n >\n <wx-image v-if=\"preview\" :src=\"preview\" fit=\"contain\" width=\"100%\" height=\"100%\" />\n <span v-else class=\"wx-media-field__missing\">{{ t('field.no-preview') }}</span>\n </button>\n\n <div v-if=\"!disabled\" class=\"wx-media-field__bar\">\n <wx-actions size=\"sm\">\n <!--\n The captions are asked for where the picture is, not in a dialog over the whole form:\n what they describe has to stay in sight while they are being written.\n -->\n <wx-popover\n v-if=\"captions\"\n v-model:open=\"editing\"\n :title=\"t('field.captions')\"\n :width=\"360\"\n side=\"top\"\n teleport\n >\n <template #trigger>\n <wx-action type=\"edit\" :title=\"t('field.edit')\" />\n </template>\n\n <div class=\"wx-media-field__captions\">\n <div class=\"wx-media-field__caption\">\n <label class=\"wx-media-field__caption-label\" for=\"wx-media-alt\">alt</label>\n <wx-input\n id=\"wx-media-alt\"\n localized\n :model-value=\"words('alt')\"\n :placeholder=\"t('field.alt-hint')\"\n @update:model-value=\"(next) => setWords('alt', next)\"\n />\n </div>\n\n <div class=\"wx-media-field__caption\">\n <label class=\"wx-media-field__caption-label\" for=\"wx-media-title\">title</label>\n <wx-input\n id=\"wx-media-title\"\n localized\n :model-value=\"words('title')\"\n :placeholder=\"t('field.title-hint')\"\n @update:model-value=\"(next) => setWords('title', next)\"\n />\n </div>\n\n <div class=\"wx-media-field__captions-footer\">\n <wx-button size=\"sm\" type=\"primary\" @click=\"editing = false\">\n {{ t('manager.save') }}\n </wx-button>\n </div>\n </div>\n </wx-popover>\n\n <!--\n Clearing the field is not deleting the file: the same picture is very likely used by\n another record, and this is the one place somebody would expect otherwise.\n -->\n <wx-popconfirm\n :title=\"t('field.clear-title')\"\n :description=\"t('field.clear-text')\"\n :confirm-text=\"t('field.clear-confirm')\"\n :cancel-text=\"t('manager.cancel')\"\n confirm-type=\"danger\"\n side=\"top\"\n @confirm=\"clear\"\n >\n <template #trigger>\n <wx-action type=\"remove\" :title=\"t('field.clear-confirm')\" />\n </template>\n </wx-popconfirm>\n </wx-actions>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n.wx-media-field {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-6);\n}\n\n.wx-media-field__label {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-text-muted);\n}\n\n.wx-media-field__frame {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n padding: var(--wx-space-8);\n border: 1px solid var(--wx-border-default);\n border-radius: var(--wx-radius-md);\n background: var(--wx-bg-surface);\n overflow: hidden;\n}\n\n.wx-media-field__frame.is-empty {\n border-style: dashed;\n padding: 0;\n}\n\n.wx-media-field__pick {\n width: 100%;\n height: 100%;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--wx-text-placeholder);\n font-family: inherit;\n font-size: var(--wx-font-size-md);\n cursor: pointer;\n transition: color var(--wx-duration-fast) var(--wx-easing-standard);\n}\n\n.wx-media-field__pick:hover:not(:disabled) {\n color: var(--wx-text-muted);\n}\n\n.wx-media-field__pick:disabled {\n cursor: not-allowed;\n}\n\n.wx-media-field__picture {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n min-width: 0;\n padding: 0;\n border: none;\n /* Inscribed in the frame, so rounded a step less than it — concentric, not parallel. */\n border-radius: var(--wx-radius-xs);\n overflow: hidden;\n background: transparent;\n cursor: pointer;\n}\n\n.wx-media-field__picture:disabled {\n cursor: not-allowed;\n}\n\n.wx-media-field__missing {\n color: var(--wx-text-placeholder);\n font-size: var(--wx-font-size-sm);\n}\n\n/* On the picture, out of the way of it: the frame is the preview, not a toolbar. */\n.wx-media-field__bar {\n position: absolute;\n /* Clear of the frame's inner edge rather than sitting on it. */\n bottom: var(--wx-space-16);\n left: 50%;\n transform: translateX(-50%);\n padding: var(--wx-space-4);\n border-radius: var(--wx-radius-control);\n background: var(--wx-bg-surface);\n box-shadow: var(--wx-shadow-sm);\n}\n\n/* A label belongs to the field under it, so it sits closer to that than to the next one. */\n.wx-media-field__captions {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n width: 100%;\n}\n\n.wx-media-field__caption {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-2);\n}\n\n.wx-media-field__caption-label {\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-xs);\n}\n\n.wx-media-field__captions-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--wx-space-8);\n}\n</style>\n","import type { AdminModule } from '@webx-ui/module-admin'\nimport MediaPage from './MediaPage.vue'\nimport { mediaMessages } from './messages'\n\nexport interface MediaOptions {\n /** Where the section lives inside the panel. */\n path?: string\n}\n\n/**\n * The file manager as a section of the panel.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed, and stays out of the way when\n * only one is.\n */\nexport function media(options: MediaOptions = {}): AdminModule {\n const path = options.path ?? '/media'\n\n return {\n id: 'media',\n path,\n routes: [\n {\n path,\n name: 'webx.media',\n component: MediaPage,\n },\n ],\n }\n}\n\nexport { mediaMessages }\nexport { createMediaApi, type MediaApi } from './api'\nexport { openMediaLibrary, openMediaPicker, type MediaPickerOptions } from './openMediaPicker'\nexport { default as WxMediaManager } from './MediaManager.vue'\nexport { default as WxMediaPage } from './MediaPage.vue'\nexport { default as WxMediaPicker } from './MediaPicker.vue'\nexport { default as WxMediaField } from './MediaField.vue'\nexport type {\n DirectoryNotEmpty,\n EditOperations,\n FileQuery,\n MediaDirectory,\n MediaFile,\n MediaAspect,\n MediaKind,\n MediaPage,\n MediaValue,\n} from './types'\n"],"names":["props","__props","selected","_useModel","emit","__emit","t","useTranslate","nodes","ref","expanded","watch","directories","label","keysOf","directory","onDrop","event","parent","_createBlock","_unref","WxTree","$event","empty","computed","copied","toast","copyFailed","file","area","done","WxSelectionArea","_withCtx","isSelected","_createElementBlock","_Fragment","_renderList","WxFileCard","name","_directive_wx_select","WxEmpty","search","type","sort","types","kind","sorts","typeLabel","one","sortLabel","_openBlock","_hoisted_1","WxAction","_createVNode","WxInput","WxDropdown","option","WxDropdownItem","_createTextVNode","_toDisplayString","WxActions","open","resolve","dismiss","useModal","options","flatten","target","WxDialog","WxSpace","WxButton","_cache","WxSelect","input","useTemplateRef","onMounted","nextTick","submit","value","_createElementVNode","createMediaApi","admin","base","data","body","parentId","title","id","force","query","path","directoryId","files","onProgress","reject","request","token","csrfToken","answer","ids","operations","width","height","fit","url","cookie","part","mediaMessages","seeded","useMediaMessages","i18n","useAdmin","api","editorLabels","root","useElementWidth","compact","current","page","busy","foldersOpen","picker","rows","canManage","canUpload","folder","find","load","loadFiles","debounce","narrow","to","choose","upload","chosen","stored","duplicates","error","messageOf","askName","createModal","NameDialog","createFolder","renameFolder","deleteFolder","counts","countsOf","confirm","moveFolder","askWhereTo","MoveDialog","moveSelected","removeSelected","rename","remove","edit","result","openImageEditor","walk","node","deeper","readable","bytes","units","size","unit","run","wait","timer","_normalizeClass","DirectoryTree","_hoisted_2","MediaToolbar","FileGrid","_hoisted_4","_hoisted_5","WxPagination","WxDrawer","_hoisted_6","WxCard","MediaManager","openMediaPicker","MediaPicker","openMediaLibrary","RATIOS","editing","resolved","preview","frameStyle","words","key","setWords","next","clear","WxImage","WxPopover","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","WxPopconfirm","_hoisted_3","media","MediaPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/DirectoryTree.vue","../src/FileGrid.vue","../src/MediaToolbar.vue","../src/MoveDialog.vue","../src/NameDialog.vue","../src/messages.ts","../src/i18n.ts","../src/MediaManager.vue","../src/MediaPicker.vue","../src/openMediaPicker.ts","../src/MediaField.vue","../src/MediaPage.vue","../src/index.ts"],"sourcesContent":["import type { AdminContext } from '@webx-ui/module-admin'\nimport type { EditOperations, FileQuery, MediaDirectory, MediaFile, MediaPage } from './types'\n\nexport interface MediaApi {\n directories(): Promise<MediaDirectory[]>\n createDirectory(parentId: number, title: string): Promise<MediaDirectory>\n renameDirectory(id: number, title: string): Promise<MediaDirectory>\n moveDirectory(id: number, parentId: number): Promise<MediaDirectory>\n /** Without `force` the server refuses a folder that holds anything, and says what it holds. */\n deleteDirectory(id: number, force?: boolean): Promise<void>\n\n files(query?: FileQuery): Promise<MediaPage>\n file(id: number): Promise<MediaFile>\n /**\n * The file behind a key.\n *\n * An entity stores the key and nothing else, so this is how a form draws the picture it\n * saved last time. `null` when the file is gone from the library.\n */\n fileByPath(path: string): Promise<MediaFile | null>\n upload(\n directoryId: number,\n files: File[],\n onProgress?: (percent: number) => void,\n ): Promise<MediaFile[]>\n rename(id: number, name: string): Promise<MediaFile>\n move(ids: number[], directoryId: number): Promise<number>\n remove(ids: number[]): Promise<number>\n removeOne(id: number): Promise<void>\n\n edit(id: number, operations: EditOperations): Promise<MediaFile>\n copy(id: number): Promise<MediaFile>\n restoreOriginal(id: number): Promise<MediaFile>\n\n /** The address of a preview at a size the server allows. */\n thumb(file: MediaFile, width: number, height?: number, fit?: 'cover' | 'contain'): string | null\n}\n\nexport function createMediaApi(admin: AdminContext): MediaApi {\n const base = `${admin.apiPath}/media`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n directories: () => admin.http.get<{ data: MediaDirectory[] }>(`${base}/directories`).then(data),\n\n createDirectory: (parentId, title) =>\n admin.http\n .post<{ data: MediaDirectory }>(`${base}/directories`, { parent_id: parentId, title })\n .then(data),\n\n renameDirectory: (id, title) =>\n admin.http.patch<{ data: MediaDirectory }>(`${base}/directories/${id}`, { title }).then(data),\n\n moveDirectory: (id, parentId) =>\n admin.http\n .patch<{ data: MediaDirectory }>(`${base}/directories/${id}/move`, { parent_id: parentId })\n .then(data),\n\n deleteDirectory: (id, force = false) =>\n admin.http.delete<void>(`${base}/directories/${id}`, {\n query: { force: force ? 1 : undefined },\n }),\n\n files: (query = {}) =>\n admin.http.get<MediaPage>(`${base}/files`, {\n query: {\n directory_id: query.directory_id ?? undefined,\n q: query.q || undefined,\n type: query.type ?? undefined,\n sort: query.sort,\n page: query.page,\n per_page: query.per_page,\n },\n }),\n\n file: (id) => admin.http.get<{ data: MediaFile }>(`${base}/files/${id}`).then(data),\n\n fileByPath: (path) =>\n admin.http\n .get<{ data: MediaFile }>(`${base}/files/by-path?path=${encodeURIComponent(path)}`)\n .then(data)\n .catch(() => null),\n\n async upload(directoryId, files, onProgress) {\n const body = new FormData()\n body.append('directory_id', String(directoryId))\n files.forEach((file) => body.append('files[]', file))\n\n // XHR rather than fetch, for the one thing fetch still cannot do: say how far an upload\n // has got. A person watching a forty-megabyte video wants a bar, not a spinner.\n return new Promise<MediaFile[]>((resolve, reject) => {\n const request = new XMLHttpRequest()\n\n request.open('POST', `${base}/files`)\n request.responseType = 'json'\n request.withCredentials = true\n request.setRequestHeader('Accept', 'application/json')\n request.setRequestHeader('X-Requested-With', 'XMLHttpRequest')\n // Without this the refusal comes back in English while the panel is in Russian: the\n // panel's own client sends it on every request, and this one is not that client.\n request.setRequestHeader('X-Webx-Locale', admin.i18n.state.locale)\n\n const token = csrfToken()\n if (token) {\n request.setRequestHeader('X-XSRF-TOKEN', token)\n }\n\n request.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * 100))\n }\n })\n\n request.addEventListener('load', () => {\n const answer = request.response as { data?: MediaFile[]; message?: string } | null\n\n if (request.status >= 200 && request.status < 300 && answer?.data) {\n resolve(answer.data)\n\n return\n }\n\n reject(\n Object.assign(new Error(answer?.message ?? 'Upload failed'), {\n status: request.status,\n body: answer,\n }),\n )\n })\n\n request.addEventListener('error', () => reject(new Error('Upload failed')))\n request.send(body)\n })\n },\n\n rename: (id, name) =>\n admin.http.patch<{ data: MediaFile }>(`${base}/files/${id}`, { name }).then(data),\n\n move: (ids, directoryId) =>\n admin.http\n .post<{ data: { moved: number } }>(`${base}/files/move`, { ids, directory_id: directoryId })\n .then((body) => body.data.moved),\n\n // A POST for the batch, not a DELETE: the panel's own client sends no body on DELETE, and\n // a list of ids in a query string is a worse answer than an honest verb.\n remove: (ids) =>\n admin.http\n .post<{ data: { deleted: number } }>(`${base}/files/delete`, { ids })\n .then((body) => body.data.deleted),\n\n removeOne: (id) => admin.http.delete<void>(`${base}/files/${id}`),\n\n edit: (id, operations) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/edit`, operations).then(data),\n\n copy: (id) => admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/copy`).then(data),\n\n restoreOriginal: (id) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/restore-original`).then(data),\n\n thumb(file, width, height, fit = 'cover') {\n if (!file.thumb) {\n return null\n }\n\n const url = new URL(file.thumb, window.location.origin)\n url.searchParams.set('w', String(width))\n if (height) {\n url.searchParams.set('h', String(height))\n }\n url.searchParams.set('fit', fit)\n\n return url.pathname + url.search\n },\n }\n}\n\nfunction csrfToken(): string | null {\n const cookie = document.cookie.split('; ').find((part) => part.startsWith('XSRF-TOKEN='))\n\n return cookie ? decodeURIComponent(cookie.slice('XSRF-TOKEN='.length)) : null\n}\n","<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { WxTree, type TreeDropEvent } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * The folders, on the left.\n *\n * The root carries a translated label rather than the title it was given in the database: it is\n * the library itself, and an editor should not have to know it is a row.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n}>()\n\nconst selected = defineModel<number | null>('selected', { default: null })\n\nconst emit = defineEmits<{\n move: [id: number, parentId: number]\n}>()\n\nconst t = useTranslate('webx-media')\n\ntype Node = MediaDirectory & { label: string }\n\n// The tree rearranges this list itself when something is dropped, and then says so; the answer\n// from the server replaces it a moment later. A computed would have nothing to rearrange.\nconst nodes = ref<Node[]>([])\nconst expanded = ref<number[]>([])\n\nwatch(\n () => props.directories,\n (directories) => {\n nodes.value = directories.map(label)\n expanded.value = keysOf(directories)\n },\n { immediate: true, deep: true },\n)\n\nfunction label(directory: MediaDirectory): Node {\n return {\n ...directory,\n label: directory.is_root ? t('manager.root') : directory.title,\n children: (directory.children ?? []).map(label),\n }\n}\n\nfunction keysOf(directories: MediaDirectory[]): number[] {\n return directories.flatMap((directory) => [directory.id, ...keysOf(directory.children ?? [])])\n}\n\nfunction onDrop(event: TreeDropEvent<Node>): void {\n const parent = event.zone === 'inside' ? event.target : event.parent\n\n if (parent) {\n emit('move', event.node.id, parent.id)\n }\n}\n</script>\n\n<template>\n <wx-tree\n v-model:selected=\"selected\"\n v-model:expanded=\"expanded\"\n v-model=\"nodes\"\n class=\"wx-media-tree\"\n node-key=\"id\"\n draggable\n show-lines\n @drop=\"onDrop\"\n />\n</template>\n\n<style>\n.wx-media-tree {\n min-width: 0;\n overflow: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { toast, WxEmpty, WxFileCard, WxSelectionArea } from '@webx-ui/core'\nimport type { MediaApi } from './api'\nimport type { MediaFile } from './types'\n\n/**\n * The files, as cards, with a rubber band over them.\n *\n * Previews come from the server's thumbnail endpoint rather than the files themselves: a grid of\n * forty photographs is forty full-size images otherwise, and on a phone that is the difference\n * between a screen and a wait.\n */\nconst props = defineProps<{\n files: MediaFile[]\n api: MediaApi\n query?: string\n /** A picker takes one file; the manager selects to act on a batch. */\n single?: boolean\n}>()\n\nconst selected = defineModel<number[]>('selected', { default: () => [] })\n\nconst emit = defineEmits<{\n open: [file: MediaFile]\n rename: [file: MediaFile, name: string]\n edit: [file: MediaFile]\n remove: [file: MediaFile]\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst empty = computed(() =>\n props.query ? t('manager.empty-search', { query: props.query }) : t('manager.empty'),\n)\n\nfunction copied(): void {\n toast.success(t('manager.link-copied'))\n}\n\n/**\n * The clipboard is not always there: it is missing outside a secure context, and an iframe has\n * to be granted it. Rather than let the card fail silently, try the old way — and if that is\n * refused too, put the address on screen, where it can at least be copied by hand.\n */\nfunction copyFailed(file: MediaFile): void {\n const area = document.createElement('textarea')\n area.value = file.url\n area.setAttribute('readonly', '')\n area.style.position = 'fixed'\n area.style.opacity = '0'\n document.body.appendChild(area)\n area.select()\n\n let done = false\n\n try {\n done = document.execCommand('copy')\n } catch {\n done = false\n }\n\n area.remove()\n\n if (done) {\n copied()\n\n return\n }\n\n toast.danger(`${t('errors.copy')} ${file.url}`, { duration: 0 })\n}\n</script>\n\n<template>\n <wx-selection-area\n v-if=\"files.length > 0\"\n v-slot=\"{ isSelected }\"\n v-model=\"selected\"\n class=\"wx-media-grid\"\n :multiple=\"!single\"\n >\n <wx-file-card\n v-for=\"file in files\"\n :key=\"file.id\"\n v-wx-select=\"file.id\"\n class=\"wx-media-grid__card\"\n :name=\"file.name\"\n :url=\"file.url\"\n :thumbnail=\"api.thumb(file, 320, 320) ?? undefined\"\n :type=\"file.mime\"\n :selected=\"isSelected(file.id)\"\n renamable\n :editable=\"file.editable\"\n removable\n copyable\n :rename-label=\"t('manager.rename')\"\n :edit-label=\"t('manager.edit')\"\n :remove-label=\"t('manager.delete')\"\n :remove-confirm-text=\"t('dialogs.delete-file', { name: file.name })\"\n :cancel-label=\"t('manager.cancel')\"\n :save-label=\"t('manager.save')\"\n :copy-label=\"t('manager.copy-link')\"\n :copied-label=\"t('manager.link-copied')\"\n @rename=\"(name) => emit('rename', file, name)\"\n @edit=\"emit('edit', file)\"\n @remove=\"emit('remove', file)\"\n @copy=\"copied\"\n @copy-error=\"copyFailed(file)\"\n @dblclick=\"emit('open', file)\"\n />\n </wx-selection-area>\n\n <wx-empty v-else :description=\"empty\" />\n</template>\n\n<style>\n.wx-media-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: var(--wx-space-12);\n align-content: start;\n min-height: 0;\n overflow: auto;\n padding: var(--wx-space-2);\n /* A rubber band over names would otherwise select the names. */\n user-select: none;\n}\n\n.wx-media--compact .wx-media-grid {\n grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n gap: var(--wx-space-8);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { WxAction, WxActions, WxDropdown, WxDropdownItem, WxInput } from '@webx-ui/core'\nimport type { MediaKind } from './types'\n\n/**\n * One row of icons over the files.\n *\n * Actions rather than labelled buttons: the row has to survive a phone and a picker dialog half\n * the width of a window, and a row of words does not. What each icon means is a tooltip away,\n * and what is chosen is said in the icon's own label.\n */\nconst props = defineProps<{\n canUpload: boolean\n canManage: boolean\n selected: number\n /** The caller asked for one kind of file, so there is nothing to filter by. */\n fixedType?: boolean\n /** Narrow screens get the folders behind a button instead of beside the files. */\n compact: boolean\n}>()\n\nconst search = defineModel<string>('search', { default: '' })\nconst type = defineModel<MediaKind | 'all'>('type', { default: 'all' })\nconst sort = defineModel<string>('sort', { default: '-created_at' })\n\nconst emit = defineEmits<{\n upload: []\n move: []\n remove: []\n folders: []\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst types = computed<{ value: MediaKind | 'all'; label: string }[]>(() => [\n { value: 'all', label: t('manager.all-types') },\n ...(['image', 'video', 'audio', 'document', 'other'] as MediaKind[]).map((kind) => ({\n value: kind,\n label: t(`manager.${kind}`),\n })),\n])\n\nconst sorts = computed(() => [\n { value: '-created_at', label: t('manager.sort-newest') },\n { value: 'created_at', label: t('manager.sort-oldest') },\n { value: 'name', label: t('manager.sort-name') },\n { value: '-size', label: t('manager.sort-size') },\n])\n\nconst typeLabel = computed(() => types.value.find((one) => one.value === type.value)?.label ?? '')\nconst sortLabel = computed(() => sorts.value.find((one) => one.value === sort.value)?.label ?? '')\n</script>\n\n<template>\n <header class=\"wx-media-toolbar\">\n <wx-action\n v-if=\"compact\"\n icon=\"folder\"\n :title=\"t('manager.folders')\"\n size=\"sm\"\n @click=\"emit('folders')\"\n />\n\n <wx-input\n v-model=\"search\"\n class=\"wx-media-toolbar__search\"\n :placeholder=\"t('manager.search')\"\n clearable\n size=\"sm\"\n />\n\n <wx-dropdown v-if=\"!fixedType\">\n <template #trigger>\n <wx-action icon=\"filter\" :title=\"`${t('manager.all-types')}: ${typeLabel}`\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in types\"\n :key=\"String(option.value)\"\n :active=\"option.value === type\"\n @click=\"type = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-dropdown>\n <template #trigger>\n <!-- Not `type=\"sort\"`: in this set that means reordering by hand, and its icon is a drag handle. -->\n <wx-action icon=\"arrow-down\" :title=\"sortLabel\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in sorts\"\n :key=\"option.value\"\n :active=\"option.value === sort\"\n @click=\"sort = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-action\n v-if=\"canUpload\"\n type=\"upload\"\n :title=\"t('manager.upload')\"\n size=\"sm\"\n @click=\"emit('upload')\"\n />\n\n <!-- Kept in the row rather than added to it: the icons are always here and simply cannot\n be used while nothing is selected, so the grid never moves down a line. -->\n <wx-actions v-if=\"canManage\" size=\"sm\">\n <wx-action\n icon=\"folder\"\n :title=\"t('manager.move')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('move')\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('remove')\"\n />\n </wx-actions>\n </header>\n</template>\n\n<style>\n.wx-media-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-6);\n}\n\n.wx-media-toolbar__search {\n flex: 1 1 180px;\n min-width: 120px;\n}\n\n.wx-media-toolbar__count {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n margin-inline-start: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxSelect, WxSpace } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * Where to put the selected files.\n *\n * A list of folders rather than a box to type in: an editor knows a folder by its name and its\n * place in the tree, and has no reason to ever learn that it also has a number.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n /** The folder they are in now, which is not worth offering. */\n from?: number | null\n count: number\n}>()\n\nconst { open, resolve, dismiss } = useModal<number>()\n\nconst t = useTranslate('webx-media')\n\nconst options = computed(() =>\n flatten(props.directories).filter((option) => option.value !== props.from),\n)\nconst target = ref<number | null>(options.value[0]?.value ?? null)\n\nfunction flatten(directories: MediaDirectory[]): { value: number; label: string }[] {\n return directories.flatMap((directory) => [\n {\n value: directory.id,\n // Indented, so the tree is still readable once it is a flat list.\n label:\n ' '.repeat(directory.depth * 3) + (directory.is_root ? t('manager.root') : directory.title),\n },\n ...flatten(directory.children ?? []),\n ])\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('manager.move-to')\" :width=\"420\">\n <wx-select v-model=\"target\" :options=\"options\" />\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"target === null\" @click=\"resolve(target!)\">\n {{ t('manager.move') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxInput, WxSpace } from '@webx-ui/core'\n\n/**\n * A name, asked for properly.\n *\n * `window.prompt` is not an option: a browser is free to refuse it — an embedded frame usually\n * does — and then the button simply does nothing, which is exactly how \"new folder\" looked.\n */\nconst props = withDefaults(\n defineProps<{\n title: string\n label?: string\n value?: string\n confirmText?: string\n }>(),\n { label: undefined, value: '', confirmText: undefined },\n)\n\nconst { open, resolve, dismiss } = useModal<string>()\n\nconst t = useTranslate('webx-media')\n\nconst name = ref(props.value)\nconst input = useTemplateRef<HTMLElement>('input')\n\nonMounted(async () => {\n await nextTick()\n input.value?.querySelector('input')?.focus()\n})\n\nfunction submit(): void {\n const value = name.value.trim()\n\n if (value !== '') {\n resolve(value)\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title\" :width=\"420\">\n <div ref=\"input\">\n <!-- The dialog's title says what is being named, so the field carries the same words as a\n placeholder and as its accessible name rather than repeating them above itself. -->\n <wx-input\n v-model=\"name\"\n :placeholder=\"label ?? t('manager.folder-name')\"\n :aria-label=\"label ?? t('manager.folder-name')\"\n @keyup.enter=\"submit\"\n />\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"name.trim() === ''\" @click=\"submit\">\n {{ confirmText ?? t('manager.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-media` ships as `lang/en/*.php`, and the same ones every other\n * language there has. Kept here so a manager placed by hand, with no WebX UI server behind it,\n * still has words — and so a key never reaches the screen when a translation is missing.\n */\nexport const mediaMessages: Record<string, Messages> = {\n module: {\n title: 'Files',\n },\n manager: {\n root: 'Library',\n search: 'Search files',\n 'new-folder': 'New folder',\n 'folder-name': 'Folder name',\n rename: 'Rename',\n move: 'Move',\n 'move-to': 'Move to…',\n delete: 'Delete',\n upload: 'Upload',\n 'upload-hint': 'Drop files here or choose them',\n select: 'Select',\n selected: ':count selected',\n empty: 'This folder is empty',\n 'empty-search': 'Nothing matches :query',\n 'all-types': 'All types',\n image: 'Images',\n video: 'Video',\n audio: 'Audio',\n document: 'Documents',\n other: 'Other',\n 'sort-newest': 'Newest first',\n 'sort-oldest': 'Oldest first',\n 'sort-name': 'By name',\n 'sort-size': 'Largest first',\n 'duplicate-added': 'That file was already in this folder',\n uploaded: ':count file(s) added',\n cancel: 'Cancel',\n save: 'Save',\n folders: 'Folders',\n edit: 'Edit',\n 'copy-link': 'Copy the link',\n 'link-copied': 'Link copied',\n 'status-files': 'Files: :count',\n 'status-selected': 'selected: :count',\n 'status-size': 'size: :size',\n },\n // The picture field on a form, which is the module used from outside its own section.\n field: {\n select: 'Choose a picture',\n edit: 'Captions',\n replace: 'Replace',\n captions: 'Captions',\n 'alt-hint': 'What is on the picture, for a reader who cannot see it',\n 'title-hint': 'A caption shown on hover',\n 'no-preview': 'No preview',\n 'clear-title': 'Clear the field?',\n 'clear-text': 'The file stays in the library — only this record stops using it.',\n 'clear-confirm': 'Clear',\n },\n // The image editor is a component of the design system, so its words are props rather than\n // lines in the dictionary — the manager hands it these when it opens it.\n editor: {\n title: 'Edit picture',\n save: 'Save',\n cancel: 'Cancel',\n reset: 'Reset',\n 'rotate-left': 'Turn left',\n 'rotate-right': 'Turn right',\n 'flip-horizontal': 'Mirror across',\n 'flip-vertical': 'Mirror down',\n ratio: 'Ratio',\n crop: 'Crop',\n output: 'Output',\n 'output-hint': 'The size of the picture you will get',\n width: 'Width',\n height: 'Height',\n free: 'Free',\n original: 'Original',\n error: 'This picture could not be loaded',\n },\n dialogs: {\n 'delete-files-title': 'Delete :count file(s)?',\n 'delete-files-text': 'Anywhere they are already used, they will stop opening.',\n 'delete-folder-title': 'Delete the folder :title?',\n 'delete-folder-contents': 'It holds :files file(s) and :directories folder(s).',\n 'delete-folder-warning':\n 'Everything inside goes too, and pictures already placed in content will stop opening.',\n confirm: 'Delete',\n 'delete-file': 'Delete :name?',\n },\n errors: {\n 'directory-not-empty': 'This folder is not empty.',\n 'root-immutable': 'The library root cannot be moved or deleted.',\n 'directory-into-itself': 'A folder cannot be moved into itself.',\n 'not-an-image': 'This file is not an image.',\n 'image-too-large': 'This image is larger than :megapixels megapixels.',\n upload: 'That file was not accepted.',\n copy: 'The clipboard is not available here. The address:',\n 'unsupported-type': 'Only these can be uploaded: :types.',\n 'file-too-large': 'The file is larger than :size MB.',\n 'too-many-files': 'No more than :count files at a time.',\n },\n files: {\n copy: ':name (copy)',\n },\n validation: {\n title: 'name',\n parent_id: 'parent folder',\n directory_id: 'folder',\n files: 'files',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { mediaMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/**\n * Put this package's English under the panel's dictionary, once per panel.\n *\n * The server's translations land on top of it when they arrive. What this prevents is the half\n * second — or the whole session, in a panel assembled without a server — where a screen shows\n * `manager.upload` instead of a word.\n */\nexport function useMediaMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-media', mediaMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n openImageEditor,\n toast,\n useElementWidth,\n WxAction,\n WxActions,\n WxDrawer,\n WxPagination,\n} from '@webx-ui/core'\nimport DirectoryTree from './DirectoryTree.vue'\nimport FileGrid from './FileGrid.vue'\nimport MediaToolbar from './MediaToolbar.vue'\nimport MoveDialog from './MoveDialog.vue'\nimport NameDialog from './NameDialog.vue'\nimport { createMediaApi } from './api'\nimport { useMediaMessages } from './i18n'\nimport type { MediaDirectory, MediaFile, MediaKind, MediaPage } from './types'\n\n/**\n * The library: folders on the left, files on the right, a row of icons over both.\n *\n * Everything the screen does is one request and a reload of the part that changed — there is no\n * cache here on purpose. A library is edited by several people, and a panel that believes its\n * own copy shows a folder that was emptied an hour ago.\n *\n * The width that decides the layout is this component's own, not the window's: the same manager\n * is a page, half of a picker dialog, and a phone.\n */\nconst props = withDefaults(\n defineProps<{\n /** A picker hands one file back instead of managing the library. */\n picking?: boolean\n /** Only these kinds can be chosen, when picking. */\n accept?: MediaKind | null\n }>(),\n { picking: false, accept: null },\n)\n\nconst emit = defineEmits<{ pick: [file: MediaFile] }>()\n\nconst admin = useAdmin()\nconst api = createMediaApi(admin)\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\n/**\n * The image editor's words, in the language the panel is being read in.\n *\n * Read when the editor is opened rather than kept in a computed: the dialog is mounted outside\n * the app, so it is handed plain strings once and nothing re-renders it afterwards.\n */\nfunction editorLabels(): Record<string, string> {\n return {\n title: t('editor.title'),\n saveLabel: t('editor.save'),\n cancelLabel: t('editor.cancel'),\n resetLabel: t('editor.reset'),\n rotateLeftLabel: t('editor.rotate-left'),\n rotateRightLabel: t('editor.rotate-right'),\n flipHorizontalLabel: t('editor.flip-horizontal'),\n flipVerticalLabel: t('editor.flip-vertical'),\n ratioLabel: t('editor.ratio'),\n cropLabel: t('editor.crop'),\n outputLabel: t('editor.output'),\n outputHint: t('editor.output-hint'),\n widthLabel: t('editor.width'),\n heightLabel: t('editor.height'),\n freeLabel: t('editor.free'),\n originalLabel: t('editor.original'),\n errorText: t('editor.error'),\n }\n}\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst width = useElementWidth(root)\nconst compact = computed(() => width.value > 0 && width.value < 640)\n\nconst directories = ref<MediaDirectory[]>([])\nconst current = ref<number | null>(null)\nconst page = ref<MediaPage | null>(null)\nconst selected = ref<number[]>([])\nconst search = ref('')\n/*\n * What the caller asked for is not a default the person can put back: a field that wants a\n * picture would otherwise be handed a PDF, and find out when the page renders.\n */\nconst type = ref<MediaKind | 'all'>(props.accept ?? 'all')\n\nwatch(\n () => props.accept,\n (kind) => {\n type.value = kind ?? 'all'\n },\n)\nconst sort = ref('-created_at')\nconst busy = ref(false)\nconst foldersOpen = ref(false)\nconst picker = useTemplateRef<HTMLInputElement>('picker')\n\nconst rows = computed(() => page.value?.data ?? [])\nconst canManage = computed(() => admin.can('media.manage'))\nconst canUpload = computed(() => admin.can('media.upload') || canManage.value)\nconst folder = computed(() => find(current.value))\n\nonMounted(load)\n\nwatch([current, type, sort], () => loadFiles(1))\nwatch(\n search,\n debounce(() => loadFiles(1), 300),\n)\nwatch(compact, (narrow) => {\n if (!narrow) {\n foldersOpen.value = false\n }\n})\n\nasync function load(): Promise<void> {\n directories.value = await api.directories()\n current.value ??= directories.value[0]?.id ?? null\n await loadFiles(1)\n}\n\nasync function loadFiles(to = page.value?.meta.current_page ?? 1): Promise<void> {\n busy.value = true\n\n try {\n page.value = await api.files({\n directory_id: search.value ? null : current.value,\n q: search.value,\n type: type.value === 'all' ? null : type.value,\n sort: sort.value,\n page: to,\n })\n selected.value = []\n } finally {\n busy.value = false\n }\n}\n\nfunction choose(): void {\n picker.value?.click()\n}\n\n/**\n * Uploading says what happened in a toast and nowhere else.\n *\n * A list of what was just added under the toolbar is a second place to look and a thing to\n * dismiss; the files themselves appear in the grid a moment later, which is the answer.\n */\nasync function upload(event: Event): Promise<void> {\n const input = event.target as HTMLInputElement\n const chosen = Array.from(input.files ?? [])\n\n // Cleared straight away, so choosing the same file twice in a row still counts as a change.\n input.value = ''\n\n if (chosen.length === 0 || current.value === null) {\n return\n }\n\n try {\n const stored = await api.upload(current.value, chosen)\n const duplicates = stored.filter((file) => file.duplicate).length\n\n if (stored.length > duplicates) {\n toast.success(t('manager.uploaded', { count: stored.length - duplicates }))\n }\n\n if (duplicates > 0) {\n toast.info(t('manager.duplicate-added'))\n }\n\n await Promise.all([load(), loadFiles(1)])\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.upload'))\n }\n}\n\nconst askName = createModal<\n string,\n { title: string; label?: string; value?: string; confirmText?: string }\n>(NameDialog)\n\nasync function createFolder(): Promise<void> {\n const title = await askName({\n title: t('manager.new-folder'),\n confirmText: t('manager.new-folder'),\n })\n\n if (title === undefined || current.value === null) {\n return\n }\n\n await api.createDirectory(current.value, title)\n await load()\n}\n\nasync function renameFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n const title = await askName({\n title: t('manager.rename'),\n value: target.title,\n confirmText: t('manager.save'),\n })\n\n if (title !== undefined) {\n await api.renameDirectory(target.id, title)\n await load()\n }\n}\n\nasync function deleteFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n // Asked twice on purpose, and the second question carries the counts the server sent back:\n // what goes with the folder is somebody's article illustrations.\n try {\n await api.deleteDirectory(target.id)\n } catch (error) {\n const counts = countsOf(error)\n\n if (!counts) {\n throw error\n }\n\n const agreed = await confirm({\n title: t('dialogs.delete-folder-title', { title: target.title }),\n message: `${t('dialogs.delete-folder-contents', counts)} ${t('dialogs.delete-folder-warning')}`,\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) {\n return\n }\n\n await api.deleteDirectory(target.id, true)\n }\n\n current.value = null\n await load()\n}\n\nasync function moveFolder(id: number, parentId: number): Promise<void> {\n try {\n await api.moveDirectory(id, parentId)\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.directory-into-itself'))\n }\n\n await load()\n}\n\nconst askWhereTo = createModal<\n number,\n { directories: MediaDirectory[]; from: number | null; count: number }\n>(MoveDialog)\n\nasync function moveSelected(): Promise<void> {\n const to = await askWhereTo({\n directories: directories.value,\n from: current.value,\n count: selected.value.length,\n })\n\n if (to === undefined) {\n return\n }\n\n await api.move([...selected.value], to)\n await Promise.all([load(), loadFiles()])\n}\n\nasync function removeSelected(): Promise<void> {\n const ids = [...selected.value]\n\n const agreed = await confirm({\n title: t('dialogs.delete-files-title', { count: ids.length }),\n message: t('dialogs.delete-files-text'),\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (agreed) {\n await api.remove(ids)\n await Promise.all([load(), loadFiles()])\n }\n}\n\nasync function rename(file: MediaFile, name: string): Promise<void> {\n await api.rename(file.id, name)\n await loadFiles()\n}\n\nasync function remove(file: MediaFile): Promise<void> {\n await api.removeOne(file.id)\n await Promise.all([load(), loadFiles()])\n}\n\n/**\n * The editor works on a preview and hands back a blob; the server is told the operations\n * instead and applies them to the original, at full size.\n *\n * Filters and adjustments are switched off for the same reason: they cannot be said as an\n * operation, and a picture that is quietly smaller than it was is worse than a button that is\n * not there.\n */\nasync function edit(file: MediaFile): Promise<void> {\n // `source`, not `url`: the editor draws the picture onto a canvas and writes that canvas out,\n // and a browser refuses to do that for bytes from another origin without CORS headers — which\n // is exactly what a CDN in front of the library is. `url` stays what everything that only\n // looks at the picture uses.\n const result = await openImageEditor({\n src: file.source ?? file.url,\n fileName: file.file_name,\n filters: false,\n // The editor is a component of the design system: its words are props with English\n // defaults, not lines in the panel's dictionary. Whoever opens it is the one who knows\n // what language the panel is being read in.\n ...editorLabels(),\n })\n\n if (!result) {\n return\n }\n\n await api.edit(file.id, {\n // The editor's frame is read against the picture as it is on screen — already turned — and\n // the server applies the operations in that order for the same reason.\n crop: result.crop,\n rotate: (((result.rotation % 360) + 360) % 360) as 0 | 90 | 180 | 270,\n flip: result.flipX ? 'horizontal' : result.flipY ? 'vertical' : undefined,\n resize: { width: result.width, height: result.height },\n })\n\n // Both addresses — the picture and its preview — carry the new version, so what is on screen\n // a moment later is what was just saved rather than what the browser kept.\n await loadFiles()\n}\n\nfunction find(id: number | null): MediaDirectory | null {\n const walk = (nodes: MediaDirectory[]): MediaDirectory | null => {\n for (const node of nodes) {\n if (node.id === id) {\n return node\n }\n\n const deeper = walk(node.children ?? [])\n\n if (deeper) {\n return deeper\n }\n }\n\n return null\n }\n\n return id === null ? null : walk(directories.value)\n}\n\nfunction countsOf(error: unknown): { files: number; directories: number } | null {\n const body = (\n error as { body?: { code?: string; counts?: { files: number; directories: number } } }\n )?.body\n\n return body?.code === 'directory_not_empty' ? (body.counts ?? null) : null\n}\n\nfunction messageOf(error: unknown): string | null {\n return (error as { message?: string })?.message ?? null\n}\n\n/** Bytes as somebody would say them: 32 MB, not 33 554 432. */\nfunction readable(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let size = bytes\n let unit = 0\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024\n unit++\n }\n\n return `${size >= 10 || unit === 0 ? Math.round(size) : size.toFixed(1)} ${units[unit]}`\n}\n\nfunction debounce(run: () => void, wait: number): () => void {\n let timer: ReturnType<typeof setTimeout> | undefined\n\n return () => {\n clearTimeout(timer)\n timer = setTimeout(run, wait)\n }\n}\n</script>\n\n<template>\n <div ref=\"root\" class=\"wx-media\" :class=\"{ 'wx-media--compact': compact }\">\n <aside v-if=\"!compact\" class=\"wx-media__folders\">\n <directory-tree v-model:selected=\"current\" :directories=\"directories\" @move=\"moveFolder\" />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </aside>\n\n <section class=\"wx-media__files\">\n <!--\n Uploading is offered while picking too. Most of the time the picture somebody is looking\n for is the one on their desk, and a picker that can only choose from what is already\n there sends them off to the library, to the upload, and back to a form they then have to\n find again.\n -->\n <media-toolbar\n v-model:search=\"search\"\n v-model:type=\"type\"\n v-model:sort=\"sort\"\n :can-upload=\"canUpload\"\n :fixed-type=\"accept !== null\"\n :can-manage=\"canManage\"\n :selected=\"selected.length\"\n :compact=\"compact\"\n @upload=\"choose\"\n @move=\"moveSelected\"\n @remove=\"removeSelected\"\n @folders=\"foldersOpen = true\"\n />\n\n <!--\n `accept` here is a hint to the file dialog, not a rule: what may be uploaded at all is\n the server's list of extensions, and it says so in a language the person reads.\n -->\n <input\n ref=\"picker\"\n type=\"file\"\n multiple\n hidden\n :accept=\"accept === null ? undefined : `${accept}/*`\"\n @change=\"upload\"\n />\n\n <file-grid\n v-model:selected=\"selected\"\n :files=\"rows\"\n :api=\"api\"\n :query=\"search\"\n :single=\"picking\"\n @rename=\"rename\"\n @edit=\"edit\"\n @remove=\"remove\"\n @open=\"(file) => emit('pick', file)\"\n />\n\n <footer class=\"wx-media__status\">\n <span>{{ t('manager.status-files', { count: page?.stats.files ?? 0 }) }}</span>\n <span v-if=\"selected.length > 0\">\n {{ t('manager.status-selected', { count: selected.length }) }}\n </span>\n <span>{{ t('manager.status-size', { size: readable(page?.stats.size ?? 0) }) }}</span>\n </footer>\n\n <wx-pagination\n v-if=\"page && page.meta.last_page > 1\"\n :page=\"page.meta.current_page\"\n :per-page=\"page.meta.per_page\"\n :total=\"page.meta.total\"\n :last-page=\"page.meta.last_page\"\n :disabled=\"busy\"\n size=\"sm\"\n @change=\"({ page: to }) => loadFiles(to)\"\n />\n </section>\n\n <!-- On a narrow screen the folders are a drawer: a tree beside a grid leaves room for\n neither, and an editor on a phone is looking for one thing at a time. -->\n <wx-drawer v-model:open=\"foldersOpen\" side=\"left\" :title=\"t('manager.folders')\" :size=\"280\">\n <div class=\"wx-media__drawer\">\n <directory-tree\n v-model:selected=\"current\"\n :directories=\"directories\"\n @move=\"moveFolder\"\n @update:selected=\"foldersOpen = false\"\n />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </div>\n </wx-drawer>\n </div>\n</template>\n\n<style>\n.wx-media {\n display: grid;\n grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);\n gap: var(--wx-space-16);\n min-height: 0;\n height: 100%;\n}\n\n.wx-media--compact {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--wx-space-8);\n}\n\n.wx-media__folders {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-8);\n min-height: 0;\n border-right: 1px solid var(--wx-color-border);\n padding-right: var(--wx-space-12);\n}\n\n/* The drawer hands its content the edge of the screen; the tree and its actions want air. */\n.wx-media__drawer {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n padding-block: var(--wx-space-4);\n}\n\n.wx-media__status {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n border-top: 1px solid var(--wx-color-border);\n padding-top: var(--wx-space-6);\n}\n\n.wx-media__files {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n min-height: 0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxDialog } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\nimport type { MediaFile, MediaKind } from './types'\n\n/**\n * The library as a dialog, for a form that needs a picture.\n *\n * The same manager, not a second one: a picker that browses differently from the manager is a\n * second thing to learn and a second thing to keep working.\n */\nwithDefaults(\n defineProps<{\n accept?: MediaKind | null\n title?: string\n /** Opened to manage the library rather than to choose from it. */\n manage?: boolean\n }>(),\n { accept: 'image', title: undefined, manage: false },\n)\n\nconst { open, resolve } = useModal<MediaFile>()\n\nconst t = useTranslate('webx-media')\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title ?? t('module.title')\" :width=\"960\">\n <div class=\"wx-media-picker\">\n <media-manager\n :picking=\"!manage\"\n :accept=\"manage ? null : accept\"\n @pick=\"(file) => resolve(file)\"\n />\n </div>\n </wx-dialog>\n</template>\n\n<style>\n.wx-media-picker {\n height: min(70vh, 560px);\n min-height: 0;\n}\n</style>\n","import { createModal } from '@webx-ui/core'\nimport MediaPicker from './MediaPicker.vue'\nimport type { MediaFile, MediaKind } from './types'\n\nexport interface MediaPickerOptions {\n /** Narrow what can be chosen; `null` offers everything. */\n accept?: MediaKind | null\n title?: string\n}\n\n/**\n * The library, opened from code, resolving with the file that was chosen.\n *\n * A form does not need to own a dialog for this — `const file = await openMediaPicker()` is the\n * whole integration, and `undefined` means the person closed it.\n */\nexport const openMediaPicker = createModal<MediaFile, MediaPickerOptions>(MediaPicker)\n\n/**\n * The whole library as a dialog, from anywhere.\n *\n * The same manager, the same folders, the same uploads — for a screen that needs to put files in\n * order without navigating away from what it was doing. It resolves when a file is opened, and\n * with `undefined` when the dialog is simply closed.\n */\nexport const openMediaLibrary = createModal<MediaFile, Omit<MediaPickerOptions, 'accept'>>(\n MediaPicker,\n { props: { manage: true } },\n)\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n WxAction,\n WxActions,\n WxButton,\n WxImage,\n WxInput,\n WxPopconfirm,\n WxPopover,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport { createMediaApi } from './api'\nimport { openMediaPicker } from './openMediaPicker'\nimport { useMediaMessages } from './i18n'\nimport type { MediaAspect, MediaKind, MediaValue } from './types'\n\n/**\n * A picture on a form, with the words this entity uses for it.\n *\n * Two states and nothing in between: an empty frame that opens the library, and the picture\n * with what can be done to it. `alt` and `title` live here rather than on the file, because one\n * picture used by two articles needs two captions — and they are localized for the same reason\n * everything else on a form is.\n *\n * What the field stores is the key and those words. The address is worked out when the page is\n * drawn, so moving the library to another disk changes nothing written before.\n */\nconst props = withDefaults(\n defineProps<{\n label?: string\n accept?: MediaKind | null\n /** Off for a decorative picture, where a caption is noise. */\n captions?: boolean\n /**\n * Shape of the frame: one of the presets, any `width / height`, or `null` for a fixed\n * height instead.\n *\n * A field that holds a cover and a field that holds an avatar are not the same shape, and\n * the frame should say which before anything is in it.\n */\n aspect?: MediaAspect | null\n /** Height of the frame when there is no aspect — a number in pixels, or any CSS length. */\n height?: number | string\n disabled?: boolean\n }>(),\n {\n label: undefined,\n accept: 'image',\n captions: true,\n aspect: '16/9',\n height: 220,\n disabled: false,\n },\n)\n\nconst value = defineModel<MediaValue | null>({ default: null })\n\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\nconst api = createMediaApi(useAdmin())\n\n/** The presets, spelled the way CSS wants them. Anything else is passed through as given. */\nconst RATIOS: Record<string, string> = {\n '16/9': '16 / 9',\n '4/3': '4 / 3',\n '1/1': '1 / 1',\n}\n\nconst editing = ref(false)\n\n/*\n * What a record stores is the key. The address beside it is only there when the picture was\n * chosen in this session, so a form opened on something saved last week has a key and nothing\n * to draw — and the library is the only thing that knows where that key currently lives.\n */\nconst resolved = ref<Record<string, string>>({})\n\nwatch(\n () => value.value?.path,\n async (path) => {\n if (!path || value.value?.url || resolved.value[path]) return\n\n const file = await api.fileByPath(path)\n\n if (file) resolved.value = { ...resolved.value, [path]: file.url }\n },\n { immediate: true },\n)\n\nconst preview = computed(() => {\n const current = value.value\n\n if (!current) return null\n\n return current.url ?? resolved.value[current.path] ?? null\n})\n\nconst frameStyle = computed(() =>\n props.aspect\n ? { aspectRatio: RATIOS[props.aspect] ?? props.aspect }\n : { height: typeof props.height === 'number' ? `${props.height}px` : props.height },\n)\n\n/**\n * `alt` and `title` are edited through the field, so they have to be readable as a record even\n * when what was stored is the plain string a column held before the site had a second language.\n */\nfunction words(key: 'alt' | 'title'): LocalizedValue | string {\n return value.value?.[key] ?? {}\n}\n\nfunction setWords(key: 'alt' | 'title', next: unknown): void {\n if (!value.value) return\n\n value.value = { ...value.value, [key]: next as LocalizedValue }\n}\n\nasync function choose(): Promise<void> {\n if (props.disabled) return\n\n const file = await openMediaPicker({ accept: props.accept })\n\n if (!file) return\n\n // The captions belong to the entity, so picking a different picture keeps them: usually the\n // words are still right and retyping them in three languages is the punishment for a swap.\n value.value = { ...value.value, path: file.path, url: file.url }\n}\n\n/** Clears the field. The file stays in the library — it is very likely used somewhere else. */\nfunction clear(): void {\n value.value = null\n editing.value = false\n}\n</script>\n\n<template>\n <div class=\"wx-media-field\">\n <span v-if=\"label\" class=\"wx-media-field__label\">{{ label }}</span>\n\n <div v-if=\"!value\" class=\"wx-media-field__frame is-empty\" :style=\"frameStyle\">\n <button type=\"button\" class=\"wx-media-field__pick\" :disabled=\"disabled\" @click=\"choose\">\n {{ t('field.select') }}\n </button>\n </div>\n\n <div v-else class=\"wx-media-field__frame\" :style=\"frameStyle\">\n <!--\n The picture and the buttons are wrapped in elements of our own rather than styled\n through their class: a component of the design system carries scoped rules, and a scoped\n rule is a class plus an attribute, so a single class from outside loses to it — silently,\n and only in a browser.\n\n The picture is also the way to swap it. A button labelled \"replace\" inside the captions\n popover is a second place to look for something the picture itself already offers.\n -->\n <button\n type=\"button\"\n class=\"wx-media-field__picture\"\n :disabled=\"disabled\"\n :title=\"t('field.replace')\"\n @click=\"choose\"\n >\n <wx-image v-if=\"preview\" :src=\"preview\" fit=\"contain\" width=\"100%\" height=\"100%\" />\n <span v-else class=\"wx-media-field__missing\">{{ t('field.no-preview') }}</span>\n </button>\n\n <div v-if=\"!disabled\" class=\"wx-media-field__bar\">\n <wx-actions size=\"sm\">\n <!--\n The captions are asked for where the picture is, not in a dialog over the whole form:\n what they describe has to stay in sight while they are being written.\n -->\n <wx-popover\n v-if=\"captions\"\n v-model:open=\"editing\"\n :title=\"t('field.captions')\"\n :width=\"360\"\n side=\"top\"\n teleport\n >\n <template #trigger>\n <wx-action type=\"edit\" :title=\"t('field.edit')\" />\n </template>\n\n <div class=\"wx-media-field__captions\">\n <div class=\"wx-media-field__caption\">\n <label class=\"wx-media-field__caption-label\" for=\"wx-media-alt\">alt</label>\n <wx-input\n id=\"wx-media-alt\"\n localized\n :model-value=\"words('alt')\"\n :placeholder=\"t('field.alt-hint')\"\n @update:model-value=\"(next) => setWords('alt', next)\"\n />\n </div>\n\n <div class=\"wx-media-field__caption\">\n <label class=\"wx-media-field__caption-label\" for=\"wx-media-title\">title</label>\n <wx-input\n id=\"wx-media-title\"\n localized\n :model-value=\"words('title')\"\n :placeholder=\"t('field.title-hint')\"\n @update:model-value=\"(next) => setWords('title', next)\"\n />\n </div>\n\n <div class=\"wx-media-field__captions-footer\">\n <wx-button size=\"sm\" type=\"primary\" @click=\"editing = false\">\n {{ t('manager.save') }}\n </wx-button>\n </div>\n </div>\n </wx-popover>\n\n <!--\n Clearing the field is not deleting the file: the same picture is very likely used by\n another record, and this is the one place somebody would expect otherwise.\n -->\n <wx-popconfirm\n :title=\"t('field.clear-title')\"\n :description=\"t('field.clear-text')\"\n :confirm-text=\"t('field.clear-confirm')\"\n :cancel-text=\"t('manager.cancel')\"\n confirm-type=\"danger\"\n side=\"top\"\n @confirm=\"clear\"\n >\n <template #trigger>\n <wx-action type=\"remove\" :title=\"t('field.clear-confirm')\" />\n </template>\n </wx-popconfirm>\n </wx-actions>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n.wx-media-field {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-6);\n}\n\n.wx-media-field__label {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-text-muted);\n}\n\n.wx-media-field__frame {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n padding: var(--wx-space-8);\n border: 1px solid var(--wx-border-default);\n border-radius: var(--wx-radius-md);\n background: var(--wx-bg-surface);\n overflow: hidden;\n}\n\n.wx-media-field__frame.is-empty {\n border-style: dashed;\n padding: 0;\n}\n\n.wx-media-field__pick {\n width: 100%;\n height: 100%;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--wx-text-placeholder);\n font-family: inherit;\n font-size: var(--wx-font-size-md);\n cursor: pointer;\n transition: color var(--wx-duration-fast) var(--wx-easing-standard);\n}\n\n.wx-media-field__pick:hover:not(:disabled) {\n color: var(--wx-text-muted);\n}\n\n.wx-media-field__pick:disabled {\n cursor: not-allowed;\n}\n\n.wx-media-field__picture {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n min-width: 0;\n padding: 0;\n border: none;\n /* Inscribed in the frame, so rounded a step less than it — concentric, not parallel. */\n border-radius: var(--wx-radius-xs);\n overflow: hidden;\n background: transparent;\n cursor: pointer;\n}\n\n.wx-media-field__picture:disabled {\n cursor: not-allowed;\n}\n\n.wx-media-field__missing {\n color: var(--wx-text-placeholder);\n font-size: var(--wx-font-size-sm);\n}\n\n/* On the picture, out of the way of it: the frame is the preview, not a toolbar. */\n.wx-media-field__bar {\n position: absolute;\n /* Clear of the frame's inner edge rather than sitting on it. */\n bottom: var(--wx-space-16);\n left: 50%;\n transform: translateX(-50%);\n padding: var(--wx-space-4);\n border-radius: var(--wx-radius-control);\n background: var(--wx-bg-surface);\n box-shadow: var(--wx-shadow-sm);\n}\n\n/* A label belongs to the field under it, so it sits closer to that than to the next one. */\n.wx-media-field__captions {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n width: 100%;\n}\n\n.wx-media-field__caption {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-2);\n}\n\n.wx-media-field__caption-label {\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-xs);\n}\n\n.wx-media-field__captions-footer {\n display: flex;\n justify-content: flex-end;\n gap: var(--wx-space-8);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { WxCard } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\n\n/**\n * The library as a page of the panel.\n *\n * On its own, the manager sits straight on the panel's background, which reads as something\n * that has not finished loading. In a dialog it already has walls; here it needs its own.\n */\n</script>\n\n<template>\n <wx-card class=\"wx-media-page\" padding=\"md\">\n <media-manager />\n </wx-card>\n</template>\n\n<style>\n.wx-media-page {\n height: 100%;\n min-height: 0;\n}\n\n/* The card is the page: its body is what scrolls, not the page behind it. */\n.wx-media-page > .wx-card__body {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n</style>\n","import type { AdminModule } from '@webx-ui/module-admin'\nimport MediaField from './MediaField.vue'\nimport MediaPage from './MediaPage.vue'\nimport { mediaMessages } from './messages'\n\nexport interface MediaOptions {\n /** Where the section lives inside the panel. */\n path?: string\n}\n\n/**\n * The file manager as a section of the panel.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed, and stays out of the way when\n * only one is.\n */\nexport function media(options: MediaOptions = {}): AdminModule {\n const path = options.path ?? '/media'\n\n return {\n id: 'media',\n path,\n routes: [\n {\n path,\n name: 'webx.media',\n component: MediaPage,\n },\n ],\n // What a screen means by `wx-media`: the field over the library. The server registers\n // the same name for the value it stores.\n types: {\n 'wx-media': { component: MediaField, kind: 'field' },\n },\n }\n}\n\nexport { mediaMessages }\nexport { createMediaApi, type MediaApi } from './api'\nexport { openMediaLibrary, openMediaPicker, type MediaPickerOptions } from './openMediaPicker'\nexport { default as WxMediaManager } from './MediaManager.vue'\nexport { default as WxMediaPage } from './MediaPage.vue'\nexport { default as WxMediaPicker } from './MediaPicker.vue'\nexport { default as WxMediaField } from './MediaField.vue'\nexport type {\n DirectoryNotEmpty,\n EditOperations,\n FileQuery,\n MediaDirectory,\n MediaFile,\n MediaAspect,\n MediaKind,\n MediaPage,\n MediaValue,\n} from './types'\n"],"names":["createMediaApi","admin","base","data","body","parentId","title","id","force","query","path","directoryId","files","onProgress","file","resolve","reject","request","token","csrfToken","event","answer","name","ids","operations","width","height","fit","url","cookie","part","props","__props","selected","_useModel","emit","__emit","t","useTranslate","nodes","ref","expanded","watch","directories","label","keysOf","directory","onDrop","parent","_createBlock","_unref","WxTree","$event","empty","computed","copied","toast","copyFailed","area","done","WxSelectionArea","_withCtx","isSelected","_createElementBlock","_Fragment","_renderList","WxFileCard","_directive_wx_select","WxEmpty","search","type","sort","types","kind","sorts","typeLabel","one","sortLabel","_openBlock","_hoisted_1","WxAction","_createVNode","WxInput","WxDropdown","option","WxDropdownItem","_createTextVNode","_toDisplayString","WxActions","open","dismiss","useModal","options","flatten","target","WxDialog","WxSpace","WxButton","_cache","WxSelect","input","useTemplateRef","onMounted","nextTick","submit","value","_createElementVNode","mediaMessages","seeded","useMediaMessages","i18n","useAdmin","api","editorLabels","root","useElementWidth","compact","current","page","busy","foldersOpen","picker","rows","canManage","canUpload","folder","find","load","loadFiles","debounce","narrow","to","choose","upload","chosen","stored","duplicates","error","messageOf","askName","createModal","NameDialog","createFolder","renameFolder","deleteFolder","counts","countsOf","confirm","moveFolder","askWhereTo","MoveDialog","moveSelected","removeSelected","rename","remove","edit","result","openImageEditor","walk","node","deeper","readable","bytes","units","size","unit","run","wait","timer","_normalizeClass","DirectoryTree","_hoisted_2","MediaToolbar","FileGrid","_hoisted_4","_hoisted_5","WxPagination","WxDrawer","_hoisted_6","MediaManager","openMediaPicker","MediaPicker","openMediaLibrary","RATIOS","editing","resolved","preview","frameStyle","words","key","setWords","next","clear","WxImage","WxPopover","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","WxPopconfirm","_hoisted_3","WxCard","media","MediaPage","MediaField"],"mappings":"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"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-media-tree{min-width:0;overflow:auto}.wx-media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--wx-space-12);align-content:start;min-height:0;overflow:auto;padding:var(--wx-space-2);user-select:none}.wx-media--compact .wx-media-grid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--wx-space-8)}.wx-media-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-6)}.wx-media-toolbar__search{flex:1 1 180px;min-width:120px}.wx-media-toolbar__count{font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);margin-inline-start:auto}.wx-media{display:grid;grid-template-columns:minmax(180px,240px) minmax(0,1fr);gap:var(--wx-space-16);min-height:0;height:100%}.wx-media--compact{grid-template-columns:minmax(0,1fr);gap:var(--wx-space-8)}.wx-media__folders{display:flex;flex-direction:column;gap:var(--wx-space-8);min-height:0;border-right:1px solid var(--wx-color-border);padding-right:var(--wx-space-12)}.wx-media__drawer{display:flex;flex-direction:column;gap:var(--wx-space-10);padding-block:var(--wx-space-4)}.wx-media__status{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);border-top:1px solid var(--wx-color-border);padding-top:var(--wx-space-6)}.wx-media__files{display:flex;flex-direction:column;gap:var(--wx-space-10);min-height:0}.wx-media-
|
|
1
|
+
.wx-media-tree{min-width:0;overflow:auto}.wx-media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--wx-space-12);align-content:start;min-height:0;overflow:auto;padding:var(--wx-space-2);user-select:none}.wx-media--compact .wx-media-grid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--wx-space-8)}.wx-media-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-6)}.wx-media-toolbar__search{flex:1 1 180px;min-width:120px}.wx-media-toolbar__count{font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);margin-inline-start:auto}.wx-media{display:grid;grid-template-columns:minmax(180px,240px) minmax(0,1fr);gap:var(--wx-space-16);min-height:0;height:100%}.wx-media--compact{grid-template-columns:minmax(0,1fr);gap:var(--wx-space-8)}.wx-media__folders{display:flex;flex-direction:column;gap:var(--wx-space-8);min-height:0;border-right:1px solid var(--wx-color-border);padding-right:var(--wx-space-12)}.wx-media__drawer{display:flex;flex-direction:column;gap:var(--wx-space-10);padding-block:var(--wx-space-4)}.wx-media__status{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);border-top:1px solid var(--wx-color-border);padding-top:var(--wx-space-6)}.wx-media__files{display:flex;flex-direction:column;gap:var(--wx-space-10);min-height:0}.wx-media-picker{height:min(70vh,560px);min-height:0}.wx-media-field{display:flex;flex-direction:column;gap:var(--wx-space-6)}.wx-media-field__label{font-size:var(--wx-font-size-sm);color:var(--wx-text-muted)}.wx-media-field__frame{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:var(--wx-space-8);border:1px solid var(--wx-border-default);border-radius:var(--wx-radius-md);background:var(--wx-bg-surface);overflow:hidden}.wx-media-field__frame.is-empty{border-style:dashed;padding:0}.wx-media-field__pick{width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--wx-text-placeholder);font-family:inherit;font-size:var(--wx-font-size-md);cursor:pointer;transition:color var(--wx-duration-fast) var(--wx-easing-standard)}.wx-media-field__pick:hover:not(:disabled){color:var(--wx-text-muted)}.wx-media-field__pick:disabled{cursor:not-allowed}.wx-media-field__picture{display:flex;align-items:center;justify-content:center;width:100%;height:100%;min-width:0;padding:0;border:none;border-radius:var(--wx-radius-xs);overflow:hidden;background:transparent;cursor:pointer}.wx-media-field__picture:disabled{cursor:not-allowed}.wx-media-field__missing{color:var(--wx-text-placeholder);font-size:var(--wx-font-size-sm)}.wx-media-field__bar{position:absolute;bottom:var(--wx-space-16);left:50%;transform:translate(-50%);padding:var(--wx-space-4);border-radius:var(--wx-radius-control);background:var(--wx-bg-surface);box-shadow:var(--wx-shadow-sm)}.wx-media-field__captions{display:flex;flex-direction:column;gap:var(--wx-space-12);width:100%}.wx-media-field__caption{display:flex;flex-direction:column;gap:var(--wx-space-2)}.wx-media-field__caption-label{color:var(--wx-text-muted);font-size:var(--wx-font-size-xs)}.wx-media-field__captions-footer{display:flex;justify-content:flex-end;gap:var(--wx-space-8)}.wx-media-page{height:100%;min-height:0}.wx-media-page>.wx-card__body{height:100%;min-height:0;display:flex;flex-direction:column}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The file manager of a WebX UI admin panel: folders, files, uploads and an image editor. Pairs with webx-ui/module-media on the server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
"vue-router": "^4.5.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@webx-ui/module-admin": "^0.
|
|
49
|
-
"@webx-ui/core": "^0.
|
|
48
|
+
"@webx-ui/module-admin": "^0.4.0",
|
|
49
|
+
"@webx-ui/core": "^0.18.0",
|
|
50
|
+
"@webx-ui/schema": "^0.1.1"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^24.10.1",
|