(() => { document.querySelectorAll("[data-auto-submit='true']").forEach((element) => { element.addEventListener("change", () => { if (element.form) element.form.submit(); }); }); })(); (() => { const usage = document.getElementById("Usage"); const redirect = document.getElementById("RedirectUris"); const clientType = document.getElementById("ClientType"); if (!usage || !redirect || !clientType) return; const syncRedirectInputState = () => { const usageValue = usage.value; const needsRedirect = usageValue === "web_login" || usageValue === "webhook_outbound"; const requiresConfidential = usageValue === "tenant_api" || usageValue === "send_api" || usageValue === "platform_service" || usageValue === "file_api"; redirect.disabled = !needsRedirect; if (!needsRedirect) redirect.value = ""; const publicOption = clientType.querySelector('option[value="public"]'); if (publicOption) publicOption.disabled = requiresConfidential; if (requiresConfidential) clientType.value = "confidential"; }; usage.addEventListener("change", syncRedirectInputState); syncRedirectInputState(); })(); (() => { const forms = document.querySelectorAll("[data-avatar-upload]"); if (!forms.length) return; const targetSize = 256; const maxOutputBytes = 512 * 1024; const readFileAsImage = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("Invalid image.")); image.src = reader.result; }; reader.onerror = () => reject(new Error("Invalid image.")); reader.readAsDataURL(file); }); const canvasToBlob = (canvas, type, quality) => new Promise((resolve) => { canvas.toBlob(resolve, type, quality); }); const blobToDataUrl = (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); const submitForm = (form) => { HTMLFormElement.prototype.submit.call(form); }; forms.forEach((form) => { const fileInput = form.querySelector("[data-avatar-file]"); const dataUrlInput = form.querySelector("[data-avatar-data-url]"); const contentTypeInput = form.querySelector("[data-avatar-content-type]"); const widthInput = form.querySelector("[data-avatar-width]"); const heightInput = form.querySelector("[data-avatar-height]"); if (!fileInput || !dataUrlInput || !contentTypeInput || !widthInput || !heightInput) return; fileInput.addEventListener("change", async () => { const file = fileInput.files && fileInput.files[0]; if (!file) return; try { const image = await readFileAsImage(file); openCropDialog(form, image, () => { fileInput.value = ""; }, async (cropState) => { const canvas = document.createElement("canvas"); canvas.width = targetSize; canvas.height = targetSize; const context = canvas.getContext("2d"); if (!context) throw new Error("Canvas is not available."); context.drawImage( image, (targetSize - cropState.renderWidth) / 2 + cropState.offsetX, (targetSize - cropState.renderHeight) / 2 + cropState.offsetY, cropState.renderWidth, cropState.renderHeight); let blob = await canvasToBlob(canvas, "image/webp", 0.82); if (!blob) blob = await canvasToBlob(canvas, "image/png"); if (!blob || blob.size > maxOutputBytes) { throw new Error("Image output is too large."); } dataUrlInput.value = await blobToDataUrl(blob); contentTypeInput.value = blob.type || "image/png"; widthInput.value = String(targetSize); heightInput.value = String(targetSize); submitForm(form); }); } catch { fileInput.value = ""; } }); }); function openCropDialog(form, image, onCancel, onSave) { const strings = { title: form.dataset.cropTitle || "Adjust profile image", zoom: form.dataset.cropZoom || "Zoom", save: form.dataset.cropSave || "Save", cancel: form.dataset.cropCancel || "Cancel", error: form.dataset.cropError || "Unable to process this image." }; const dialog = document.createElement("div"); dialog.className = "avatar-crop-modal"; dialog.innerHTML = `
`; dialog.querySelector("#avatar-crop-title").textContent = strings.title; dialog.querySelector(".avatar-crop-slider span").textContent = strings.zoom; dialog.querySelector("[data-avatar-crop-cancel].profile-pill-button").textContent = strings.cancel; dialog.querySelector("[data-avatar-crop-save]").textContent = strings.save; const preview = dialog.querySelector(".avatar-crop-stage img"); const stage = dialog.querySelector(".avatar-crop-stage"); const zoomInput = dialog.querySelector(".avatar-crop-slider input"); const error = dialog.querySelector(".avatar-crop-error"); const state = { zoom: 1, offsetX: 0, offsetY: 0, renderWidth: targetSize, renderHeight: targetSize }; const baseScale = targetSize / Math.min(image.naturalWidth, image.naturalHeight); let dragStart = null; const clamp = () => { state.renderWidth = image.naturalWidth * baseScale * state.zoom; state.renderHeight = image.naturalHeight * baseScale * state.zoom; const maxX = Math.max(0, (state.renderWidth - targetSize) / 2); const maxY = Math.max(0, (state.renderHeight - targetSize) / 2); state.offsetX = Math.max(-maxX, Math.min(maxX, state.offsetX)); state.offsetY = Math.max(-maxY, Math.min(maxY, state.offsetY)); }; const render = () => { clamp(); preview.style.width = `${state.renderWidth}px`; preview.style.height = `${state.renderHeight}px`; preview.style.transform = `translate(-50%, -50%) translate(${state.offsetX}px, ${state.offsetY}px)`; }; const close = () => { dialog.remove(); }; preview.src = image.src; document.body.appendChild(dialog); render(); dialog.querySelectorAll("[data-avatar-crop-cancel]").forEach((button) => { button.addEventListener("click", () => { close(); onCancel(); }); }); zoomInput.addEventListener("input", () => { const previousZoom = state.zoom; state.zoom = Number(zoomInput.value); if (previousZoom > 0) { state.offsetX *= state.zoom / previousZoom; state.offsetY *= state.zoom / previousZoom; } render(); }); stage.addEventListener("pointerdown", (event) => { stage.setPointerCapture(event.pointerId); dragStart = { x: event.clientX, y: event.clientY, offsetX: state.offsetX, offsetY: state.offsetY }; }); stage.addEventListener("pointermove", (event) => { if (!dragStart) return; state.offsetX = dragStart.offsetX + event.clientX - dragStart.x; state.offsetY = dragStart.offsetY + event.clientY - dragStart.y; render(); }); stage.addEventListener("pointerup", () => { dragStart = null; }); stage.addEventListener("pointercancel", () => { dragStart = null; }); dialog.querySelector("[data-avatar-crop-save]").addEventListener("click", async () => { error.hidden = true; try { await onSave({ ...state }); } catch { error.textContent = strings.error; error.hidden = false; } }); } })();