@idds/js 1.0.57 → 1.0.58

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.
@@ -25,11 +25,13 @@ var InaUI = (() => {
25
25
  initCheckbox: () => initCheckbox,
26
26
  initDatepicker: () => initDatepicker,
27
27
  initDropdown: () => initDropdown,
28
+ initFileUpload: () => initFileUpload,
28
29
  initFileUploadBase: () => initFileUploadBase,
29
30
  initFileUploadItem: () => initFileUploadItem,
30
31
  initImgCompare: () => initImgCompare,
31
32
  initModal: () => initModal,
32
33
  initRangeDatepicker: () => initRangeDatepicker,
34
+ initSingleFileUpload: () => initSingleFileUpload,
33
35
  initTab: () => initTab,
34
36
  initTimepicker: () => initTimepicker,
35
37
  initToggle: () => initToggle,
@@ -971,6 +973,386 @@ var InaUI = (() => {
971
973
  });
972
974
  }
973
975
 
976
+ // src/js/components/stateful/file-upload.js
977
+ var ICONS = {
978
+ upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="16"></line></svg>`,
979
+ file: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
980
+ trash: `<svg width="20" height="20" 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"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
981
+ check: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>`,
982
+ error: `<svg width="20" height="20" 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"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`,
983
+ loader: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${PREFIX}-file-upload__file-icon--spinning"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg>`
984
+ };
985
+ function initFileUpload(rootSelector = `.${PREFIX}-file-upload`) {
986
+ const fileUploads = document.querySelectorAll(rootSelector);
987
+ fileUploads.forEach((container) => {
988
+ if (container.__inaFileUploadInitialized) return;
989
+ const input = container.querySelector(`.${PREFIX}-file-upload__input`);
990
+ const dropzone = container.querySelector(
991
+ `.${PREFIX}-file-upload__dropzone`
992
+ );
993
+ let filesContainer = container.querySelector(
994
+ `.${PREFIX}-file-upload__files`
995
+ );
996
+ if (!filesContainer) {
997
+ filesContainer = document.createElement("div");
998
+ filesContainer.className = `${PREFIX}-file-upload__files`;
999
+ container.appendChild(filesContainer);
1000
+ }
1001
+ let errorsContainer = container.querySelector(
1002
+ `.${PREFIX}-file-upload__errors`
1003
+ );
1004
+ if (!errorsContainer) {
1005
+ errorsContainer = document.createElement("div");
1006
+ errorsContainer.className = `${PREFIX}-file-upload__errors`;
1007
+ container.appendChild(errorsContainer);
1008
+ }
1009
+ if (!input || !dropzone) return;
1010
+ const maxFiles = parseInt(container.getAttribute("data-max-files")) || 0;
1011
+ const maxSize = parseInt(container.getAttribute("data-max-size")) || 0;
1012
+ const allowedExtensions = (container.getAttribute("data-allowed-extensions") || "").split(",").filter(Boolean);
1013
+ const multiple = input.hasAttribute("multiple");
1014
+ let uploadedFiles = [];
1015
+ const formatFileSize = (bytes) => {
1016
+ if (bytes === 0) return "0 Bytes";
1017
+ const k = 1024;
1018
+ const sizes = ["Bytes", "KB", "MB", "GB"];
1019
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1020
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
1021
+ };
1022
+ const generateId = () => Math.random().toString(36).substr(2, 9);
1023
+ const validateFile = (file) => {
1024
+ if (allowedExtensions.length > 0) {
1025
+ const ext = file.name.split(".").pop().toLowerCase();
1026
+ if (!allowedExtensions.includes(ext.toLowerCase())) {
1027
+ return {
1028
+ valid: false,
1029
+ error: `Ekstensi file harus: ${allowedExtensions.join(", ")}`
1030
+ };
1031
+ }
1032
+ }
1033
+ if (maxSize > 0 && file.size > maxSize) {
1034
+ return {
1035
+ valid: false,
1036
+ error: `Ukuran file maksimal ${formatFileSize(maxSize)}`
1037
+ };
1038
+ }
1039
+ return { valid: true };
1040
+ };
1041
+ const renderFiles = () => {
1042
+ filesContainer.innerHTML = "";
1043
+ errorsContainer.innerHTML = "";
1044
+ uploadedFiles.forEach((f, index) => {
1045
+ const fileEl = document.createElement("div");
1046
+ fileEl.className = `${PREFIX}-file-upload__file`;
1047
+ let statusClass = "";
1048
+ let iconHtml = "";
1049
+ if (f.status === "uploading") {
1050
+ statusClass = `${PREFIX}-file-upload__file--uploading`;
1051
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--uploading">${ICONS.loader}</div>`;
1052
+ } else if (f.status === "success") {
1053
+ statusClass = `${PREFIX}-file-upload__file--success`;
1054
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--success">${ICONS.check}</div>`;
1055
+ } else if (f.status === "error") {
1056
+ statusClass = `${PREFIX}-file-upload__file--error`;
1057
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--error">${ICONS.error}</div>`;
1058
+ } else {
1059
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--success">${ICONS.check}</div>`;
1060
+ }
1061
+ if (statusClass) fileEl.classList.add(statusClass);
1062
+ fileEl.innerHTML = `
1063
+ <div class="${PREFIX}-file-upload__file-indicator">
1064
+ ${iconHtml}
1065
+ </div>
1066
+ <div class="${PREFIX}-file-upload__file-info">
1067
+ <div class="${PREFIX}-file-upload__file-name">${f.file.name}</div>
1068
+ <div class="${PREFIX}-file-upload__file-size">${formatFileSize(f.file.size)}</div>
1069
+ ${f.error ? `<div class="${PREFIX}-file-upload__file-error">${f.error}</div>` : ""}
1070
+ </div>
1071
+ <div class="${PREFIX}-file-upload__file-actions">
1072
+ <button type="button" class="${PREFIX}-file-upload__file-remove" data-id="${f.id}" title="Hapus file">
1073
+ ${ICONS.trash}
1074
+ </button>
1075
+ </div>
1076
+ `;
1077
+ filesContainer.appendChild(fileEl);
1078
+ });
1079
+ filesContainer.querySelectorAll(`.${PREFIX}-file-upload__file-remove`).forEach((btn) => {
1080
+ btn.addEventListener("click", (e) => {
1081
+ e.stopPropagation();
1082
+ const id = btn.getAttribute("data-id");
1083
+ removeFile(id);
1084
+ });
1085
+ });
1086
+ };
1087
+ const addFiles = (newFiles) => {
1088
+ const validNewFiles = [];
1089
+ const errors = [];
1090
+ Array.from(newFiles).forEach((file) => {
1091
+ const validation = validateFile(file);
1092
+ if (!validation.valid) {
1093
+ errors.push({ file, error: validation.error });
1094
+ } else {
1095
+ validNewFiles.push({
1096
+ file,
1097
+ status: "idle",
1098
+ // In vanilla we might simulate upload or just set success immediately
1099
+ id: generateId()
1100
+ });
1101
+ }
1102
+ });
1103
+ if (multiple && maxFiles > 0) {
1104
+ if (uploadedFiles.length + validNewFiles.length > maxFiles) {
1105
+ const spaceLeft = maxFiles - uploadedFiles.length;
1106
+ if (spaceLeft > 0) {
1107
+ validNewFiles.splice(spaceLeft);
1108
+ } else {
1109
+ validNewFiles.length = 0;
1110
+ }
1111
+ }
1112
+ }
1113
+ if (!multiple) {
1114
+ uploadedFiles = validNewFiles.slice(0, 1);
1115
+ } else {
1116
+ uploadedFiles = [...uploadedFiles, ...validNewFiles];
1117
+ }
1118
+ errorsContainer.innerHTML = "";
1119
+ errors.forEach((err) => {
1120
+ const errEl = document.createElement("div");
1121
+ errEl.className = `${PREFIX}-file-upload__error-message`;
1122
+ errEl.textContent = `${err.file.name}: ${err.error}`;
1123
+ errorsContainer.appendChild(errEl);
1124
+ });
1125
+ renderFiles();
1126
+ container.dispatchEvent(
1127
+ new CustomEvent("file-upload:change", {
1128
+ detail: {
1129
+ files: uploadedFiles.map((f) => f.file),
1130
+ errors
1131
+ },
1132
+ bubbles: true
1133
+ })
1134
+ );
1135
+ validNewFiles.forEach((f) => {
1136
+ f.status = "uploading";
1137
+ });
1138
+ renderFiles();
1139
+ setTimeout(() => {
1140
+ validNewFiles.forEach((f) => {
1141
+ f.status = "success";
1142
+ });
1143
+ renderFiles();
1144
+ }, 1e3);
1145
+ };
1146
+ const removeFile = (id) => {
1147
+ uploadedFiles = uploadedFiles.filter((f) => f.id !== id);
1148
+ renderFiles();
1149
+ input.value = "";
1150
+ };
1151
+ dropzone.addEventListener("click", () => {
1152
+ if (!input.disabled) input.click();
1153
+ });
1154
+ input.addEventListener("change", (e) => {
1155
+ if (input.files.length > 0) {
1156
+ addFiles(input.files);
1157
+ }
1158
+ });
1159
+ ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
1160
+ dropzone.addEventListener(eventName, (e) => {
1161
+ e.preventDefault();
1162
+ e.stopPropagation();
1163
+ });
1164
+ });
1165
+ dropzone.addEventListener("dragover", () => {
1166
+ if (!input.disabled)
1167
+ dropzone.classList.add(`${PREFIX}-file-upload__dropzone--drag-over`);
1168
+ });
1169
+ dropzone.addEventListener("dragleave", () => {
1170
+ dropzone.classList.remove(`${PREFIX}-file-upload__dropzone--drag-over`);
1171
+ });
1172
+ dropzone.addEventListener("drop", (e) => {
1173
+ dropzone.classList.remove(`${PREFIX}-file-upload__dropzone--drag-over`);
1174
+ if (input.disabled) return;
1175
+ const dt = e.dataTransfer;
1176
+ const files = dt.files;
1177
+ if (files.length > 0) {
1178
+ addFiles(files);
1179
+ }
1180
+ });
1181
+ container.__inaFileUploadInitialized = true;
1182
+ });
1183
+ }
1184
+
1185
+ // src/js/components/stateful/single-file-upload.js
1186
+ var ICONS2 = {
1187
+ upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="16"></line></svg>`,
1188
+ trash: `<svg width="18" height="18" 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"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
1189
+ file: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
1190
+ pdf: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M10 13a1 1 0 0 0-1 1v4"></path><path d="M10 13h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1"></path><path d="M14 13h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1v-2"></path><path d="M14 15h2"></path></svg>`,
1191
+ image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`
1192
+ };
1193
+ function initSingleFileUpload(rootSelector = `.${PREFIX}-single-file-upload`) {
1194
+ const fileUploads = document.querySelectorAll(rootSelector);
1195
+ fileUploads.forEach((container) => {
1196
+ if (container.__inaSingleFileUploadInitialized) return;
1197
+ const input = container.querySelector(
1198
+ `.${PREFIX}-single-file-upload__input`
1199
+ );
1200
+ const containerEl = container.querySelector(
1201
+ `.${PREFIX}-single-file-upload__container`
1202
+ );
1203
+ if (!input || !containerEl) return;
1204
+ const titleEl = container.querySelector(
1205
+ `.${PREFIX}-single-file-upload__title`
1206
+ );
1207
+ const descriptionEl = container.querySelector(
1208
+ `.${PREFIX}-single-file-upload__description`
1209
+ );
1210
+ const initialTitle = titleEl ? titleEl.textContent : "Unggah File";
1211
+ const initialDescription = descriptionEl ? descriptionEl.textContent : "";
1212
+ const maxSize = parseInt(container.getAttribute("data-max-size")) || 0;
1213
+ const allowedExtensions = (container.getAttribute("data-allowed-extensions") || "").split(",").filter(Boolean);
1214
+ let status = "idle";
1215
+ let currentFile = null;
1216
+ let progress = 0;
1217
+ const getFileIcon = (file) => {
1218
+ if (file.type.includes("pdf")) return ICONS2.pdf;
1219
+ if (file.type.includes("image")) return ICONS2.image;
1220
+ return ICONS2.file;
1221
+ };
1222
+ const formatFileSize = (bytes) => {
1223
+ return (bytes / (1024 * 1024)).toFixed(2) + " MB";
1224
+ };
1225
+ const updateUI = () => {
1226
+ containerEl.innerHTML = "";
1227
+ if (!currentFile && status === "idle") {
1228
+ containerEl.innerHTML = `
1229
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--default">
1230
+ ${ICONS2.upload}
1231
+ </div>
1232
+ <div class="${PREFIX}-single-file-upload__content">
1233
+ <div class="${PREFIX}-single-file-upload__title">${initialTitle}</div>
1234
+ <div class="${PREFIX}-single-file-upload__description">${initialDescription}</div>
1235
+ </div>
1236
+ `;
1237
+ } else if (!currentFile && status === "uploading") {
1238
+ containerEl.innerHTML = `
1239
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--default">
1240
+ ${ICONS2.upload}
1241
+ </div>
1242
+ <div class="${PREFIX}-single-file-upload__progress">
1243
+ <div class="${PREFIX}-single-file-upload__progress-bar">
1244
+ <div class="${PREFIX}-single-file-upload__progress-fill" style="width: ${progress}%"></div>
1245
+ </div>
1246
+ <div class="${PREFIX}-single-file-upload__progress-text">
1247
+ Uploading... ${progress}%
1248
+ </div>
1249
+ </div>
1250
+ `;
1251
+ } else if (currentFile && status === "success") {
1252
+ containerEl.innerHTML = `
1253
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--file">
1254
+ ${getFileIcon(currentFile)}
1255
+ </div>
1256
+ <div class="${PREFIX}-single-file-upload__content">
1257
+ <div class="${PREFIX}-single-file-upload__title">${currentFile.name}</div>
1258
+ <div class="${PREFIX}-single-file-upload__description">${initialDescription}</div>
1259
+ </div>
1260
+ <button type="button" class="${PREFIX}-single-file-upload__delete-button" aria-label="Remove file">
1261
+ ${ICONS2.trash}
1262
+ </button>
1263
+ `;
1264
+ const deleteBtn = containerEl.querySelector(
1265
+ `.${PREFIX}-single-file-upload__delete-button`
1266
+ );
1267
+ if (deleteBtn) {
1268
+ deleteBtn.addEventListener("click", (e) => {
1269
+ e.stopPropagation();
1270
+ removeFile();
1271
+ });
1272
+ }
1273
+ }
1274
+ };
1275
+ const handleFile = (file) => {
1276
+ if (maxSize > 0 && file.size > maxSize) {
1277
+ alert("File size exceeds limit.");
1278
+ return;
1279
+ }
1280
+ if (allowedExtensions.length > 0) {
1281
+ const ext = file.name.split(".").pop();
1282
+ if (!allowedExtensions.includes(ext)) {
1283
+ alert("Invalid file extension.");
1284
+ return;
1285
+ }
1286
+ }
1287
+ currentFile = file;
1288
+ status = "uploading";
1289
+ progress = 0;
1290
+ updateUI();
1291
+ const interval = setInterval(() => {
1292
+ progress += 10;
1293
+ updateUI();
1294
+ if (progress >= 100) {
1295
+ clearInterval(interval);
1296
+ status = "success";
1297
+ updateUI();
1298
+ container.dispatchEvent(
1299
+ new CustomEvent("single-file-upload:change", {
1300
+ detail: { file },
1301
+ bubbles: true
1302
+ })
1303
+ );
1304
+ }
1305
+ }, 100);
1306
+ };
1307
+ const removeFile = () => {
1308
+ currentFile = null;
1309
+ status = "idle";
1310
+ input.value = "";
1311
+ updateUI();
1312
+ container.dispatchEvent(
1313
+ new CustomEvent("single-file-upload:change", {
1314
+ detail: { file: null },
1315
+ bubbles: true
1316
+ })
1317
+ );
1318
+ };
1319
+ input.addEventListener("change", () => {
1320
+ if (input.files[0]) handleFile(input.files[0]);
1321
+ });
1322
+ containerEl.addEventListener("click", (e) => {
1323
+ if (status === "idle") input.click();
1324
+ });
1325
+ ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
1326
+ containerEl.addEventListener(eventName, (e) => {
1327
+ e.preventDefault();
1328
+ e.stopPropagation();
1329
+ });
1330
+ });
1331
+ containerEl.addEventListener("dragover", () => {
1332
+ if (!currentFile && !input.disabled) {
1333
+ containerEl.classList.add(
1334
+ `${PREFIX}-single-file-upload__container--active`
1335
+ );
1336
+ }
1337
+ });
1338
+ containerEl.addEventListener("dragleave", () => {
1339
+ containerEl.classList.remove(
1340
+ `${PREFIX}-single-file-upload__container--active`
1341
+ );
1342
+ });
1343
+ containerEl.addEventListener("drop", (e) => {
1344
+ containerEl.classList.remove(
1345
+ `${PREFIX}-single-file-upload__container--active`
1346
+ );
1347
+ if (input.disabled || currentFile) return;
1348
+ const file = e.dataTransfer.files[0];
1349
+ if (file) handleFile(file);
1350
+ });
1351
+ updateUI();
1352
+ container.__inaSingleFileUploadInitialized = true;
1353
+ });
1354
+ }
1355
+
974
1356
  // src/js/components/stateful/file-upload-base.js
975
1357
  function initFileUploadBase(rootSelector = `.${PREFIX}-file-base`) {
976
1358
  document.querySelectorAll(rootSelector).forEach((fileUploadBase) => {
@@ -1841,6 +2223,8 @@ var InaUI = (() => {
1841
2223
  initCheckbox();
1842
2224
  initDatepicker();
1843
2225
  initDropdown();
2226
+ initFileUpload();
2227
+ initSingleFileUpload();
1844
2228
  initFileUploadBase();
1845
2229
  initFileUploadItem();
1846
2230
  initImgCompare();
package/dist/index.js CHANGED
@@ -976,6 +976,386 @@ function initDropdown(rootSelector = `.${PREFIX}-dropdown`) {
976
976
  });
977
977
  }
978
978
 
979
+ // src/js/components/stateful/file-upload.js
980
+ var ICONS = {
981
+ upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="16"></line></svg>`,
982
+ file: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
983
+ trash: `<svg width="20" height="20" 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"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
984
+ check: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>`,
985
+ error: `<svg width="20" height="20" 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"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`,
986
+ loader: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${PREFIX}-file-upload__file-icon--spinning"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg>`
987
+ };
988
+ function initFileUpload(rootSelector = `.${PREFIX}-file-upload`) {
989
+ const fileUploads = document.querySelectorAll(rootSelector);
990
+ fileUploads.forEach((container) => {
991
+ if (container.__inaFileUploadInitialized) return;
992
+ const input = container.querySelector(`.${PREFIX}-file-upload__input`);
993
+ const dropzone = container.querySelector(
994
+ `.${PREFIX}-file-upload__dropzone`
995
+ );
996
+ let filesContainer = container.querySelector(
997
+ `.${PREFIX}-file-upload__files`
998
+ );
999
+ if (!filesContainer) {
1000
+ filesContainer = document.createElement("div");
1001
+ filesContainer.className = `${PREFIX}-file-upload__files`;
1002
+ container.appendChild(filesContainer);
1003
+ }
1004
+ let errorsContainer = container.querySelector(
1005
+ `.${PREFIX}-file-upload__errors`
1006
+ );
1007
+ if (!errorsContainer) {
1008
+ errorsContainer = document.createElement("div");
1009
+ errorsContainer.className = `${PREFIX}-file-upload__errors`;
1010
+ container.appendChild(errorsContainer);
1011
+ }
1012
+ if (!input || !dropzone) return;
1013
+ const maxFiles = parseInt(container.getAttribute("data-max-files")) || 0;
1014
+ const maxSize = parseInt(container.getAttribute("data-max-size")) || 0;
1015
+ const allowedExtensions = (container.getAttribute("data-allowed-extensions") || "").split(",").filter(Boolean);
1016
+ const multiple = input.hasAttribute("multiple");
1017
+ let uploadedFiles = [];
1018
+ const formatFileSize = (bytes) => {
1019
+ if (bytes === 0) return "0 Bytes";
1020
+ const k = 1024;
1021
+ const sizes = ["Bytes", "KB", "MB", "GB"];
1022
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1023
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
1024
+ };
1025
+ const generateId = () => Math.random().toString(36).substr(2, 9);
1026
+ const validateFile = (file) => {
1027
+ if (allowedExtensions.length > 0) {
1028
+ const ext = file.name.split(".").pop().toLowerCase();
1029
+ if (!allowedExtensions.includes(ext.toLowerCase())) {
1030
+ return {
1031
+ valid: false,
1032
+ error: `Ekstensi file harus: ${allowedExtensions.join(", ")}`
1033
+ };
1034
+ }
1035
+ }
1036
+ if (maxSize > 0 && file.size > maxSize) {
1037
+ return {
1038
+ valid: false,
1039
+ error: `Ukuran file maksimal ${formatFileSize(maxSize)}`
1040
+ };
1041
+ }
1042
+ return { valid: true };
1043
+ };
1044
+ const renderFiles = () => {
1045
+ filesContainer.innerHTML = "";
1046
+ errorsContainer.innerHTML = "";
1047
+ uploadedFiles.forEach((f, index) => {
1048
+ const fileEl = document.createElement("div");
1049
+ fileEl.className = `${PREFIX}-file-upload__file`;
1050
+ let statusClass = "";
1051
+ let iconHtml = "";
1052
+ if (f.status === "uploading") {
1053
+ statusClass = `${PREFIX}-file-upload__file--uploading`;
1054
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--uploading">${ICONS.loader}</div>`;
1055
+ } else if (f.status === "success") {
1056
+ statusClass = `${PREFIX}-file-upload__file--success`;
1057
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--success">${ICONS.check}</div>`;
1058
+ } else if (f.status === "error") {
1059
+ statusClass = `${PREFIX}-file-upload__file--error`;
1060
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--error">${ICONS.error}</div>`;
1061
+ } else {
1062
+ iconHtml = `<div class="${PREFIX}-file-upload__file-icon-wrapper ${PREFIX}-file-upload__file-icon-wrapper--success">${ICONS.check}</div>`;
1063
+ }
1064
+ if (statusClass) fileEl.classList.add(statusClass);
1065
+ fileEl.innerHTML = `
1066
+ <div class="${PREFIX}-file-upload__file-indicator">
1067
+ ${iconHtml}
1068
+ </div>
1069
+ <div class="${PREFIX}-file-upload__file-info">
1070
+ <div class="${PREFIX}-file-upload__file-name">${f.file.name}</div>
1071
+ <div class="${PREFIX}-file-upload__file-size">${formatFileSize(f.file.size)}</div>
1072
+ ${f.error ? `<div class="${PREFIX}-file-upload__file-error">${f.error}</div>` : ""}
1073
+ </div>
1074
+ <div class="${PREFIX}-file-upload__file-actions">
1075
+ <button type="button" class="${PREFIX}-file-upload__file-remove" data-id="${f.id}" title="Hapus file">
1076
+ ${ICONS.trash}
1077
+ </button>
1078
+ </div>
1079
+ `;
1080
+ filesContainer.appendChild(fileEl);
1081
+ });
1082
+ filesContainer.querySelectorAll(`.${PREFIX}-file-upload__file-remove`).forEach((btn) => {
1083
+ btn.addEventListener("click", (e) => {
1084
+ e.stopPropagation();
1085
+ const id = btn.getAttribute("data-id");
1086
+ removeFile(id);
1087
+ });
1088
+ });
1089
+ };
1090
+ const addFiles = (newFiles) => {
1091
+ const validNewFiles = [];
1092
+ const errors = [];
1093
+ Array.from(newFiles).forEach((file) => {
1094
+ const validation = validateFile(file);
1095
+ if (!validation.valid) {
1096
+ errors.push({ file, error: validation.error });
1097
+ } else {
1098
+ validNewFiles.push({
1099
+ file,
1100
+ status: "idle",
1101
+ // In vanilla we might simulate upload or just set success immediately
1102
+ id: generateId()
1103
+ });
1104
+ }
1105
+ });
1106
+ if (multiple && maxFiles > 0) {
1107
+ if (uploadedFiles.length + validNewFiles.length > maxFiles) {
1108
+ const spaceLeft = maxFiles - uploadedFiles.length;
1109
+ if (spaceLeft > 0) {
1110
+ validNewFiles.splice(spaceLeft);
1111
+ } else {
1112
+ validNewFiles.length = 0;
1113
+ }
1114
+ }
1115
+ }
1116
+ if (!multiple) {
1117
+ uploadedFiles = validNewFiles.slice(0, 1);
1118
+ } else {
1119
+ uploadedFiles = [...uploadedFiles, ...validNewFiles];
1120
+ }
1121
+ errorsContainer.innerHTML = "";
1122
+ errors.forEach((err) => {
1123
+ const errEl = document.createElement("div");
1124
+ errEl.className = `${PREFIX}-file-upload__error-message`;
1125
+ errEl.textContent = `${err.file.name}: ${err.error}`;
1126
+ errorsContainer.appendChild(errEl);
1127
+ });
1128
+ renderFiles();
1129
+ container.dispatchEvent(
1130
+ new CustomEvent("file-upload:change", {
1131
+ detail: {
1132
+ files: uploadedFiles.map((f) => f.file),
1133
+ errors
1134
+ },
1135
+ bubbles: true
1136
+ })
1137
+ );
1138
+ validNewFiles.forEach((f) => {
1139
+ f.status = "uploading";
1140
+ });
1141
+ renderFiles();
1142
+ setTimeout(() => {
1143
+ validNewFiles.forEach((f) => {
1144
+ f.status = "success";
1145
+ });
1146
+ renderFiles();
1147
+ }, 1e3);
1148
+ };
1149
+ const removeFile = (id) => {
1150
+ uploadedFiles = uploadedFiles.filter((f) => f.id !== id);
1151
+ renderFiles();
1152
+ input.value = "";
1153
+ };
1154
+ dropzone.addEventListener("click", () => {
1155
+ if (!input.disabled) input.click();
1156
+ });
1157
+ input.addEventListener("change", (e) => {
1158
+ if (input.files.length > 0) {
1159
+ addFiles(input.files);
1160
+ }
1161
+ });
1162
+ ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
1163
+ dropzone.addEventListener(eventName, (e) => {
1164
+ e.preventDefault();
1165
+ e.stopPropagation();
1166
+ });
1167
+ });
1168
+ dropzone.addEventListener("dragover", () => {
1169
+ if (!input.disabled)
1170
+ dropzone.classList.add(`${PREFIX}-file-upload__dropzone--drag-over`);
1171
+ });
1172
+ dropzone.addEventListener("dragleave", () => {
1173
+ dropzone.classList.remove(`${PREFIX}-file-upload__dropzone--drag-over`);
1174
+ });
1175
+ dropzone.addEventListener("drop", (e) => {
1176
+ dropzone.classList.remove(`${PREFIX}-file-upload__dropzone--drag-over`);
1177
+ if (input.disabled) return;
1178
+ const dt = e.dataTransfer;
1179
+ const files = dt.files;
1180
+ if (files.length > 0) {
1181
+ addFiles(files);
1182
+ }
1183
+ });
1184
+ container.__inaFileUploadInitialized = true;
1185
+ });
1186
+ }
1187
+
1188
+ // src/js/components/stateful/single-file-upload.js
1189
+ var ICONS2 = {
1190
+ upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="16"></line></svg>`,
1191
+ trash: `<svg width="18" height="18" 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"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
1192
+ file: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
1193
+ pdf: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M10 13a1 1 0 0 0-1 1v4"></path><path d="M10 13h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1"></path><path d="M14 13h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1v-2"></path><path d="M14 15h2"></path></svg>`,
1194
+ image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`
1195
+ };
1196
+ function initSingleFileUpload(rootSelector = `.${PREFIX}-single-file-upload`) {
1197
+ const fileUploads = document.querySelectorAll(rootSelector);
1198
+ fileUploads.forEach((container) => {
1199
+ if (container.__inaSingleFileUploadInitialized) return;
1200
+ const input = container.querySelector(
1201
+ `.${PREFIX}-single-file-upload__input`
1202
+ );
1203
+ const containerEl = container.querySelector(
1204
+ `.${PREFIX}-single-file-upload__container`
1205
+ );
1206
+ if (!input || !containerEl) return;
1207
+ const titleEl = container.querySelector(
1208
+ `.${PREFIX}-single-file-upload__title`
1209
+ );
1210
+ const descriptionEl = container.querySelector(
1211
+ `.${PREFIX}-single-file-upload__description`
1212
+ );
1213
+ const initialTitle = titleEl ? titleEl.textContent : "Unggah File";
1214
+ const initialDescription = descriptionEl ? descriptionEl.textContent : "";
1215
+ const maxSize = parseInt(container.getAttribute("data-max-size")) || 0;
1216
+ const allowedExtensions = (container.getAttribute("data-allowed-extensions") || "").split(",").filter(Boolean);
1217
+ let status = "idle";
1218
+ let currentFile = null;
1219
+ let progress = 0;
1220
+ const getFileIcon = (file) => {
1221
+ if (file.type.includes("pdf")) return ICONS2.pdf;
1222
+ if (file.type.includes("image")) return ICONS2.image;
1223
+ return ICONS2.file;
1224
+ };
1225
+ const formatFileSize = (bytes) => {
1226
+ return (bytes / (1024 * 1024)).toFixed(2) + " MB";
1227
+ };
1228
+ const updateUI = () => {
1229
+ containerEl.innerHTML = "";
1230
+ if (!currentFile && status === "idle") {
1231
+ containerEl.innerHTML = `
1232
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--default">
1233
+ ${ICONS2.upload}
1234
+ </div>
1235
+ <div class="${PREFIX}-single-file-upload__content">
1236
+ <div class="${PREFIX}-single-file-upload__title">${initialTitle}</div>
1237
+ <div class="${PREFIX}-single-file-upload__description">${initialDescription}</div>
1238
+ </div>
1239
+ `;
1240
+ } else if (!currentFile && status === "uploading") {
1241
+ containerEl.innerHTML = `
1242
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--default">
1243
+ ${ICONS2.upload}
1244
+ </div>
1245
+ <div class="${PREFIX}-single-file-upload__progress">
1246
+ <div class="${PREFIX}-single-file-upload__progress-bar">
1247
+ <div class="${PREFIX}-single-file-upload__progress-fill" style="width: ${progress}%"></div>
1248
+ </div>
1249
+ <div class="${PREFIX}-single-file-upload__progress-text">
1250
+ Uploading... ${progress}%
1251
+ </div>
1252
+ </div>
1253
+ `;
1254
+ } else if (currentFile && status === "success") {
1255
+ containerEl.innerHTML = `
1256
+ <div class="${PREFIX}-single-file-upload__icon-wrapper ${PREFIX}-single-file-upload__icon-wrapper--file">
1257
+ ${getFileIcon(currentFile)}
1258
+ </div>
1259
+ <div class="${PREFIX}-single-file-upload__content">
1260
+ <div class="${PREFIX}-single-file-upload__title">${currentFile.name}</div>
1261
+ <div class="${PREFIX}-single-file-upload__description">${initialDescription}</div>
1262
+ </div>
1263
+ <button type="button" class="${PREFIX}-single-file-upload__delete-button" aria-label="Remove file">
1264
+ ${ICONS2.trash}
1265
+ </button>
1266
+ `;
1267
+ const deleteBtn = containerEl.querySelector(
1268
+ `.${PREFIX}-single-file-upload__delete-button`
1269
+ );
1270
+ if (deleteBtn) {
1271
+ deleteBtn.addEventListener("click", (e) => {
1272
+ e.stopPropagation();
1273
+ removeFile();
1274
+ });
1275
+ }
1276
+ }
1277
+ };
1278
+ const handleFile = (file) => {
1279
+ if (maxSize > 0 && file.size > maxSize) {
1280
+ alert("File size exceeds limit.");
1281
+ return;
1282
+ }
1283
+ if (allowedExtensions.length > 0) {
1284
+ const ext = file.name.split(".").pop();
1285
+ if (!allowedExtensions.includes(ext)) {
1286
+ alert("Invalid file extension.");
1287
+ return;
1288
+ }
1289
+ }
1290
+ currentFile = file;
1291
+ status = "uploading";
1292
+ progress = 0;
1293
+ updateUI();
1294
+ const interval = setInterval(() => {
1295
+ progress += 10;
1296
+ updateUI();
1297
+ if (progress >= 100) {
1298
+ clearInterval(interval);
1299
+ status = "success";
1300
+ updateUI();
1301
+ container.dispatchEvent(
1302
+ new CustomEvent("single-file-upload:change", {
1303
+ detail: { file },
1304
+ bubbles: true
1305
+ })
1306
+ );
1307
+ }
1308
+ }, 100);
1309
+ };
1310
+ const removeFile = () => {
1311
+ currentFile = null;
1312
+ status = "idle";
1313
+ input.value = "";
1314
+ updateUI();
1315
+ container.dispatchEvent(
1316
+ new CustomEvent("single-file-upload:change", {
1317
+ detail: { file: null },
1318
+ bubbles: true
1319
+ })
1320
+ );
1321
+ };
1322
+ input.addEventListener("change", () => {
1323
+ if (input.files[0]) handleFile(input.files[0]);
1324
+ });
1325
+ containerEl.addEventListener("click", (e) => {
1326
+ if (status === "idle") input.click();
1327
+ });
1328
+ ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
1329
+ containerEl.addEventListener(eventName, (e) => {
1330
+ e.preventDefault();
1331
+ e.stopPropagation();
1332
+ });
1333
+ });
1334
+ containerEl.addEventListener("dragover", () => {
1335
+ if (!currentFile && !input.disabled) {
1336
+ containerEl.classList.add(
1337
+ `${PREFIX}-single-file-upload__container--active`
1338
+ );
1339
+ }
1340
+ });
1341
+ containerEl.addEventListener("dragleave", () => {
1342
+ containerEl.classList.remove(
1343
+ `${PREFIX}-single-file-upload__container--active`
1344
+ );
1345
+ });
1346
+ containerEl.addEventListener("drop", (e) => {
1347
+ containerEl.classList.remove(
1348
+ `${PREFIX}-single-file-upload__container--active`
1349
+ );
1350
+ if (input.disabled || currentFile) return;
1351
+ const file = e.dataTransfer.files[0];
1352
+ if (file) handleFile(file);
1353
+ });
1354
+ updateUI();
1355
+ container.__inaSingleFileUploadInitialized = true;
1356
+ });
1357
+ }
1358
+
979
1359
  // src/js/components/stateful/file-upload-base.js
980
1360
  function initFileUploadBase(rootSelector = `.${PREFIX}-file-base`) {
981
1361
  document.querySelectorAll(rootSelector).forEach((fileUploadBase) => {
@@ -1731,6 +2111,8 @@ if (typeof window !== void 0) {
1731
2111
  initCheckbox();
1732
2112
  initDatepicker();
1733
2113
  initDropdown();
2114
+ initFileUpload();
2115
+ initSingleFileUpload();
1734
2116
  initFileUploadBase();
1735
2117
  initFileUploadItem();
1736
2118
  initImgCompare();
@@ -1751,6 +2133,8 @@ function initAll() {
1751
2133
  initCheckbox();
1752
2134
  initDatepicker();
1753
2135
  initDropdown();
2136
+ initFileUpload();
2137
+ initSingleFileUpload();
1754
2138
  initFileUploadBase();
1755
2139
  initFileUploadItem();
1756
2140
  initImgCompare();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idds/js",
3
- "version": "1.0.57",
3
+ "version": "1.0.58",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },