mbkbucket 3.2.0 → 3.3.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/package.json +1 -1
- package/public/mbkbucket-helper-demo.html +1177 -0
- package/public/mbkbucket-helper.css +1235 -0
- package/public/mbkbucket-helper.js +1550 -0
- package/src/app.js +7 -0
- package/src/services/s3.service.js +3 -2
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mbkbucket-helper.js v1.2.0
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Drop-in frontend helper utilities for mbkbucket.
|
|
5
|
+
* Include this script (and mbkbucket-helper.css) in any HTML page.
|
|
6
|
+
*
|
|
7
|
+
* Exposes: window.MBKBucket
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Gallery Browser Modal (MBKBucket.openGallery) with multi-select & in-modal upload
|
|
11
|
+
* - File Picker Input (MBKBucket.createPicker / data-mbk-picker) with live preview binding
|
|
12
|
+
* - Upload Widget (MBKBucket.createUploader / data-mbk-uploader)
|
|
13
|
+
* - URL Builders (MBKBucket.url.*)
|
|
14
|
+
* - API Helpers (MBKBucket.api.*)
|
|
15
|
+
* - Toast Notifications (MBKBucket.toast.*)
|
|
16
|
+
*
|
|
17
|
+
* Auth: uses existing cookie session (same origin).
|
|
18
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
*/
|
|
20
|
+
(function (global) {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Config & Helpers
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const BASE = '/mbkbucket';
|
|
28
|
+
const MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10 MB → multipart upload
|
|
29
|
+
const CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB chunks
|
|
30
|
+
|
|
31
|
+
function esc(str) {
|
|
32
|
+
return String(str == null ? '' : str)
|
|
33
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
34
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function baseName(key) {
|
|
38
|
+
const s = String(key || '').replace(/\/+$/, '');
|
|
39
|
+
const i = s.lastIndexOf('/');
|
|
40
|
+
return i === -1 ? s : s.slice(i + 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ext(fileName) {
|
|
44
|
+
const n = baseName(fileName);
|
|
45
|
+
const d = n.lastIndexOf('.');
|
|
46
|
+
return d > -1 ? n.slice(d + 1).toLowerCase() : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatBytes(bytes) {
|
|
50
|
+
if (!bytes || bytes < 0) return '';
|
|
51
|
+
if (bytes < 1024) return bytes + ' B';
|
|
52
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
53
|
+
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
|
54
|
+
return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildUrl(path, params) {
|
|
58
|
+
const u = new URL(path, location.origin);
|
|
59
|
+
if (params) {
|
|
60
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
61
|
+
if (v !== undefined && v !== null && v !== '') {
|
|
62
|
+
u.searchParams.set(k, v);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return u.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function apiFetch(url, options) {
|
|
70
|
+
return fetch(url, { credentials: 'include', ...options })
|
|
71
|
+
.then(async r => {
|
|
72
|
+
const json = await r.json().catch(() => ({ success: false, error: r.statusText }));
|
|
73
|
+
if (!r.ok) throw Object.assign(new Error(json.error || r.statusText), { status: r.status, body: json });
|
|
74
|
+
return json;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const IMAGE_EXTS = new Set(['jpg','jpeg','png','gif','webp','svg','bmp','ico','avif']);
|
|
79
|
+
const VIDEO_EXTS = new Set(['mp4','webm','ogg','mov','avi']);
|
|
80
|
+
const AUDIO_EXTS = new Set(['mp3','wav','flac','aac','m4a','ogg']);
|
|
81
|
+
const DOC_EXTS = new Set(['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md','csv','rtf']);
|
|
82
|
+
const CODE_EXTS = new Set(['js','ts','jsx','tsx','html','htm','css','scss','php','py','java','cpp','c','h','cs','rb','go','rs','sql','sh','bat','ps1','yaml','yml','toml','ini','conf','json','xml','log']);
|
|
83
|
+
|
|
84
|
+
const TYPE_MAP = {
|
|
85
|
+
pdf: { label:'PDF', color:'#dc2626', bg:'rgba(220, 38, 38, 0.1)' },
|
|
86
|
+
doc: { label:'DOC', color:'#066fd1', bg:'rgba(6, 111, 209, 0.1)' },
|
|
87
|
+
docx: { label:'DOC', color:'#066fd1', bg:'rgba(6, 111, 209, 0.1)' },
|
|
88
|
+
xls: { label:'XLS', color:'#16a34a', bg:'rgba(22, 163, 74, 0.12)' },
|
|
89
|
+
xlsx: { label:'XLS', color:'#16a34a', bg:'rgba(22, 163, 74, 0.12)' },
|
|
90
|
+
csv: { label:'CSV', color:'#16a34a', bg:'rgba(22, 163, 74, 0.12)' },
|
|
91
|
+
ppt: { label:'PPT', color:'#d97706', bg:'rgba(217, 119, 6, 0.12)' },
|
|
92
|
+
pptx: { label:'PPT', color:'#d97706', bg:'rgba(217, 119, 6, 0.12)' },
|
|
93
|
+
txt: { label:'TXT', color:'#667c79', bg:'rgba(102, 124, 121, 0.12)' },
|
|
94
|
+
md: { label:'MD', color:'#7c3aed', bg:'rgba(124, 58, 237, 0.1)' },
|
|
95
|
+
js: { label:'JS', color:'#0f766e', bg:'rgba(15, 118, 110, 0.12)' },
|
|
96
|
+
ts: { label:'TS', color:'#0f766e', bg:'rgba(15, 118, 110, 0.12)' },
|
|
97
|
+
json: { label:'JSON', color:'#0f766e', bg:'rgba(15, 118, 110, 0.12)' },
|
|
98
|
+
html: { label:'HTML', color:'#ea580c', bg:'rgba(234, 88, 12, 0.12)' },
|
|
99
|
+
css: { label:'CSS', color:'#066fd1', bg:'rgba(6, 111, 209, 0.1)' },
|
|
100
|
+
py: { label:'PY', color:'#0f766e', bg:'rgba(15, 118, 110, 0.12)' },
|
|
101
|
+
zip: { label:'ZIP', color:'#667c79', bg:'rgba(102, 124, 121, 0.12)' },
|
|
102
|
+
tar: { label:'TAR', color:'#667c79', bg:'rgba(102, 124, 121, 0.12)' },
|
|
103
|
+
gz: { label:'GZ', color:'#667c79', bg:'rgba(102, 124, 121, 0.12)' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function fileTypeInfo(fileName) {
|
|
107
|
+
const e = ext(fileName);
|
|
108
|
+
if (IMAGE_EXTS.has(e)) return { label: 'IMG', color: '#d97706', bg: 'rgba(217, 119, 6, 0.12)' };
|
|
109
|
+
if (VIDEO_EXTS.has(e)) return { label: 'VID', color: '#1f8f84', bg: 'rgba(31, 143, 132, 0.12)' };
|
|
110
|
+
if (AUDIO_EXTS.has(e)) return { label: 'AUD', color: '#7c3aed', bg: 'rgba(124, 58, 237, 0.1)' };
|
|
111
|
+
return TYPE_MAP[e] || { label: (e.toUpperCase().slice(0,4) || 'FILE'), color: '#667c79', bg: 'rgba(102, 124, 121, 0.12)' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// URL Builders
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const url = {
|
|
119
|
+
view(key, bucket) {
|
|
120
|
+
return key ? buildUrl(`${BASE}/view/${encodeURIComponent(key)}`, { bucket: bucket || '' }) : '';
|
|
121
|
+
},
|
|
122
|
+
public(key, bucket) {
|
|
123
|
+
return key ? buildUrl(`${BASE}/p_view/${encodeURIComponent(key)}`, { bucket: bucket || '' }) : '';
|
|
124
|
+
},
|
|
125
|
+
download(key, bucket) {
|
|
126
|
+
return key ? buildUrl(`${BASE}/download/${encodeURIComponent(key)}`, { bucket: bucket || '' }) : '';
|
|
127
|
+
},
|
|
128
|
+
player(key, bucket) {
|
|
129
|
+
return key ? buildUrl(`${BASE}/player/${encodeURIComponent(key)}`, { bucket: bucket || '' }) : '';
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// SVG Icons
|
|
135
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
const ICONS = {
|
|
138
|
+
bucket: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3C8 3 4 5 4 8v1c0 3 2 5 8 6 6-1 8-3 8-6V8c0-3-4-5-8-5z"/><path d="M4 9c0 3 2 5 8 6 6-1 8-3 8-6"/><path d="M4 13c0 3 2 5 8 6 6-1 8-3 8-6"/></svg>`,
|
|
139
|
+
folder: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z"/></svg>`,
|
|
140
|
+
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>`,
|
|
141
|
+
image: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`,
|
|
142
|
+
video: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="2" y="7" width="15" height="10" rx="2"/><path d="M22 8l-5 4 5 4V8z"/></svg>`,
|
|
143
|
+
audio: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
|
|
144
|
+
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
|
145
|
+
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
146
|
+
grid: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>`,
|
|
147
|
+
list: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
|
|
148
|
+
upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>`,
|
|
149
|
+
plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
|
150
|
+
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
|
151
|
+
warn: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
152
|
+
info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
153
|
+
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>`,
|
|
154
|
+
browse: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function icon(name, size) {
|
|
158
|
+
const s = size || 16;
|
|
159
|
+
const svg = ICONS[name] || ICONS.file;
|
|
160
|
+
return svg.replace('<svg', `<svg width="${s}" height="${s}"`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
164
|
+
// Toast Notifications
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
let _toastContainer = null;
|
|
168
|
+
|
|
169
|
+
function getToastContainer() {
|
|
170
|
+
if (!_toastContainer) {
|
|
171
|
+
_toastContainer = document.createElement('div');
|
|
172
|
+
_toastContainer.className = 'mbkh-toast-container';
|
|
173
|
+
document.body.appendChild(_toastContainer);
|
|
174
|
+
}
|
|
175
|
+
return _toastContainer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function showToast(message, type, duration) {
|
|
179
|
+
type = type || 'info';
|
|
180
|
+
duration = duration || 3500;
|
|
181
|
+
const iconMap = { success: 'check', error: 'warn', warning: 'warn', info: 'info' };
|
|
182
|
+
const colorMap = { success: '#16a34a', error: '#dc2626', warning: '#d97706', info: '#0f766e' };
|
|
183
|
+
|
|
184
|
+
const el = document.createElement('div');
|
|
185
|
+
el.className = `mbkh-toast mbkh-toast-${type}`;
|
|
186
|
+
el.innerHTML = `
|
|
187
|
+
<span class="mbkh-toast-icon" style="color:${colorMap[type]}">${icon(iconMap[type] || 'info', 16)}</span>
|
|
188
|
+
<span class="mbkh-toast-msg">${esc(message)}</span>`;
|
|
189
|
+
|
|
190
|
+
const container = getToastContainer();
|
|
191
|
+
container.appendChild(el);
|
|
192
|
+
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
el.classList.add('mbkh-toast-out');
|
|
195
|
+
el.addEventListener('animationend', () => el.remove(), { once: true });
|
|
196
|
+
}, duration);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const toast = {
|
|
200
|
+
success: (msg, dur) => showToast(msg, 'success', dur),
|
|
201
|
+
error: (msg, dur) => showToast(msg, 'error', dur),
|
|
202
|
+
warning: (msg, dur) => showToast(msg, 'warning', dur),
|
|
203
|
+
info: (msg, dur) => showToast(msg, 'info', dur),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
207
|
+
// API Helpers
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
const api = {
|
|
211
|
+
listFiles(prefix, opts) {
|
|
212
|
+
opts = opts || {};
|
|
213
|
+
return apiFetch(buildUrl(`${BASE}/api/files`, {
|
|
214
|
+
prefix: prefix || '',
|
|
215
|
+
bucket: opts.bucket || '',
|
|
216
|
+
search: opts.search || '',
|
|
217
|
+
token: opts.token || '',
|
|
218
|
+
recursive: opts.recursive ? 'true' : 'false',
|
|
219
|
+
}));
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
uploadSingle(prefix, file, opts) {
|
|
223
|
+
opts = opts || {};
|
|
224
|
+
const fd = new FormData();
|
|
225
|
+
fd.append('file', file);
|
|
226
|
+
fd.append('prefix', prefix || '');
|
|
227
|
+
if (opts.bucket) fd.append('bucket', opts.bucket);
|
|
228
|
+
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const xhr = new XMLHttpRequest();
|
|
231
|
+
xhr.open('POST', `${BASE}/upload`);
|
|
232
|
+
xhr.withCredentials = true;
|
|
233
|
+
if (opts.onProgress) {
|
|
234
|
+
xhr.upload.onprogress = e => {
|
|
235
|
+
if (e.lengthComputable) opts.onProgress(e.loaded / e.total);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
xhr.onload = () => {
|
|
239
|
+
try {
|
|
240
|
+
const json = JSON.parse(xhr.responseText);
|
|
241
|
+
if (xhr.status >= 200 && xhr.status < 300 && json.success) resolve(json);
|
|
242
|
+
else reject(new Error(json.error || xhr.statusText));
|
|
243
|
+
} catch(e) { reject(new Error('Upload failed')); }
|
|
244
|
+
};
|
|
245
|
+
xhr.onerror = () => reject(new Error('Network error'));
|
|
246
|
+
xhr.send(fd);
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
async uploadMultipart(prefix, file, opts) {
|
|
251
|
+
opts = opts || {};
|
|
252
|
+
const bucket = opts.bucket || '';
|
|
253
|
+
|
|
254
|
+
const init = await apiFetch(`${BASE}/upload-init`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
credentials: 'include',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({ fileName: file.name, prefix, contentType: file.type || 'application/octet-stream', ...(bucket ? { bucket } : {}) }),
|
|
259
|
+
});
|
|
260
|
+
const { uploadId, key } = init;
|
|
261
|
+
|
|
262
|
+
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
263
|
+
const parts = [];
|
|
264
|
+
let uploadedBytes = 0;
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
267
|
+
if (opts.signal && opts.signal.aborted) {
|
|
268
|
+
await apiFetch(`${BASE}/upload-abort`, {
|
|
269
|
+
method: 'POST', credentials: 'include',
|
|
270
|
+
headers: { 'Content-Type': 'application/json' },
|
|
271
|
+
body: JSON.stringify({ uploadId, key, ...(bucket ? { bucket } : {}) }),
|
|
272
|
+
}).catch(() => {});
|
|
273
|
+
throw new Error('Upload aborted');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const start = i * CHUNK_SIZE;
|
|
277
|
+
const end = Math.min(start + CHUNK_SIZE, file.size);
|
|
278
|
+
const chunk = file.slice(start, end);
|
|
279
|
+
|
|
280
|
+
const fd = new FormData();
|
|
281
|
+
fd.append('chunk', chunk, file.name);
|
|
282
|
+
fd.append('uploadId', uploadId);
|
|
283
|
+
fd.append('key', key);
|
|
284
|
+
fd.append('partNumber', String(i + 1));
|
|
285
|
+
if (bucket) fd.append('bucket', bucket);
|
|
286
|
+
|
|
287
|
+
const result = await new Promise((resolve, reject) => {
|
|
288
|
+
const xhr = new XMLHttpRequest();
|
|
289
|
+
xhr.open('POST', `${BASE}/upload-chunk`);
|
|
290
|
+
xhr.withCredentials = true;
|
|
291
|
+
xhr.upload.onprogress = e => {
|
|
292
|
+
if (e.lengthComputable && opts.onProgress) {
|
|
293
|
+
opts.onProgress((uploadedBytes + e.loaded) / file.size);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
xhr.onload = () => {
|
|
297
|
+
try {
|
|
298
|
+
const json = JSON.parse(xhr.responseText);
|
|
299
|
+
if (xhr.status >= 200 && xhr.status < 300 && json.success) resolve(json);
|
|
300
|
+
else reject(new Error(json.error || xhr.statusText));
|
|
301
|
+
} catch(e) { reject(new Error('Chunk upload failed')); }
|
|
302
|
+
};
|
|
303
|
+
xhr.onerror = () => reject(new Error('Network error'));
|
|
304
|
+
xhr.send(fd);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
uploadedBytes += (end - start);
|
|
308
|
+
parts.push({ partNumber: i + 1, ETag: result.ETag });
|
|
309
|
+
if (opts.onProgress) opts.onProgress(uploadedBytes / file.size);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return apiFetch(`${BASE}/upload-complete`, {
|
|
313
|
+
method: 'POST', credentials: 'include',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({ uploadId, key, parts, ...(bucket ? { bucket } : {}) }),
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
uploadFile(prefix, file, opts) {
|
|
320
|
+
if (file.size > MULTIPART_THRESHOLD) {
|
|
321
|
+
return this.uploadMultipart(prefix, file, opts);
|
|
322
|
+
}
|
|
323
|
+
return this.uploadSingle(prefix, file, opts);
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
getSignedUrl(key, opts) {
|
|
327
|
+
opts = opts || {};
|
|
328
|
+
return apiFetch(buildUrl(`${BASE}/api/signed-url`, {
|
|
329
|
+
key,
|
|
330
|
+
bucket: opts.bucket || '',
|
|
331
|
+
expiresIn: opts.expiresIn || 3600,
|
|
332
|
+
}));
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
337
|
+
// Gallery Modal (with In-Modal Upload & Multi-Select)
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
let _galleryOverlay = null;
|
|
341
|
+
let _galleryState = null;
|
|
342
|
+
let _galleryResolve = null;
|
|
343
|
+
|
|
344
|
+
function buildGalleryDom() {
|
|
345
|
+
const overlay = document.createElement('div');
|
|
346
|
+
overlay.className = 'mbkh-overlay';
|
|
347
|
+
overlay.setAttribute('role', 'dialog');
|
|
348
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
349
|
+
overlay.setAttribute('aria-label', 'Browse Bucket');
|
|
350
|
+
|
|
351
|
+
overlay.innerHTML = `
|
|
352
|
+
<div class="mbkh-modal" id="mbkh-modal">
|
|
353
|
+
<!-- Header -->
|
|
354
|
+
<div class="mbkh-modal-header">
|
|
355
|
+
<div class="mbkh-modal-logo">${icon('bucket', 14)}</div>
|
|
356
|
+
<div class="mbkh-modal-title" id="mbkh-modal-title">Browse Bucket</div>
|
|
357
|
+
<div class="mbkh-header-actions">
|
|
358
|
+
<button class="mbkh-btn-upload-header" id="mbkh-modal-upload-trigger">
|
|
359
|
+
${icon('plus', 12)} Upload
|
|
360
|
+
</button>
|
|
361
|
+
<input type="file" id="mbkh-modal-file-input" multiple style="display:none" />
|
|
362
|
+
<button class="mbkh-modal-close" id="mbkh-close-btn" aria-label="Close">${icon('close', 14)}</button>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<!-- Toolbar & Breadcrumb -->
|
|
367
|
+
<div class="mbkh-toolbar">
|
|
368
|
+
<nav class="mbkh-breadcrumb" id="mbkh-breadcrumb" aria-label="Path"></nav>
|
|
369
|
+
<div class="mbkh-toolbar-actions">
|
|
370
|
+
<div class="mbkh-search-wrap">
|
|
371
|
+
<span class="mbkh-search-icon">${icon('search', 13)}</span>
|
|
372
|
+
<input class="mbkh-search" id="mbkh-search" type="search" placeholder="Search in folder…" autocomplete="off" aria-label="Search files"/>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="mbkh-view-toggle">
|
|
375
|
+
<button class="mbkh-view-btn mbkh-active" id="mbkh-view-grid" title="Grid view" aria-pressed="true">${icon('grid', 13)}</button>
|
|
376
|
+
<button class="mbkh-view-btn" id="mbkh-view-list" title="List view" aria-pressed="false">${icon('list', 13)}</button>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<!-- Filter bar -->
|
|
382
|
+
<div class="mbkh-filter-bar" id="mbkh-filter-bar">
|
|
383
|
+
<button class="mbkh-filter-btn mbkh-active" data-filter="all">All</button>
|
|
384
|
+
<button class="mbkh-filter-btn" data-filter="images">Images</button>
|
|
385
|
+
<button class="mbkh-filter-btn" data-filter="docs">Documents</button>
|
|
386
|
+
<button class="mbkh-filter-btn" data-filter="media">Media</button>
|
|
387
|
+
<button class="mbkh-filter-btn" data-filter="code">Code</button>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<!-- In-Modal Upload Progress / Drop Banner -->
|
|
391
|
+
<div class="mbkh-modal-upload-banner" id="mbkh-modal-upload-banner" style="display:none">
|
|
392
|
+
<div class="mbkh-upload-banner-text" id="mbkh-upload-banner-text">Uploading…</div>
|
|
393
|
+
<div class="mbkh-upload-banner-bar"><div class="mbkh-upload-banner-fill" id="mbkh-upload-banner-fill"></div></div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Body -->
|
|
397
|
+
<div class="mbkh-modal-body" id="mbkh-modal-body">
|
|
398
|
+
<div class="mbkh-file-grid" id="mbkh-grid" role="listbox" aria-multiselectable="false" aria-label="Files"></div>
|
|
399
|
+
<div class="mbkh-state" id="mbkh-state" style="display:none"></div>
|
|
400
|
+
<div class="mbkh-drag-overlay" id="mbkh-drag-overlay" style="display:none">
|
|
401
|
+
<div class="mbkh-drag-box">
|
|
402
|
+
${icon('upload', 36)}
|
|
403
|
+
<div class="mbkh-drag-text">Drop files here to upload to this folder</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- Footer -->
|
|
409
|
+
<div class="mbkh-modal-footer">
|
|
410
|
+
<div class="mbkh-selected-wrap" id="mbkh-sel-wrap">
|
|
411
|
+
<span class="mbkh-selected-placeholder">Click a file to select</span>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="mbkh-footer-btns">
|
|
414
|
+
<button class="mbkh-btn mbkh-btn-ghost" id="mbkh-cancel-btn">Cancel</button>
|
|
415
|
+
<button class="mbkh-btn mbkh-btn-primary" id="mbkh-select-btn" disabled>
|
|
416
|
+
${icon('check', 13)} <span id="mbkh-select-btn-text">Select File</span>
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
`;
|
|
422
|
+
|
|
423
|
+
document.body.appendChild(overlay);
|
|
424
|
+
return overlay;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getGalleryEl(id) { return _galleryOverlay && _galleryOverlay.querySelector(`#${id}`); }
|
|
428
|
+
|
|
429
|
+
function setGalleryState(mode, message) {
|
|
430
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
431
|
+
const state = getGalleryEl('mbkh-state');
|
|
432
|
+
if (!grid || !state) return;
|
|
433
|
+
|
|
434
|
+
if (mode === 'loading') {
|
|
435
|
+
grid.style.display = 'none';
|
|
436
|
+
state.style.display = '';
|
|
437
|
+
state.innerHTML = `
|
|
438
|
+
<div class="mbkh-spinner"></div>
|
|
439
|
+
<div class="mbkh-state-text">${esc(message || 'Loading bucket files…')}</div>
|
|
440
|
+
`;
|
|
441
|
+
} else if (mode === 'empty') {
|
|
442
|
+
grid.style.display = 'none';
|
|
443
|
+
state.style.display = '';
|
|
444
|
+
state.innerHTML = `
|
|
445
|
+
<div class="mbkh-state-icon">
|
|
446
|
+
<svg width="48" height="48" viewBox="0 0 32 32" fill="var(--mbkh-primary-lt)" stroke="var(--mbkh-primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="mbkh-state-title">No files found</div>
|
|
449
|
+
<div class="mbkh-state-sub">${esc(message || 'This folder is empty or no files match the active filter.')}</div>
|
|
450
|
+
`;
|
|
451
|
+
} else if (mode === 'error') {
|
|
452
|
+
grid.style.display = 'none';
|
|
453
|
+
state.style.display = '';
|
|
454
|
+
state.innerHTML = `
|
|
455
|
+
<div class="mbkh-state-icon" style="color:var(--mbkh-danger);">${icon('warn', 40)}</div>
|
|
456
|
+
<div class="mbkh-state-title" style="color:var(--mbkh-danger);">Failed to load files</div>
|
|
457
|
+
<div class="mbkh-state-sub">${esc(message || 'Please check your connection or permissions.')}</div>
|
|
458
|
+
`;
|
|
459
|
+
} else {
|
|
460
|
+
grid.style.display = '';
|
|
461
|
+
state.style.display = 'none';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderBreadcrumb(prefix, bucket) {
|
|
466
|
+
const bc = getGalleryEl('mbkh-breadcrumb');
|
|
467
|
+
const title = getGalleryEl('mbkh-modal-title');
|
|
468
|
+
if (!bc) return;
|
|
469
|
+
|
|
470
|
+
const parts = (prefix || '').split('/').filter(Boolean);
|
|
471
|
+
const label = bucket ? `Bucket: ${bucket}` : 'My Drive';
|
|
472
|
+
if (title) title.textContent = parts.length ? baseName(parts[parts.length - 1]) : label;
|
|
473
|
+
|
|
474
|
+
let html = `<span class="mbkh-crumb${parts.length === 0 ? ' mbkh-crumb-active' : ''}" data-prefix="">
|
|
475
|
+
${icon('bucket', 12)} <span class="mbkh-crumb-text">Drive</span>
|
|
476
|
+
</span>`;
|
|
477
|
+
parts.forEach((part, idx) => {
|
|
478
|
+
const seg = parts.slice(0, idx + 1).join('/') + '/';
|
|
479
|
+
const isLast = idx === parts.length - 1;
|
|
480
|
+
html += `<span class="mbkh-crumb-sep">/</span>
|
|
481
|
+
<span class="mbkh-crumb${isLast ? ' mbkh-crumb-active' : ''}" data-prefix="${esc(seg)}" title="${esc(part)}">
|
|
482
|
+
<span class="mbkh-crumb-text">${esc(part)}</span>
|
|
483
|
+
</span>`;
|
|
484
|
+
});
|
|
485
|
+
bc.innerHTML = html;
|
|
486
|
+
|
|
487
|
+
bc.querySelectorAll('[data-prefix]').forEach(el => {
|
|
488
|
+
if (!el.classList.contains('mbkh-crumb-active')) {
|
|
489
|
+
el.addEventListener('click', () => loadGalleryFolder(el.dataset.prefix));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function filterItems(files, filterKey) {
|
|
495
|
+
if (!filterKey || filterKey === 'all') return files;
|
|
496
|
+
return files.filter(file => {
|
|
497
|
+
const e = ext(file.Key || file.key || '');
|
|
498
|
+
if (filterKey === 'images') return IMAGE_EXTS.has(e);
|
|
499
|
+
if (filterKey === 'docs') return DOC_EXTS.has(e);
|
|
500
|
+
if (filterKey === 'media') return VIDEO_EXTS.has(e) || AUDIO_EXTS.has(e);
|
|
501
|
+
if (filterKey === 'code') return CODE_EXTS.has(e);
|
|
502
|
+
return true;
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderGalleryItems(files, folders, viewMode, options) {
|
|
507
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
508
|
+
if (!grid) return;
|
|
509
|
+
|
|
510
|
+
grid.innerHTML = '';
|
|
511
|
+
grid.classList.toggle('mbkh-list-mode', viewMode === 'list');
|
|
512
|
+
grid.setAttribute('aria-multiselectable', _galleryState?.multiple ? 'true' : 'false');
|
|
513
|
+
|
|
514
|
+
// Render folders first
|
|
515
|
+
folders.forEach(folderPrefix => {
|
|
516
|
+
const name = baseName(folderPrefix) || folderPrefix;
|
|
517
|
+
const div = document.createElement('div');
|
|
518
|
+
div.className = 'mbkh-item mbkh-card-folder';
|
|
519
|
+
div.setAttribute('role', 'option');
|
|
520
|
+
div.setAttribute('aria-selected', 'false');
|
|
521
|
+
div.dataset.type = 'folder';
|
|
522
|
+
div.dataset.prefix = folderPrefix;
|
|
523
|
+
div.title = `Open folder: ${name}`;
|
|
524
|
+
|
|
525
|
+
if (viewMode === 'list') {
|
|
526
|
+
div.innerHTML = `
|
|
527
|
+
<div class="mbkh-row-icon mbkh-folder-icon">
|
|
528
|
+
<svg viewBox="0 0 32 32" width="18" height="18" fill="var(--mbkh-primary-lt)" stroke="var(--mbkh-primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="mbkh-row-name">${esc(name)}</div>
|
|
531
|
+
<div class="mbkh-row-badge">Folder</div>
|
|
532
|
+
<div class="mbkh-row-size">—</div>
|
|
533
|
+
`;
|
|
534
|
+
} else {
|
|
535
|
+
div.innerHTML = `
|
|
536
|
+
<div class="mbkh-card-preview mbkh-folder-preview">
|
|
537
|
+
<svg class="mbkh-folder-svg" viewBox="0 0 32 32" fill="var(--mbkh-primary-lt)" stroke="var(--mbkh-primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="mbkh-card-details">
|
|
540
|
+
<div class="mbkh-card-name" title="${esc(name)}">${esc(name)}</div>
|
|
541
|
+
<div class="mbkh-card-meta">Folder</div>
|
|
542
|
+
</div>
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
div.addEventListener('click', () => loadGalleryFolder(folderPrefix));
|
|
547
|
+
div.addEventListener('dblclick', () => loadGalleryFolder(folderPrefix));
|
|
548
|
+
grid.appendChild(div);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Render files
|
|
552
|
+
files.forEach(fileObj => {
|
|
553
|
+
const key = fileObj.Key || fileObj.key || '';
|
|
554
|
+
const name = baseName(key);
|
|
555
|
+
const size = fileObj.Size || fileObj.ContentLength || 0;
|
|
556
|
+
const e = ext(key);
|
|
557
|
+
const tinfo = fileTypeInfo(name);
|
|
558
|
+
const isImg = IMAGE_EXTS.has(e);
|
|
559
|
+
const isSelected = _galleryState?.selectedMap?.has(key);
|
|
560
|
+
|
|
561
|
+
const div = document.createElement('div');
|
|
562
|
+
div.className = `mbkh-item mbkh-card-file${isSelected ? ' mbkh-selected' : ''}`;
|
|
563
|
+
div.setAttribute('role', 'option');
|
|
564
|
+
div.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
|
565
|
+
div.dataset.type = 'file';
|
|
566
|
+
div.dataset.key = key;
|
|
567
|
+
div.dataset.name = name;
|
|
568
|
+
div.dataset.size = size;
|
|
569
|
+
div.title = `${name} (${formatBytes(size)})`;
|
|
570
|
+
|
|
571
|
+
const thumbSrc = isImg ? url.view(key, _galleryState?.bucket) : null;
|
|
572
|
+
const fileData = { key, name, size, thumbSrc, tinfo, url: thumbSrc || url.download(key, _galleryState?.bucket) };
|
|
573
|
+
|
|
574
|
+
if (viewMode === 'list') {
|
|
575
|
+
let iconHtml;
|
|
576
|
+
if (isImg) {
|
|
577
|
+
iconHtml = `<div class="mbkh-row-icon mbkh-thumb"><img src="${esc(thumbSrc)}" alt="${esc(name)}" loading="lazy"/></div>`;
|
|
578
|
+
} else {
|
|
579
|
+
const fileIco = VIDEO_EXTS.has(e) ? 'video' : AUDIO_EXTS.has(e) ? 'audio' : 'file';
|
|
580
|
+
iconHtml = `<div class="mbkh-row-icon" style="background:${tinfo.bg};color:${tinfo.color};">${icon(fileIco, 16)}</div>`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
div.innerHTML = `
|
|
584
|
+
${iconHtml}
|
|
585
|
+
<div class="mbkh-row-name">${esc(name)}</div>
|
|
586
|
+
<div class="mbkh-row-badge" style="background:${tinfo.bg};color:${tinfo.color};">${esc(tinfo.label)}</div>
|
|
587
|
+
<div class="mbkh-row-size">${formatBytes(size)}</div>
|
|
588
|
+
<div class="mbkh-check-badge">${icon('check', 11)}</div>
|
|
589
|
+
`;
|
|
590
|
+
} else {
|
|
591
|
+
let previewHtml;
|
|
592
|
+
if (isImg) {
|
|
593
|
+
previewHtml = `
|
|
594
|
+
<div class="mbkh-card-preview mbkh-thumb">
|
|
595
|
+
<img src="${esc(thumbSrc)}" alt="${esc(name)}" loading="lazy"/>
|
|
596
|
+
<span class="mbkh-ext-tag">${esc(e.toUpperCase() || 'IMG')}</span>
|
|
597
|
+
</div>
|
|
598
|
+
`;
|
|
599
|
+
} else {
|
|
600
|
+
const fileIco = VIDEO_EXTS.has(e) ? 'video' : AUDIO_EXTS.has(e) ? 'audio' : 'file';
|
|
601
|
+
previewHtml = `
|
|
602
|
+
<div class="mbkh-card-preview" style="background:${tinfo.bg};color:${tinfo.color};">
|
|
603
|
+
${icon(fileIco, 32)}
|
|
604
|
+
<span class="mbkh-ext-tag" style="background:${tinfo.color};color:#fff;">${esc(tinfo.label)}</span>
|
|
605
|
+
</div>
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
div.innerHTML = `
|
|
610
|
+
${previewHtml}
|
|
611
|
+
<div class="mbkh-card-details">
|
|
612
|
+
<div class="mbkh-card-name" title="${esc(name)}">${esc(name)}</div>
|
|
613
|
+
<div class="mbkh-card-meta">${formatBytes(size) || 'File'}</div>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="mbkh-check-badge">${icon('check', 11)}</div>
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (isImg) {
|
|
620
|
+
const imgEl = div.querySelector('img');
|
|
621
|
+
if (imgEl) {
|
|
622
|
+
imgEl.addEventListener('error', () => {
|
|
623
|
+
const parent = imgEl.parentElement;
|
|
624
|
+
if (parent) {
|
|
625
|
+
parent.classList.remove('mbkh-thumb');
|
|
626
|
+
parent.style.background = tinfo.bg;
|
|
627
|
+
parent.style.color = tinfo.color;
|
|
628
|
+
parent.innerHTML = icon('image', 28);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
div.addEventListener('click', () => toggleGalleryItemSelection(div, fileData));
|
|
635
|
+
div.addEventListener('dblclick', () => {
|
|
636
|
+
if (!_galleryState?.multiple) {
|
|
637
|
+
confirmGallerySelection();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
grid.appendChild(div);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Load-more button if there's a next page
|
|
644
|
+
const state = _galleryState;
|
|
645
|
+
if (state && state.nextToken) {
|
|
646
|
+
const wrap = document.createElement('div');
|
|
647
|
+
wrap.className = 'mbkh-load-more-wrap';
|
|
648
|
+
wrap.innerHTML = `<button class="mbkh-btn mbkh-btn-ghost" id="mbkh-load-more">Load more files…</button>`;
|
|
649
|
+
grid.appendChild(wrap);
|
|
650
|
+
wrap.querySelector('#mbkh-load-more').addEventListener('click', () => loadMoreGallery());
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function toggleGalleryItemSelection(div, fileData) {
|
|
655
|
+
if (!_galleryState) return;
|
|
656
|
+
const isMulti = !!_galleryState.multiple;
|
|
657
|
+
|
|
658
|
+
if (!isMulti) {
|
|
659
|
+
// Single selection mode
|
|
660
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
661
|
+
if (grid) grid.querySelectorAll('.mbkh-item.mbkh-selected').forEach(el => {
|
|
662
|
+
el.classList.remove('mbkh-selected');
|
|
663
|
+
el.setAttribute('aria-selected', 'false');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
_galleryState.selectedMap.clear();
|
|
667
|
+
_galleryState.selectedMap.set(fileData.key, fileData);
|
|
668
|
+
div.classList.add('mbkh-selected');
|
|
669
|
+
div.setAttribute('aria-selected', 'true');
|
|
670
|
+
} else {
|
|
671
|
+
// Multi-selection mode
|
|
672
|
+
if (_galleryState.selectedMap.has(fileData.key)) {
|
|
673
|
+
_galleryState.selectedMap.delete(fileData.key);
|
|
674
|
+
div.classList.remove('mbkh-selected');
|
|
675
|
+
div.setAttribute('aria-selected', 'false');
|
|
676
|
+
} else {
|
|
677
|
+
const max = _galleryState.options?.maxFiles;
|
|
678
|
+
if (max && _galleryState.selectedMap.size >= max) {
|
|
679
|
+
toast.warning(`Maximum ${max} files allowed`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
_galleryState.selectedMap.set(fileData.key, fileData);
|
|
683
|
+
div.classList.add('mbkh-selected');
|
|
684
|
+
div.setAttribute('aria-selected', 'true');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
updateGalleryFooterSelection();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function updateGalleryFooterSelection() {
|
|
692
|
+
const selWrap = getGalleryEl('mbkh-sel-wrap');
|
|
693
|
+
const selectBtn = getGalleryEl('mbkh-select-btn');
|
|
694
|
+
const btnText = getGalleryEl('mbkh-select-btn-text');
|
|
695
|
+
if (!selWrap || !_galleryState) return;
|
|
696
|
+
|
|
697
|
+
const count = _galleryState.selectedMap.size;
|
|
698
|
+
|
|
699
|
+
if (count === 0) {
|
|
700
|
+
selWrap.innerHTML = `<span class="mbkh-selected-placeholder">Click a file to select</span>`;
|
|
701
|
+
if (selectBtn) selectBtn.disabled = true;
|
|
702
|
+
if (btnText) btnText.textContent = 'Select File';
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (selectBtn) selectBtn.disabled = false;
|
|
707
|
+
|
|
708
|
+
if (!_galleryState.multiple) {
|
|
709
|
+
const item = _galleryState.selectedMap.values().next().value;
|
|
710
|
+
let iconSnippet;
|
|
711
|
+
if (item.thumbSrc) {
|
|
712
|
+
iconSnippet = `<div class="mbkh-sel-thumb"><img src="${esc(item.thumbSrc)}" alt=""/></div>`;
|
|
713
|
+
} else {
|
|
714
|
+
iconSnippet = `<div class="mbkh-sel-badge" style="background:${item.tinfo?.bg || 'var(--mbkh-primary-lt)'};color:${item.tinfo?.color || 'var(--mbkh-primary)'};">${esc(item.tinfo?.label || 'FILE')}</div>`;
|
|
715
|
+
}
|
|
716
|
+
selWrap.innerHTML = `
|
|
717
|
+
${iconSnippet}
|
|
718
|
+
<div class="mbkh-sel-info">
|
|
719
|
+
<div class="mbkh-sel-name" title="${esc(item.name)}">${esc(item.name)}</div>
|
|
720
|
+
<div class="mbkh-sel-meta">${formatBytes(item.size)}</div>
|
|
721
|
+
</div>
|
|
722
|
+
<button class="mbkh-sel-clear" id="mbkh-sel-clear-btn" title="Deselect">${icon('close', 11)}</button>
|
|
723
|
+
`;
|
|
724
|
+
if (btnText) btnText.textContent = 'Select File';
|
|
725
|
+
} else {
|
|
726
|
+
// Multi mode tray
|
|
727
|
+
const items = Array.from(_galleryState.selectedMap.values());
|
|
728
|
+
const maxDisplay = 3;
|
|
729
|
+
const visible = items.slice(0, maxDisplay);
|
|
730
|
+
const remaining = items.length - maxDisplay;
|
|
731
|
+
|
|
732
|
+
let chipsHtml = visible.map(it => `
|
|
733
|
+
<div class="mbkh-multi-chip" title="${esc(it.name)}">
|
|
734
|
+
<span class="mbkh-multi-chip-name">${esc(it.name)}</span>
|
|
735
|
+
<span class="mbkh-multi-chip-rm" data-rm-key="${esc(it.key)}">${icon('close', 9)}</span>
|
|
736
|
+
</div>
|
|
737
|
+
`).join('');
|
|
738
|
+
|
|
739
|
+
if (remaining > 0) {
|
|
740
|
+
chipsHtml += `<div class="mbkh-multi-chip-more">+${remaining} more</div>`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
selWrap.innerHTML = `
|
|
744
|
+
<div class="mbkh-multi-tray">
|
|
745
|
+
<div class="mbkh-multi-count">${count} selected:</div>
|
|
746
|
+
<div class="mbkh-multi-chips">${chipsHtml}</div>
|
|
747
|
+
<button class="mbkh-sel-clear" id="mbkh-sel-clear-btn" title="Clear all">${icon('close', 11)}</button>
|
|
748
|
+
</div>
|
|
749
|
+
`;
|
|
750
|
+
|
|
751
|
+
selWrap.querySelectorAll('[data-rm-key]').forEach(btn => {
|
|
752
|
+
btn.addEventListener('click', (e) => {
|
|
753
|
+
e.stopPropagation();
|
|
754
|
+
const k = btn.dataset.rmKey;
|
|
755
|
+
_galleryState.selectedMap.delete(k);
|
|
756
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
757
|
+
const card = grid?.querySelector(`[data-key="${CSS.escape(k)}"]`);
|
|
758
|
+
if (card) {
|
|
759
|
+
card.classList.remove('mbkh-selected');
|
|
760
|
+
card.setAttribute('aria-selected', 'false');
|
|
761
|
+
}
|
|
762
|
+
updateGalleryFooterSelection();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
if (btnText) btnText.textContent = `Select ${count} File${count !== 1 ? 's' : ''}`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
selWrap.querySelector('#mbkh-sel-clear-btn')?.addEventListener('click', (e) => {
|
|
770
|
+
e.stopPropagation();
|
|
771
|
+
clearGallerySelection();
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function clearGallerySelection() {
|
|
776
|
+
if (_galleryState) {
|
|
777
|
+
_galleryState.selectedMap.clear();
|
|
778
|
+
}
|
|
779
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
780
|
+
if (grid) grid.querySelectorAll('.mbkh-item.mbkh-selected').forEach(el => {
|
|
781
|
+
el.classList.remove('mbkh-selected');
|
|
782
|
+
el.setAttribute('aria-selected', 'false');
|
|
783
|
+
});
|
|
784
|
+
updateGalleryFooterSelection();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function confirmGallerySelection() {
|
|
788
|
+
if (!_galleryState || !_galleryState.selectedMap.size) return;
|
|
789
|
+
if (_galleryState.multiple) {
|
|
790
|
+
const items = Array.from(_galleryState.selectedMap.values());
|
|
791
|
+
closeGallery(items);
|
|
792
|
+
} else {
|
|
793
|
+
const item = _galleryState.selectedMap.values().next().value;
|
|
794
|
+
closeGallery(item);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function loadGalleryFolder(prefix, token, autoSelectKey) {
|
|
799
|
+
if (!_galleryState) return;
|
|
800
|
+
_galleryState.prefix = prefix || '';
|
|
801
|
+
_galleryState.nextToken = null;
|
|
802
|
+
|
|
803
|
+
if (!autoSelectKey) {
|
|
804
|
+
clearGallerySelection();
|
|
805
|
+
}
|
|
806
|
+
renderBreadcrumb(prefix, _galleryState.bucket);
|
|
807
|
+
setGalleryState('loading');
|
|
808
|
+
|
|
809
|
+
const searchEl = getGalleryEl('mbkh-search');
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
const result = await api.listFiles(prefix, {
|
|
813
|
+
bucket: _galleryState.bucket,
|
|
814
|
+
search: searchEl ? searchEl.value.trim() : '',
|
|
815
|
+
token: token || '',
|
|
816
|
+
recursive: false,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
_galleryState.nextToken = result.nextContinuationToken || null;
|
|
820
|
+
_galleryState.rawFiles = result.files || [];
|
|
821
|
+
_galleryState.rawFolders = result.folders || [];
|
|
822
|
+
|
|
823
|
+
applyGalleryFilter();
|
|
824
|
+
|
|
825
|
+
if (autoSelectKey) {
|
|
826
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
827
|
+
const card = grid?.querySelector(`[data-key="${CSS.escape(autoSelectKey)}"]`);
|
|
828
|
+
if (card) {
|
|
829
|
+
const name = baseName(autoSelectKey);
|
|
830
|
+
const tinfo = fileTypeInfo(name);
|
|
831
|
+
const thumbSrc = IMAGE_EXTS.has(ext(name)) ? url.view(autoSelectKey, _galleryState.bucket) : null;
|
|
832
|
+
toggleGalleryItemSelection(card, { key: autoSelectKey, name, size: 0, thumbSrc, tinfo, url: thumbSrc || url.download(autoSelectKey, _galleryState.bucket) });
|
|
833
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
setGalleryState('error', err.message || 'Failed to load files');
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function applyGalleryFilter() {
|
|
842
|
+
if (!_galleryState) return;
|
|
843
|
+
const filterKey = _galleryState.activeFilter || 'all';
|
|
844
|
+
const filteredFiles = filterItems(_galleryState.rawFiles || [], filterKey);
|
|
845
|
+
const folders = (filterKey === 'all') ? (_galleryState.rawFolders || []) : [];
|
|
846
|
+
|
|
847
|
+
if (!filteredFiles.length && !folders.length) {
|
|
848
|
+
setGalleryState('empty');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
setGalleryState('content');
|
|
853
|
+
renderGalleryItems(filteredFiles, folders, _galleryState.viewMode, _galleryState.options);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function loadMoreGallery() {
|
|
857
|
+
if (!_galleryState || !_galleryState.nextToken) return;
|
|
858
|
+
const grid = getGalleryEl('mbkh-grid');
|
|
859
|
+
const btn = grid && grid.querySelector('#mbkh-load-more');
|
|
860
|
+
if (btn) { btn.textContent = 'Loading…'; btn.disabled = true; }
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
const result = await api.listFiles(_galleryState.prefix, {
|
|
864
|
+
bucket: _galleryState.bucket,
|
|
865
|
+
token: _galleryState.nextToken,
|
|
866
|
+
recursive: false,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
_galleryState.nextToken = result.nextContinuationToken || null;
|
|
870
|
+
|
|
871
|
+
const oldWrap = grid && grid.querySelector('.mbkh-load-more-wrap');
|
|
872
|
+
if (oldWrap) oldWrap.remove();
|
|
873
|
+
|
|
874
|
+
const files = result.files || [];
|
|
875
|
+
const folders = result.folders || [];
|
|
876
|
+
|
|
877
|
+
_galleryState.rawFiles = [...(_galleryState.rawFiles || []), ...files];
|
|
878
|
+
_galleryState.rawFolders = [...(_galleryState.rawFolders || []), ...folders];
|
|
879
|
+
|
|
880
|
+
applyGalleryFilter();
|
|
881
|
+
} catch (err) {
|
|
882
|
+
if (btn) { btn.textContent = 'Retry'; btn.disabled = false; }
|
|
883
|
+
toast.error(err.message || 'Failed to load more');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// In-Modal Upload Handler
|
|
888
|
+
async function handleInModalUpload(files) {
|
|
889
|
+
if (!files || !files.length || !_galleryState) return;
|
|
890
|
+
const banner = getGalleryEl('mbkh-modal-upload-banner');
|
|
891
|
+
const textEl = getGalleryEl('mbkh-upload-banner-text');
|
|
892
|
+
const fillEl = getGalleryEl('mbkh-upload-banner-fill');
|
|
893
|
+
if (banner) banner.style.display = 'flex';
|
|
894
|
+
|
|
895
|
+
let lastUploadedKey = null;
|
|
896
|
+
const total = files.length;
|
|
897
|
+
|
|
898
|
+
for (let i = 0; i < total; i++) {
|
|
899
|
+
const file = files[i];
|
|
900
|
+
if (textEl) textEl.textContent = `Uploading ${file.name} (${i + 1}/${total})…`;
|
|
901
|
+
if (fillEl) fillEl.style.width = '0%';
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
const res = await api.uploadFile(_galleryState.prefix || '', file, {
|
|
905
|
+
bucket: _galleryState.bucket || '',
|
|
906
|
+
onProgress: p => {
|
|
907
|
+
if (fillEl) fillEl.style.width = `${Math.round(p * 100)}%`;
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
lastUploadedKey = res.key;
|
|
911
|
+
toast.success(`Uploaded: ${file.name}`);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
toast.error(`Failed ${file.name}: ${err.message}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (banner) {
|
|
918
|
+
if (fillEl) fillEl.style.width = '100%';
|
|
919
|
+
setTimeout(() => { banner.style.display = 'none'; }, 400);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Refresh folder and auto-select
|
|
923
|
+
await loadGalleryFolder(_galleryState.prefix, null, lastUploadedKey);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function openGallery(options) {
|
|
927
|
+
options = options || {};
|
|
928
|
+
return new Promise(resolve => {
|
|
929
|
+
if (!_galleryOverlay) {
|
|
930
|
+
_galleryOverlay = buildGalleryDom();
|
|
931
|
+
attachGalleryEvents();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
_galleryState = {
|
|
935
|
+
prefix: options.prefix || '',
|
|
936
|
+
bucket: options.bucket || '',
|
|
937
|
+
multiple: !!options.multiple,
|
|
938
|
+
viewMode: options.defaultView || localStorage.getItem('mbkh_view') || 'grid',
|
|
939
|
+
selectedMap: new Map(),
|
|
940
|
+
nextToken: null,
|
|
941
|
+
options,
|
|
942
|
+
};
|
|
943
|
+
_galleryResolve = resolve;
|
|
944
|
+
|
|
945
|
+
// Restore view mode
|
|
946
|
+
const gridBtn = getGalleryEl('mbkh-view-grid');
|
|
947
|
+
const listBtn = getGalleryEl('mbkh-view-list');
|
|
948
|
+
if (gridBtn && listBtn) {
|
|
949
|
+
const isGrid = _galleryState.viewMode !== 'list';
|
|
950
|
+
gridBtn.classList.toggle('mbkh-active', isGrid);
|
|
951
|
+
listBtn.classList.toggle('mbkh-active', !isGrid);
|
|
952
|
+
gridBtn.setAttribute('aria-pressed', isGrid ? 'true' : 'false');
|
|
953
|
+
listBtn.setAttribute('aria-pressed', isGrid ? 'false' : 'true');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const filterBar = getGalleryEl('mbkh-filter-bar');
|
|
957
|
+
if (filterBar) {
|
|
958
|
+
_galleryState.activeFilter = 'all';
|
|
959
|
+
filterBar.querySelectorAll('[data-filter]').forEach(b => b.classList.toggle('mbkh-active', b.dataset.filter === 'all'));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const searchEl = getGalleryEl('mbkh-search');
|
|
963
|
+
if (searchEl) searchEl.value = '';
|
|
964
|
+
|
|
965
|
+
_galleryOverlay.style.display = '';
|
|
966
|
+
_galleryOverlay.classList.remove('mbkh-closing');
|
|
967
|
+
document.body.style.overflow = 'hidden';
|
|
968
|
+
|
|
969
|
+
loadGalleryFolder(_galleryState.prefix);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function closeGallery(result) {
|
|
974
|
+
if (!_galleryOverlay) return;
|
|
975
|
+
_galleryOverlay.classList.add('mbkh-closing');
|
|
976
|
+
document.body.style.overflow = '';
|
|
977
|
+
|
|
978
|
+
setTimeout(() => {
|
|
979
|
+
if (_galleryOverlay) _galleryOverlay.style.display = 'none';
|
|
980
|
+
if (_galleryResolve) {
|
|
981
|
+
const r = _galleryResolve;
|
|
982
|
+
_galleryResolve = null;
|
|
983
|
+
r(result || null);
|
|
984
|
+
}
|
|
985
|
+
}, 180);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function attachGalleryEvents() {
|
|
989
|
+
if (!_galleryOverlay) return;
|
|
990
|
+
|
|
991
|
+
// Close on overlay click
|
|
992
|
+
_galleryOverlay.addEventListener('click', e => {
|
|
993
|
+
if (e.target === _galleryOverlay) closeGallery(null);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Close button
|
|
997
|
+
getGalleryEl('mbkh-close-btn')?.addEventListener('click', () => closeGallery(null));
|
|
998
|
+
getGalleryEl('mbkh-cancel-btn')?.addEventListener('click', () => closeGallery(null));
|
|
999
|
+
getGalleryEl('mbkh-select-btn')?.addEventListener('click', () => confirmGallerySelection());
|
|
1000
|
+
|
|
1001
|
+
// In-modal Upload Trigger
|
|
1002
|
+
const fileInput = getGalleryEl('mbkh-modal-file-input');
|
|
1003
|
+
const uploadTrigger = getGalleryEl('mbkh-modal-upload-trigger');
|
|
1004
|
+
uploadTrigger?.addEventListener('click', () => fileInput?.click());
|
|
1005
|
+
fileInput?.addEventListener('change', () => {
|
|
1006
|
+
if (fileInput.files && fileInput.files.length) {
|
|
1007
|
+
handleInModalUpload(Array.from(fileInput.files));
|
|
1008
|
+
fileInput.value = '';
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// In-modal Drag and Drop
|
|
1013
|
+
const modalBody = getGalleryEl('mbkh-modal-body');
|
|
1014
|
+
const dragOverlay = getGalleryEl('mbkh-drag-overlay');
|
|
1015
|
+
let dragCounter = 0;
|
|
1016
|
+
|
|
1017
|
+
modalBody?.addEventListener('dragenter', e => {
|
|
1018
|
+
e.preventDefault();
|
|
1019
|
+
dragCounter++;
|
|
1020
|
+
if (dragOverlay) dragOverlay.style.display = 'flex';
|
|
1021
|
+
});
|
|
1022
|
+
modalBody?.addEventListener('dragleave', e => {
|
|
1023
|
+
e.preventDefault();
|
|
1024
|
+
dragCounter--;
|
|
1025
|
+
if (dragCounter <= 0 && dragOverlay) {
|
|
1026
|
+
dragOverlay.style.display = 'none';
|
|
1027
|
+
dragCounter = 0;
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
modalBody?.addEventListener('dragover', e => { e.preventDefault(); });
|
|
1031
|
+
modalBody?.addEventListener('drop', e => {
|
|
1032
|
+
e.preventDefault();
|
|
1033
|
+
dragCounter = 0;
|
|
1034
|
+
if (dragOverlay) dragOverlay.style.display = 'none';
|
|
1035
|
+
const files = e.dataTransfer && e.dataTransfer.files;
|
|
1036
|
+
if (files && files.length) {
|
|
1037
|
+
handleInModalUpload(Array.from(files));
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// Filter buttons
|
|
1042
|
+
const filterBar = getGalleryEl('mbkh-filter-bar');
|
|
1043
|
+
filterBar?.addEventListener('click', e => {
|
|
1044
|
+
const btn = e.target.closest('[data-filter]');
|
|
1045
|
+
if (!btn || !_galleryState) return;
|
|
1046
|
+
_galleryState.activeFilter = btn.dataset.filter;
|
|
1047
|
+
filterBar.querySelectorAll('[data-filter]').forEach(b => b.classList.toggle('mbkh-active', b === btn));
|
|
1048
|
+
applyGalleryFilter();
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// Search (debounced)
|
|
1052
|
+
const searchEl = getGalleryEl('mbkh-search');
|
|
1053
|
+
let searchTimer = null;
|
|
1054
|
+
searchEl?.addEventListener('input', () => {
|
|
1055
|
+
clearTimeout(searchTimer);
|
|
1056
|
+
searchTimer = setTimeout(() => {
|
|
1057
|
+
loadGalleryFolder(_galleryState ? _galleryState.prefix : '');
|
|
1058
|
+
}, 350);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// View toggle
|
|
1062
|
+
getGalleryEl('mbkh-view-grid')?.addEventListener('click', () => {
|
|
1063
|
+
_galleryState.viewMode = 'grid';
|
|
1064
|
+
localStorage.setItem('mbkh_view', 'grid');
|
|
1065
|
+
getGalleryEl('mbkh-view-grid')?.classList.add('mbkh-active');
|
|
1066
|
+
getGalleryEl('mbkh-view-list')?.classList.remove('mbkh-active');
|
|
1067
|
+
loadGalleryFolder(_galleryState.prefix);
|
|
1068
|
+
});
|
|
1069
|
+
getGalleryEl('mbkh-view-list')?.addEventListener('click', () => {
|
|
1070
|
+
_galleryState.viewMode = 'list';
|
|
1071
|
+
localStorage.setItem('mbkh_view', 'list');
|
|
1072
|
+
getGalleryEl('mbkh-view-list')?.classList.add('mbkh-active');
|
|
1073
|
+
getGalleryEl('mbkh-view-grid')?.classList.remove('mbkh-active');
|
|
1074
|
+
loadGalleryFolder(_galleryState.prefix);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Keyboard navigation
|
|
1078
|
+
_galleryOverlay.addEventListener('keydown', e => {
|
|
1079
|
+
if (e.key === 'Escape') closeGallery(null);
|
|
1080
|
+
if (e.key === 'Enter' && _galleryState && _galleryState.selectedMap.size) confirmGallerySelection();
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1085
|
+
// File Picker (Input wrapper with Live Preview & Multi-Select support)
|
|
1086
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Wraps an existing <input> element with a styled picker widget.
|
|
1090
|
+
*
|
|
1091
|
+
* @param {HTMLInputElement|string} inputEl
|
|
1092
|
+
* @param {{ bucket?: string, prefix?: string, placeholder?: string,
|
|
1093
|
+
* preview?: string, multiple?: boolean, onSelect?: Function }} options
|
|
1094
|
+
*/
|
|
1095
|
+
function createPicker(inputEl, options) {
|
|
1096
|
+
if (typeof inputEl === 'string') inputEl = document.querySelector(inputEl);
|
|
1097
|
+
if (!inputEl) throw new Error('[MBKBucket] createPicker: input element not found');
|
|
1098
|
+
options = options || {};
|
|
1099
|
+
|
|
1100
|
+
const isMulti = options.multiple !== undefined ? options.multiple : (inputEl.dataset.mbkMultiple === 'true');
|
|
1101
|
+
const previewSel = options.preview || inputEl.dataset.mbkPreview || '';
|
|
1102
|
+
|
|
1103
|
+
// Build wrapper
|
|
1104
|
+
const wrap = document.createElement('div');
|
|
1105
|
+
wrap.className = 'mbkh-picker';
|
|
1106
|
+
wrap.id = inputEl.id ? `mbkh-picker-${inputEl.id}` : `mbkh-picker-${Date.now()}`;
|
|
1107
|
+
|
|
1108
|
+
// Hidden real input
|
|
1109
|
+
const hidden = document.createElement('input');
|
|
1110
|
+
hidden.type = 'hidden';
|
|
1111
|
+
hidden.name = inputEl.name || '';
|
|
1112
|
+
hidden.id = inputEl.id || '';
|
|
1113
|
+
hidden.value = inputEl.value || '';
|
|
1114
|
+
inputEl.removeAttribute('id');
|
|
1115
|
+
|
|
1116
|
+
// Display input
|
|
1117
|
+
const display = document.createElement('input');
|
|
1118
|
+
display.type = 'text';
|
|
1119
|
+
display.className = 'mbkh-picker-value';
|
|
1120
|
+
display.readOnly = true;
|
|
1121
|
+
display.placeholder = options.placeholder || (isMulti ? 'Select files from bucket…' : 'Select a file from bucket…');
|
|
1122
|
+
display.value = hidden.value;
|
|
1123
|
+
display.setAttribute('aria-label', 'Selected file key(s)');
|
|
1124
|
+
|
|
1125
|
+
const actions = document.createElement('div');
|
|
1126
|
+
actions.className = 'mbkh-picker-actions';
|
|
1127
|
+
|
|
1128
|
+
const browseBtn = document.createElement('button');
|
|
1129
|
+
browseBtn.type = 'button';
|
|
1130
|
+
browseBtn.className = 'mbkh-picker-btn';
|
|
1131
|
+
browseBtn.innerHTML = `${icon('browse', 13)} Browse`;
|
|
1132
|
+
|
|
1133
|
+
const clearBtn = document.createElement('button');
|
|
1134
|
+
clearBtn.type = 'button';
|
|
1135
|
+
clearBtn.className = 'mbkh-picker-clear';
|
|
1136
|
+
clearBtn.title = 'Clear selection';
|
|
1137
|
+
clearBtn.innerHTML = icon('close', 11);
|
|
1138
|
+
clearBtn.hidden = !hidden.value;
|
|
1139
|
+
clearBtn.setAttribute('aria-label', 'Clear selection');
|
|
1140
|
+
|
|
1141
|
+
actions.appendChild(browseBtn);
|
|
1142
|
+
actions.appendChild(clearBtn);
|
|
1143
|
+
|
|
1144
|
+
wrap.appendChild(display);
|
|
1145
|
+
wrap.appendChild(actions);
|
|
1146
|
+
|
|
1147
|
+
inputEl.parentNode.insertBefore(wrap, inputEl);
|
|
1148
|
+
inputEl.parentNode.insertBefore(hidden, wrap.nextSibling);
|
|
1149
|
+
inputEl.remove();
|
|
1150
|
+
|
|
1151
|
+
// Cache initial preview element state if present
|
|
1152
|
+
let previewEl = null;
|
|
1153
|
+
let originalPreviewSrc = null;
|
|
1154
|
+
if (previewSel) {
|
|
1155
|
+
previewEl = document.querySelector(previewSel);
|
|
1156
|
+
if (previewEl) {
|
|
1157
|
+
if (previewEl.tagName.toLowerCase() === 'img') {
|
|
1158
|
+
originalPreviewSrc = previewEl.getAttribute('data-mbk-original-src') || previewEl.getAttribute('src') || '';
|
|
1159
|
+
if (!previewEl.hasAttribute('data-mbk-original-src')) previewEl.setAttribute('data-mbk-original-src', originalPreviewSrc);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function updateLivePreview(keyOrKeys) {
|
|
1165
|
+
if (!previewEl) return;
|
|
1166
|
+
const key = Array.isArray(keyOrKeys) ? keyOrKeys[0] : keyOrKeys;
|
|
1167
|
+
if (key) {
|
|
1168
|
+
const viewUrl = url.view(key, options.bucket);
|
|
1169
|
+
if (previewEl.tagName.toLowerCase() === 'img') {
|
|
1170
|
+
previewEl.src = viewUrl;
|
|
1171
|
+
} else {
|
|
1172
|
+
previewEl.style.backgroundImage = `url("${viewUrl}")`;
|
|
1173
|
+
previewEl.style.backgroundSize = 'cover';
|
|
1174
|
+
previewEl.style.backgroundPosition = 'center';
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
if (previewEl.tagName.toLowerCase() === 'img') {
|
|
1178
|
+
previewEl.src = originalPreviewSrc || '';
|
|
1179
|
+
} else {
|
|
1180
|
+
previewEl.style.backgroundImage = '';
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Set initial preview if input had pre-filled value
|
|
1186
|
+
if (hidden.value) {
|
|
1187
|
+
updateLivePreview(hidden.value);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function setValue(val) {
|
|
1191
|
+
let stringVal = '';
|
|
1192
|
+
if (Array.isArray(val)) {
|
|
1193
|
+
stringVal = val.map(v => typeof v === 'object' ? v.key : v).filter(Boolean).join(',');
|
|
1194
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
1195
|
+
stringVal = val.key || '';
|
|
1196
|
+
} else {
|
|
1197
|
+
stringVal = String(val || '');
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
hidden.value = stringVal;
|
|
1201
|
+
display.value = stringVal;
|
|
1202
|
+
clearBtn.hidden = !stringVal;
|
|
1203
|
+
display.placeholder = stringVal ? '' : (options.placeholder || 'Select a file from bucket…');
|
|
1204
|
+
|
|
1205
|
+
updateLivePreview(stringVal);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
browseBtn.addEventListener('click', async () => {
|
|
1209
|
+
const result = await openGallery({
|
|
1210
|
+
prefix: options.prefix || '',
|
|
1211
|
+
bucket: options.bucket || '',
|
|
1212
|
+
multiple: isMulti,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
if (result) {
|
|
1216
|
+
setValue(result);
|
|
1217
|
+
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1218
|
+
if (options.onSelect) options.onSelect(result);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
clearBtn.addEventListener('click', () => {
|
|
1223
|
+
setValue('');
|
|
1224
|
+
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1225
|
+
if (options.onSelect) options.onSelect(null);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
return {
|
|
1229
|
+
el: wrap,
|
|
1230
|
+
getValue: () => hidden.value,
|
|
1231
|
+
setValue,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1236
|
+
// Upload Widget
|
|
1237
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1238
|
+
|
|
1239
|
+
function createUploader(containerEl, options) {
|
|
1240
|
+
if (typeof containerEl === 'string') containerEl = document.querySelector(containerEl);
|
|
1241
|
+
if (!containerEl) throw new Error('[MBKBucket] createUploader: container element not found');
|
|
1242
|
+
options = options || {};
|
|
1243
|
+
|
|
1244
|
+
const multiple = options.multiple !== false;
|
|
1245
|
+
const accept = options.accept || '';
|
|
1246
|
+
|
|
1247
|
+
const root = document.createElement('div');
|
|
1248
|
+
root.className = 'mbkh-uploader';
|
|
1249
|
+
|
|
1250
|
+
const dropzone = document.createElement('div');
|
|
1251
|
+
dropzone.className = 'mbkh-dropzone';
|
|
1252
|
+
dropzone.setAttribute('role', 'button');
|
|
1253
|
+
dropzone.setAttribute('tabindex', '0');
|
|
1254
|
+
dropzone.setAttribute('aria-label', 'Upload files — drag and drop or click to browse');
|
|
1255
|
+
dropzone.innerHTML = `
|
|
1256
|
+
<div class="mbkh-dropzone-icon">${icon('upload', 24)}</div>
|
|
1257
|
+
<div class="mbkh-dropzone-title">Drag & drop files here</div>
|
|
1258
|
+
<div class="mbkh-dropzone-sub">or <strong>browse your device</strong></div>
|
|
1259
|
+
${accept ? `<div style="font-size:11px;color:var(--mbkh-text-dim);margin-top:2px;">Accepted: ${esc(accept)}</div>` : ''}`;
|
|
1260
|
+
|
|
1261
|
+
const fileInput = document.createElement('input');
|
|
1262
|
+
fileInput.type = 'file';
|
|
1263
|
+
fileInput.multiple = multiple;
|
|
1264
|
+
fileInput.accept = accept;
|
|
1265
|
+
fileInput.style.display = 'none';
|
|
1266
|
+
|
|
1267
|
+
const listEl = document.createElement('div');
|
|
1268
|
+
listEl.className = 'mbkh-upload-list';
|
|
1269
|
+
|
|
1270
|
+
const actionsEl = document.createElement('div');
|
|
1271
|
+
actionsEl.className = 'mbkh-upload-actions';
|
|
1272
|
+
actionsEl.style.display = 'none';
|
|
1273
|
+
actionsEl.innerHTML = `
|
|
1274
|
+
<span class="mbkh-upload-summary" id="mbkh-upl-summary"></span>
|
|
1275
|
+
<button type="button" class="mbkh-btn mbkh-btn-ghost" id="mbkh-upl-clear">Clear done</button>
|
|
1276
|
+
<button type="button" class="mbkh-btn mbkh-btn-primary" id="mbkh-upl-start">Upload All</button>`;
|
|
1277
|
+
|
|
1278
|
+
root.appendChild(dropzone);
|
|
1279
|
+
root.appendChild(fileInput);
|
|
1280
|
+
root.appendChild(listEl);
|
|
1281
|
+
root.appendChild(actionsEl);
|
|
1282
|
+
containerEl.appendChild(root);
|
|
1283
|
+
|
|
1284
|
+
let queue = [];
|
|
1285
|
+
|
|
1286
|
+
function updateSummary() {
|
|
1287
|
+
const total = queue.length;
|
|
1288
|
+
const done = queue.filter(f => f.state === 'done').length;
|
|
1289
|
+
const errors = queue.filter(f => f.state === 'error').length;
|
|
1290
|
+
const pending = queue.filter(f => f.state === 'pending').length;
|
|
1291
|
+
|
|
1292
|
+
const summaryEl = root.querySelector('#mbkh-upl-summary');
|
|
1293
|
+
if (summaryEl) {
|
|
1294
|
+
summaryEl.textContent = `${done}/${total} uploaded${errors ? ` • ${errors} failed` : ''}${pending ? ` • ${pending} pending` : ''}`;
|
|
1295
|
+
}
|
|
1296
|
+
actionsEl.style.display = total ? '' : 'none';
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function renderItem(entry) {
|
|
1300
|
+
const existing = root.querySelector(`[data-upload-id="${CSS.escape(entry.id)}"]`);
|
|
1301
|
+
if (existing) { updateUploadItemEl(existing, entry); return; }
|
|
1302
|
+
|
|
1303
|
+
const e = ext(entry.file.name);
|
|
1304
|
+
const ti = fileTypeInfo(entry.file.name);
|
|
1305
|
+
|
|
1306
|
+
const div = document.createElement('div');
|
|
1307
|
+
div.className = 'mbkh-upload-item';
|
|
1308
|
+
div.dataset.uploadId = entry.id;
|
|
1309
|
+
div.innerHTML = `
|
|
1310
|
+
<div class="mbkh-upload-file-icon" style="background:${ti.bg};color:${ti.color}">${ti.label.slice(0,4)}</div>
|
|
1311
|
+
<div class="mbkh-upload-info">
|
|
1312
|
+
<div class="mbkh-upload-name" title="${esc(entry.file.name)}">${esc(entry.file.name)}</div>
|
|
1313
|
+
<div class="mbkh-upload-progress-row">
|
|
1314
|
+
<div class="mbkh-progress-bar-wrap"><div class="mbkh-progress-bar" style="width:0%"></div></div>
|
|
1315
|
+
<span class="mbkh-upload-pct">0%</span>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div class="mbkh-upload-status">${formatBytes(entry.file.size)}</div>
|
|
1318
|
+
</div>
|
|
1319
|
+
<button class="mbkh-upload-remove" title="Remove" aria-label="Remove file">${icon('trash', 13)}</button>`;
|
|
1320
|
+
|
|
1321
|
+
div.querySelector('.mbkh-upload-remove').addEventListener('click', () => {
|
|
1322
|
+
queue = queue.filter(q => q.id !== entry.id);
|
|
1323
|
+
div.remove();
|
|
1324
|
+
updateSummary();
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
listEl.appendChild(div);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function updateUploadItemEl(el, entry) {
|
|
1331
|
+
const bar = el.querySelector('.mbkh-progress-bar');
|
|
1332
|
+
const pct = el.querySelector('.mbkh-upload-pct');
|
|
1333
|
+
const status = el.querySelector('.mbkh-upload-status');
|
|
1334
|
+
const p = Math.round((entry.progress || 0) * 100);
|
|
1335
|
+
|
|
1336
|
+
if (bar) bar.style.width = p + '%';
|
|
1337
|
+
if (pct) pct.textContent = p + '%';
|
|
1338
|
+
|
|
1339
|
+
el.classList.remove('mbkh-done', 'mbkh-error');
|
|
1340
|
+
if (entry.state === 'done') {
|
|
1341
|
+
el.classList.add('mbkh-done');
|
|
1342
|
+
if (status) status.textContent = '✓ Uploaded';
|
|
1343
|
+
if (pct) pct.textContent = '100%';
|
|
1344
|
+
if (bar) bar.style.width = '100%';
|
|
1345
|
+
const rmBtn = el.querySelector('.mbkh-upload-remove');
|
|
1346
|
+
if (rmBtn) rmBtn.disabled = false;
|
|
1347
|
+
} else if (entry.state === 'error') {
|
|
1348
|
+
el.classList.add('mbkh-error');
|
|
1349
|
+
if (status) status.textContent = '✕ ' + (entry.error || 'Failed');
|
|
1350
|
+
} else if (entry.state === 'uploading') {
|
|
1351
|
+
if (status) status.textContent = 'Uploading…';
|
|
1352
|
+
const rmBtn = el.querySelector('.mbkh-upload-remove');
|
|
1353
|
+
if (rmBtn) rmBtn.disabled = true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function addFiles(files) {
|
|
1358
|
+
const arr = Array.from(files);
|
|
1359
|
+
if (!multiple && arr.length > 0) {
|
|
1360
|
+
queue = [];
|
|
1361
|
+
listEl.innerHTML = '';
|
|
1362
|
+
}
|
|
1363
|
+
arr.forEach(file => {
|
|
1364
|
+
const id = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1365
|
+
const entry = { file, id, state: 'pending', progress: 0 };
|
|
1366
|
+
queue.push(entry);
|
|
1367
|
+
renderItem(entry);
|
|
1368
|
+
});
|
|
1369
|
+
updateSummary();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function startUploadAll() {
|
|
1373
|
+
const pending = queue.filter(e => e.state === 'pending' || e.state === 'error');
|
|
1374
|
+
if (!pending.length) {
|
|
1375
|
+
toast.info('No pending files to upload');
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const startBtn = root.querySelector('#mbkh-upl-start');
|
|
1380
|
+
if (startBtn) { startBtn.disabled = true; startBtn.textContent = 'Uploading…'; }
|
|
1381
|
+
|
|
1382
|
+
for (const entry of pending) {
|
|
1383
|
+
entry.state = 'uploading';
|
|
1384
|
+
entry.progress = 0;
|
|
1385
|
+
const el = root.querySelector(`[data-upload-id="${CSS.escape(entry.id)}"]`);
|
|
1386
|
+
if (el) updateUploadItemEl(el, entry);
|
|
1387
|
+
|
|
1388
|
+
try {
|
|
1389
|
+
const result = await api.uploadFile(options.prefix || '', entry.file, {
|
|
1390
|
+
bucket: options.bucket || '',
|
|
1391
|
+
onProgress: p => {
|
|
1392
|
+
entry.progress = p;
|
|
1393
|
+
const el2 = root.querySelector(`[data-upload-id="${CSS.escape(entry.id)}"]`);
|
|
1394
|
+
if (el2) updateUploadItemEl(el2, entry);
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
entry.state = 'done';
|
|
1399
|
+
entry.key = result.key || '';
|
|
1400
|
+
const el2 = root.querySelector(`[data-upload-id="${CSS.escape(entry.id)}"]`);
|
|
1401
|
+
if (el2) updateUploadItemEl(el2, entry);
|
|
1402
|
+
|
|
1403
|
+
if (options.onSuccess) options.onSuccess({ file: entry.file, key: entry.key, result });
|
|
1404
|
+
toast.success(`Uploaded: ${entry.file.name}`);
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
entry.state = 'error';
|
|
1407
|
+
entry.error = err.message || 'Upload failed';
|
|
1408
|
+
const el2 = root.querySelector(`[data-upload-id="${CSS.escape(entry.id)}"]`);
|
|
1409
|
+
if (el2) updateUploadItemEl(el2, entry);
|
|
1410
|
+
|
|
1411
|
+
if (options.onError) options.onError({ file: entry.file, error: err });
|
|
1412
|
+
toast.error(`Failed: ${entry.file.name} — ${err.message}`);
|
|
1413
|
+
}
|
|
1414
|
+
updateSummary();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (startBtn) { startBtn.disabled = false; startBtn.textContent = 'Upload All'; }
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
dropzone.addEventListener('click', () => fileInput.click());
|
|
1421
|
+
dropzone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') fileInput.click(); });
|
|
1422
|
+
dropzone.querySelector('strong')?.addEventListener('click', e => {
|
|
1423
|
+
e.stopPropagation();
|
|
1424
|
+
fileInput.click();
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
fileInput.addEventListener('change', () => {
|
|
1428
|
+
if (fileInput.files && fileInput.files.length) {
|
|
1429
|
+
addFiles(fileInput.files);
|
|
1430
|
+
fileInput.value = '';
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('mbkh-drag-over'); });
|
|
1435
|
+
dropzone.addEventListener('dragleave', e => { if (!dropzone.contains(e.relatedTarget)) dropzone.classList.remove('mbkh-drag-over'); });
|
|
1436
|
+
dropzone.addEventListener('drop', e => {
|
|
1437
|
+
e.preventDefault();
|
|
1438
|
+
dropzone.classList.remove('mbkh-drag-over');
|
|
1439
|
+
const files = e.dataTransfer && e.dataTransfer.files;
|
|
1440
|
+
if (files && files.length) addFiles(files);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
root.querySelector('#mbkh-upl-start')?.addEventListener('click', startUploadAll);
|
|
1444
|
+
root.querySelector('#mbkh-upl-clear')?.addEventListener('click', () => {
|
|
1445
|
+
queue = queue.filter(e => e.state !== 'done');
|
|
1446
|
+
listEl.querySelectorAll('.mbkh-upload-item.mbkh-done').forEach(el => el.remove());
|
|
1447
|
+
updateSummary();
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
return {
|
|
1451
|
+
el: root,
|
|
1452
|
+
addFiles: f => addFiles(f instanceof FileList ? f : [f]),
|
|
1453
|
+
clear: () => { queue = []; listEl.innerHTML = ''; updateSummary(); },
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1458
|
+
// Auto-init via data attributes
|
|
1459
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1460
|
+
|
|
1461
|
+
function autoInit() {
|
|
1462
|
+
document.querySelectorAll('input[data-mbk-picker]').forEach(el => {
|
|
1463
|
+
try {
|
|
1464
|
+
createPicker(el, {
|
|
1465
|
+
bucket: el.dataset.mbkBucket || '',
|
|
1466
|
+
prefix: el.dataset.mbkPrefix || '',
|
|
1467
|
+
placeholder: el.dataset.mbkPlaceholder || undefined,
|
|
1468
|
+
preview: el.dataset.mbkPreview || undefined,
|
|
1469
|
+
multiple: el.dataset.mbkMultiple === 'true',
|
|
1470
|
+
});
|
|
1471
|
+
} catch(e) { console.warn('[MBKBucket] Picker init error:', e); }
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
document.querySelectorAll('[data-mbk-uploader]').forEach(el => {
|
|
1475
|
+
try {
|
|
1476
|
+
createUploader(el, {
|
|
1477
|
+
bucket: el.dataset.mbkBucket || '',
|
|
1478
|
+
prefix: el.dataset.mbkPrefix || '',
|
|
1479
|
+
accept: el.dataset.mbkAccept || '',
|
|
1480
|
+
multiple: el.dataset.mbkMultiple !== 'false',
|
|
1481
|
+
});
|
|
1482
|
+
} catch(e) { console.warn('[MBKBucket] Uploader init error:', e); }
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
document.querySelectorAll('[data-mbk-gallery-trigger]').forEach(btn => {
|
|
1486
|
+
const targetSel = btn.dataset.mbkTarget;
|
|
1487
|
+
const previewSel = btn.dataset.mbkPreview;
|
|
1488
|
+
const isMulti = btn.dataset.mbkMultiple === 'true';
|
|
1489
|
+
|
|
1490
|
+
btn.addEventListener('click', async () => {
|
|
1491
|
+
const result = await openGallery({
|
|
1492
|
+
bucket: btn.dataset.mbkBucket || '',
|
|
1493
|
+
prefix: btn.dataset.mbkPrefix || '',
|
|
1494
|
+
multiple: isMulti,
|
|
1495
|
+
});
|
|
1496
|
+
if (result) {
|
|
1497
|
+
if (targetSel) {
|
|
1498
|
+
const target = document.querySelector(targetSel);
|
|
1499
|
+
if (target) {
|
|
1500
|
+
if (Array.isArray(result)) {
|
|
1501
|
+
target.value = result.map(r => r.key).join(',');
|
|
1502
|
+
} else {
|
|
1503
|
+
target.value = result.key;
|
|
1504
|
+
}
|
|
1505
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (previewSel) {
|
|
1509
|
+
const previewEl = document.querySelector(previewSel);
|
|
1510
|
+
if (previewEl) {
|
|
1511
|
+
const firstKey = Array.isArray(result) ? result[0]?.key : result.key;
|
|
1512
|
+
if (firstKey) {
|
|
1513
|
+
const viewUrl = url.view(firstKey, btn.dataset.mbkBucket);
|
|
1514
|
+
if (previewEl.tagName.toLowerCase() === 'img') {
|
|
1515
|
+
previewEl.src = viewUrl;
|
|
1516
|
+
} else {
|
|
1517
|
+
previewEl.style.backgroundImage = `url("${viewUrl}")`;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1528
|
+
// Public API
|
|
1529
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1530
|
+
|
|
1531
|
+
const MBKBucket = {
|
|
1532
|
+
version: '1.2.0',
|
|
1533
|
+
openGallery,
|
|
1534
|
+
createPicker,
|
|
1535
|
+
createUploader,
|
|
1536
|
+
url,
|
|
1537
|
+
api,
|
|
1538
|
+
toast,
|
|
1539
|
+
init: autoInit,
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
global.MBKBucket = MBKBucket;
|
|
1543
|
+
|
|
1544
|
+
if (document.readyState === 'loading') {
|
|
1545
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
1546
|
+
} else {
|
|
1547
|
+
autoInit();
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
})(window);
|