88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
import { nextTick, onUnmounted, ref, watch, type Ref, type WatchSource } from "vue";
|
|
import { resolveNextEditorToolbarTier, type EditorToolbarTier } from "@/lib/tabs/editorToolbarLayout";
|
|
|
|
/**
|
|
* Measured progressive condensation for single-row toolbars — the same
|
|
* mechanism as the SQL editor toolbar (resolveNextEditorToolbarTier): while
|
|
* the row's content overflows its available width the tier steps up (moving
|
|
* controls into an overflow menu); it only steps back down when there is real
|
|
* slack AND the pane has grown meaningfully since the last condensation, so a
|
|
* static narrow layout can never oscillate.
|
|
*
|
|
* Measurement contract: the toolbar row must be `overflow-hidden` with
|
|
* non-shrinking (shrink-0) or floored (min-w-*) children, so scrollWidth
|
|
* reliably exceeds clientWidth once nothing can shrink any further. Pure
|
|
* `truncate` children without a floor collapse to zero width before the row
|
|
* ever reports overflow, which would silently disable condensation.
|
|
*
|
|
* @param rootRef the toolbar row element
|
|
* @param revalidateSources reactive sources whose change alters the row's
|
|
* visible control set (active tab, feature flags, conditional controls);
|
|
* each change re-measures, because hiding/restoring controls changes the
|
|
* content extent without resizing the toolbar box.
|
|
*/
|
|
export function useToolbarOverflow(rootRef: Ref<HTMLElement | null>, revalidateSources: WatchSource[] = []) {
|
|
const tier = ref<EditorToolbarTier>(0);
|
|
// Available width when the current tier was condensed into; anchors the
|
|
// step-down hysteresis so a static narrow layout cannot oscillate.
|
|
const condensedAtWidth = ref(0);
|
|
let resizeObserver: ResizeObserver | undefined;
|
|
|
|
function measure() {
|
|
const element = rootRef.value;
|
|
if (!element) {
|
|
return;
|
|
}
|
|
const next = resolveNextEditorToolbarTier({
|
|
tier: tier.value,
|
|
availableWidth: element.clientWidth,
|
|
contentWidth: element.scrollWidth,
|
|
condensedAtWidth: condensedAtWidth.value,
|
|
});
|
|
if (next !== tier.value) {
|
|
if (next > tier.value) {
|
|
condensedAtWidth.value = element.clientWidth;
|
|
}
|
|
tier.value = next;
|
|
}
|
|
}
|
|
|
|
// Hiding or restoring controls changes the row content without resizing the
|
|
// toolbar box, so every tier change re-measures until the row settles.
|
|
watch(tier, () => {
|
|
void nextTick(measure);
|
|
});
|
|
|
|
if (revalidateSources.length > 0) {
|
|
watch(revalidateSources, () => {
|
|
void nextTick(measure);
|
|
});
|
|
}
|
|
|
|
watch(
|
|
rootRef,
|
|
(element) => {
|
|
resizeObserver?.disconnect();
|
|
resizeObserver = undefined;
|
|
// A freshly attached row (mode switch, tab remount) starts from the full
|
|
// layout and converges from real measurements instead of inheriting a
|
|
// stale tier from whatever was previously measured.
|
|
tier.value = 0;
|
|
condensedAtWidth.value = 0;
|
|
if (element && typeof ResizeObserver !== "undefined") {
|
|
resizeObserver = new ResizeObserver(measure);
|
|
resizeObserver.observe(element);
|
|
}
|
|
void nextTick(measure);
|
|
},
|
|
{ flush: "post" },
|
|
);
|
|
|
|
onUnmounted(() => {
|
|
resizeObserver?.disconnect();
|
|
resizeObserver = undefined;
|
|
});
|
|
|
|
return { tier, measure };
|
|
}
|