1document.addEventListener("DOMContentLoaded", () => { 2 let notificationIndex = 0; 3 4 const showMessage = (message, element, delay = 5000) => { 5 const removeMessage = (notification) => { 6 const animation = notification.animate( 7 { opacity: [1, 0], transform: ["translate(0, 0)", "translate(0, 50px)"] }, 8 { duration: 300, easing: "ease-in" }, 9 ); 10 11 animation.onfinish = () => { 12 notification.remove(); 13 }; 14 }; 15 16 const notification = document.createElement("span"); 17 notification.classList.add("drop-error"); 18 notificationIndex += 1; 19 notification.innerText = message; 20 21 notification.animate( 22 { opacity: [0, 1], transform: ["translate(0, 50px)", "translate(0, 0)"] }, 23 { duration: 300, easing: "ease-out" }, 24 ); 25 26 setTimeout(() => { 27 element.before(notification); 28 29 setTimeout(() => { 30 notificationIndex -= 1; 31 removeMessage(notification); 32 }, delay); 33 }, notificationIndex * 100); 34 }; 35 36 const initOne = (field) => { 37 if (field) { 38 const dropArea = field.parentNode; 39 const button = dropArea.querySelector(".dropzone-btn"); 40 const input = dropArea.querySelector("input[type='file']"); 41 const hiddenInput = dropArea.querySelector("input[type='hidden']"); 42 const filesList = dropArea.querySelector(".files-list"); 43 const label = dropArea.querySelector("label"); 44 const errorMessageEl = dropArea.querySelector(".field-validation-valid"); 45 const tooltip = dropArea.querySelector(".help-text"); 46 47 filesList?.querySelectorAll("li")?.forEach((container) => { 48 if (container.dataset.filePath) { 49 addPathToValue(container.dataset.filePath); 50 } 51 52 addRemoveHandler(container, null, null); 53 }); 54 55 const rawFiles = (input.dataset?.files || "") 56 .split(",") 57 .map((s) => s.trim().toLowerCase()) 58 .filter(Boolean); 59 60 const allowedExtensions = new Set(); 61 const allowedExactMimes = new Set(); 62 const allowedWildcardMimes = new Set(); 63 64 rawFiles.forEach((token) => { 65 if (token.includes("/")) { 66 const [type, subtype] = token.split("/"); 67 if (subtype === "*") { 68 allowedWildcardMimes.add(type); 69 } else { 70 allowedExactMimes.add(token); 71 } 72 } else { 73 const ext = token.startsWith(".") ? token.slice(1) : token; 74 if (ext) allowedExtensions.add(ext); 75 } 76 }); 77 78 const options = { 79 allowedTypesRaw: rawFiles, 80 allowedExtensions, 81 allowedExactMimes, 82 allowedWildcardMimes, 83 maxSizeMB: +input.dataset?.maxSize || 10, 84 maxFileSizeBytes: +input.dataset?.maxFileSize || 10 * 1024 * 1024, 85 maxFiles: input.hasAttribute("multiple") ? +input.dataset?.maxFiles || null : 1, 86 isMultiple: input.hasAttribute("multiple"), 87 }; 88 89 const getExtension = (name) => {
90 const idx = name.lastIndexOf("."); 91 return idx === -1 ? "" : name.slice(idx + 1).toLowerCase(); 92 }; 93 94 function isFileTypeAllowed(file) { 95 const mime = (file.type || "").toLowerCase(); 96 97 if (mime) { 98 if (options.allowedExactMimes.has(mime)) return true; 99 const mt = mime.split("/")[0]; 100 if (options.allowedWildcardMimes.has(mt)) return true; 101 } 102 103 const ext = getExtension(file.name); 104 if (ext && options.allowedExtensions.has(ext)) return true; 105 106 if (options.allowedTypesRaw.length === 0) return true; 107 108 return false; 109 } 110 111 function addPathToValue(path) { 112 if (!hiddenInput) return; 113 114 const values = hiddenInput.value ? hiddenInput.value.split(",") : []; 115 116 if (!values.includes(path)) { 117 values.push(path); 118 } 119 120 hiddenInput.value = values.join(","); 121 } 122 123 let error = false; 124 125 const parentForm = dropArea.closest("form"); 126 127 if (parentForm) { 128 parentForm.addEventListener("reset", () => { 129 dropArea.classList.remove("active"); 130 filesList.innerHTML = ""; 131 input.value = ""; 132 if (hiddenInput) hiddenInput.value = ""; 133 }); 134 } 135 136 button.onclick = () => { 137 if (!error) { 138 input.click(); 139 } 140 }; 141 142 label.addEventListener("click", (event) => { 143 event.preventDefault(); 144 }); 145 146 input.addEventListener("change", () => { 147 const files = Array.from(input.files); 148 handleFiles(files, dropArea); 149 }); 150 151 dropArea.addEventListener("dragenter", (e) => { 152 e.preventDefault(); 153 dropArea.classList.add("active"); 154 }); 155 156 dropArea.addEventListener("dragover", (e) => { 157 e.preventDefault(); 158 dropArea.classList.add("active"); 159 }); 160 161 dropArea.addEventListener("dragleave", (e) => { 162 if (!e.relatedTarget || !dropArea.contains(e.relatedTarget)) { 163 dropArea.classList.remove("active"); 164 } 165 }); 166 167 dropArea.addEventListener("drop", (e) => { 168 e.preventDefault(); 169 let files = Array.from(e.dataTransfer.files); 170 171 if (files.length) { 172 if (!options.isMultiple) { 173 files = files.slice(0, 1); 174 } 175 176 const dataTransfer = new DataTransfer(); 177 files.forEach((file) => dataTransfer.items.add(file)); 178 179 input.files = dataTransfer.files; 180 181 handleFiles(files, dropArea); 182 } 183 }); 184 185 async function handleFiles(files, dropArea) { 186 if (!options.isMultiple) { 187 filesList.innerHTML = ""; 188 if (hiddenInput) hiddenInput.value = ""; 189 } 190 191 const existingFilesCount = options.isMultiple ? Array.from(filesList.children).length : 0; 192 const validFiles = []; 193 194 dropArea.classList.add("active"); 195 196 for (const [index, file] of files.entries()) { 197 // File type check 198 if (!isFileTypeAllowed(file)) { 199 showMessage( 200 `××§×××¥ '${file.name}' ××× × ×פ×ר×× × ×ª××. × ××ª× ×××¢××ת ×§×צ×× ×ס××: ${options.allowedTypesRaw.join( 201 ", ", 202 )} ××××.`, 203 errorMessageEl || tooltip || button, 204 ); 205 continue; 206 } 207 208 // File size check 209 if (file.size > options.maxFileSizeBytes) { 210 showMessage( 211 `××§×××¥ '${file.name}' ×××¨× ×××××× ×××תר (${options.maxSizeMB}MB), ×ש ××××ר ×§×××¥ ×§×× ××תר.`, 212 errorMessageEl || tooltip || button, 213 ); 214 continue; 215 } 216 217 // Duplicate check 218 const existingFiles = Array.from(filesList.children).map((el) => ({ 219 name: el.dataset.fileName, 220 lastModified: +el.dataset.lastModified, 221 })); 222 223 const isDuplicate = 224 existingFiles.some((f) => f.name === file.name && f.lastModified === file.lastModified) || 225 validFiles.some((f) => f.name === file.name && f.lastModified === file.lastModified); 226 227 if (isDuplicate) { 228 showMessage(`×§×××¥ ×פ××: ${file.name}`, errorMessageEl || tooltip || button); 229 continue; 230 } 231 232 // Сhecking the maximum number of files 233 const totalCount = existingFilesCount + validFiles.length; 234 235 if (options.maxFiles && totalCount >= options.maxFiles) { 236 showMessage(`×ספר ×§×צ×× ×קס××××: ${options.maxFiles}`, errorMessageEl || tooltip || button); 237 continue; 238 } 239 240 const path = await uploadFile(file); 241 242 if (!path) continue; 243 244 validFiles.push(file); 245 246 // Make ui 247 248 const fileContainer = document.createElement("li"); 249 250 fileContainer.setAttribute("role", "listitem"); 251 fileContainer.setAttribute("data-file-path", path);
252 fileContainer.dataset.fileName = file.name; 253 fileContainer.dataset.lastModified = file.lastModified; 254 255 fileContainer.innerHTML = ` 256 <span class="file-name">${file.name}</span> 257 <button type="button" class="remove-file" aria-label="Remove file"><span class="remove-icon"></span></button> 258 `; 259 filesList.append(fileContainer); 260 addRemoveHandler(fileContainer, file.name, file.lastModified); 261 262 fileContainer.animate( 263 { 264 opacity: [0, 1], 265 transform: ["translateY(50px)", "translateY(0)"], 266 }, 267 { 268 duration: 400 + index * 100, 269 easing: "ease-out", 270 }, 271 ); 272 } 273 274 const updatedDT = new DataTransfer(); 275 validFiles.forEach((file) => updatedDT.items.add(file)); 276 input.files = updatedDT.files; 277 } 278 279 function addRemoveHandler(container, fileNameToRemove, fileLastModifiedToRemove) { 280 const removeBtn = container.querySelector(".remove-file"); 281 282 removeBtn.onclick = async () => { 283 if (container.dataset.filePath) { 284 await removeUploadedFile(container.dataset.filePath); 285 } 286 287 container.animate( 288 { opacity: [1, 0], transform: ["translateY(0)", "translateY(50px)"] }, 289 { duration: 300, easing: "ease-in" }, 290 ).onfinish = () => container.remove(); 291 292 if (!options.isMultiple) { 293 input.value = ""; 294 if (hiddenInput) hiddenInput.value = ""; 295 return; 296 } 297 298 if (!fileNameToRemove || !fileLastModifiedToRemove) { 299 return; 300 } 301 302 const updatedDT = new DataTransfer(); 303 Array.from(input.files).forEach((f) => { 304 if (!(f.name === fileNameToRemove && f.lastModified === fileLastModifiedToRemove)) { 305 updatedDT.items.add(f); 306 } 307 }); 308 input.files = updatedDT.files; 309 }; 310 } 311 312 async function uploadFile(file) { 313 const formData = new FormData(); 314 formData.append("file", file); 315 formData.append("fieldId", input.id); 316 formData.append("formId", input.dataset.formId); 317 318 try { 319 const response = await fetch("/umbraco/api/UploadApi/UploadFile", { 320 method: "POST", 321 body: formData, 322 }); 323 324 if (!response.ok) { 325 throw new Error(); 326 } 327 328 const data = await response.json(); 329 330 if (data.filePath && hiddenInput) { 331 addPathToValue(data.filePath); 332 } 333 334 return data.filePath; 335 } catch (err) { 336 showMessage("××¢××ת ××§×××¥ × ×ש××", errorMessageEl || tooltip || button); 337 return null; 338 } 339 } 340 341 async function removeUploadedFile(path) { 342 try { 343 const response = await fetch(`/umbraco/api/UploadApi/RemoveUploadedFile?path=${encodeURIComponent(path)}`, { 344 method: "POST", 345 }); 346 347 if (!response.ok) { 348 throw new Error(); 349 } 350 351 if (!hiddenInput) { 352 return; 353 } 354 355 hiddenInput.value = hiddenInput.value 356 .split(",") 357 .filter((x) => x !== path) 358 .join(","); 359 } catch (err) { 360 showMessage("×××קת ××§×××¥ × ×ש××", errorMessageEl || tooltip || button); 361 } 362 } 363 } 364 }; 365 366 const initAll = () => { 367 const nodes = Array.from(document.querySelectorAll(".dropzone-input")); 368 369 nodes?.forEach((el) => { 370 initOne(el); 371 }); 372 }; 373 374 initAll(); 375});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.