import { announce } from "./core.js"; const tooltip = document.getElementById("control-unavailable-tooltip"); const toast = document.getElementById("control-feedback-toast"); const controlRecords = new WeakMap(); let descriptionSequence = 0; let toastTimer = null; function recordFor(control) { if (!controlRecords.has(control)) { controlRecords.set(control, { label: "textContent" in control ? control.textContent : "", description: null, describedBy: control.getAttribute("aria-describedby") || "", }); } return controlRecords.get(control); } function removeDescription(control) { const record = recordFor(control); record.description?.remove(); record.description = null; if (record.describedBy) control.setAttribute("aria-describedby", record.describedBy); else control.removeAttribute("aria-describedby"); } function setDescription(control, reason) { const record = recordFor(control); if (!record.description) { const description = document.createElement("span"); description.id = `control-unavailable-reason-${++descriptionSequence}`; description.className = "sr-only"; control.insertAdjacentElement("afterend", description); record.description = description; } record.description.textContent = reason; const ids = [record.describedBy, record.description.id].filter(Boolean).join(" "); control.setAttribute("aria-describedby", ids); } function restoreLabel(control) { const record = recordFor(control); if ("textContent" in control && control.dataset.busyLabelApplied === "true") { control.textContent = record.label; } delete control.dataset.busyLabelApplied; } function clearBusy(control) { restoreLabel(control); if ("disabled" in control) control.disabled = false; control.removeAttribute("aria-busy"); delete control.dataset.controlState; } export function setControlReady(control) { clearBusy(control); removeDescription(control); control.removeAttribute("aria-disabled"); delete control.dataset.unavailableReason; } export function setControlUnavailable(control, reason) { if (!reason || !String(reason).trim()) throw new Error("不可用控件必须提供原因"); clearBusy(control); control.setAttribute("aria-disabled", "true"); control.dataset.unavailableReason = String(reason).trim(); setDescription(control, control.dataset.unavailableReason); } export function setControlBusy(control, { label = "", message = "" } = {}) { const record = recordFor(control); const wasBusy = control.dataset.controlState === "busy"; removeDescription(control); control.removeAttribute("aria-disabled"); delete control.dataset.unavailableReason; control.dataset.controlState = "busy"; control.setAttribute("aria-busy", "true"); if (label && "textContent" in control) { if (control.dataset.busyLabelApplied !== "true") record.label = control.textContent; control.textContent = label; control.dataset.busyLabelApplied = "true"; } if ("disabled" in control) control.disabled = true; if (message && !wasBusy) announce(message); } export async function runControlAction(control, options, action) { setControlBusy(control, options); try { return await action(); } finally { setControlReady(control); } } function unavailableControl(target) { return target instanceof Element ? target.closest('[aria-disabled="true"][data-unavailable-reason]') : null; } function positionTooltip(control, text = control.dataset.unavailableReason) { if (!tooltip || !text) return; tooltip.textContent = text; tooltip.hidden = false; const controlRect = control.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); const margin = 10; const left = Math.min( window.innerWidth - tooltipRect.width - margin, Math.max(margin, controlRect.left + (controlRect.width - tooltipRect.width) / 2), ); let top = controlRect.top - tooltipRect.height - margin; if (top < margin) top = controlRect.bottom + margin; tooltip.style.left = `${Math.round(left)}px`; tooltip.style.top = `${Math.round(top)}px`; } function overflowTooltipTarget(target) { return target instanceof Element ? target.closest("[data-hover-tooltip]") : null; } function canShowOverflowTooltip(target, event) { return event.pointerType === "mouse" && window.matchMedia("(hover: hover) and (pointer: fine)").matches && target.scrollWidth > target.clientWidth; } function hideTooltip() { if (tooltip) tooltip.hidden = true; } function showToast(reason) { if (!toast) return; window.clearTimeout(toastTimer); toast.textContent = reason; toast.hidden = false; toastTimer = window.setTimeout(() => { toast.hidden = true; }, 4000); } document.addEventListener("pointerover", (event) => { const control = unavailableControl(event.target); if (control) { positionTooltip(control); return; } const overflowTarget = overflowTooltipTarget(event.target); if (overflowTarget && canShowOverflowTooltip(overflowTarget, event)) { positionTooltip(overflowTarget, overflowTarget.dataset.hoverTooltip); } }); document.addEventListener("pointerout", (event) => { const control = unavailableControl(event.target); if (control && !control.contains(event.relatedTarget)) { hideTooltip(); return; } const overflowTarget = overflowTooltipTarget(event.target); if (overflowTarget && !overflowTarget.contains(event.relatedTarget)) hideTooltip(); }); document.addEventListener("focusin", (event) => { const control = unavailableControl(event.target); if (control) positionTooltip(control); }); document.addEventListener("focusout", (event) => { const control = unavailableControl(event.target); if (control) hideTooltip(); }); document.addEventListener("click", (event) => { const control = unavailableControl(event.target); if (!control) return; event.preventDefault(); event.stopImmediatePropagation(); const reason = control.dataset.unavailableReason; hideTooltip(); showToast(reason); announce(reason); }, true);