hangwin--mcp-chrome
164 行
4.2 KiB
TypeScript
164 行
4.2 KiB
TypeScript
/**
|
|
* Composable for textarea auto-resize functionality.
|
|
* Automatically adjusts textarea height based on content while respecting min/max constraints.
|
|
*/
|
|
import { ref, watch, nextTick, onMounted, onUnmounted, type Ref } from 'vue';
|
|
|
|
export interface UseTextareaAutoResizeOptions {
|
|
/** Ref to the textarea element */
|
|
textareaRef: Ref<HTMLTextAreaElement | null>;
|
|
/** Ref to the textarea value (for watching changes) */
|
|
value: Ref<string>;
|
|
/** Minimum height in pixels */
|
|
minHeight?: number;
|
|
/** Maximum height in pixels */
|
|
maxHeight?: number;
|
|
}
|
|
|
|
export interface UseTextareaAutoResizeReturn {
|
|
/** Current calculated height */
|
|
height: Ref<number>;
|
|
/** Whether content exceeds max height (textarea is overflowing) */
|
|
isOverflowing: Ref<boolean>;
|
|
/** Manually trigger height recalculation */
|
|
recalculate: () => void;
|
|
}
|
|
|
|
const DEFAULT_MIN_HEIGHT = 50;
|
|
const DEFAULT_MAX_HEIGHT = 200;
|
|
|
|
/**
|
|
* Composable for auto-resizing textarea based on content.
|
|
*
|
|
* Features:
|
|
* - Automatically adjusts height on input
|
|
* - Respects min/max height constraints
|
|
* - Handles width changes (line wrapping affects height)
|
|
* - Uses requestAnimationFrame for performance
|
|
*/
|
|
export function useTextareaAutoResize(
|
|
options: UseTextareaAutoResizeOptions,
|
|
): UseTextareaAutoResizeReturn {
|
|
const {
|
|
textareaRef,
|
|
value,
|
|
minHeight = DEFAULT_MIN_HEIGHT,
|
|
maxHeight = DEFAULT_MAX_HEIGHT,
|
|
} = options;
|
|
|
|
const height = ref<number>(minHeight);
|
|
const isOverflowing = ref(false);
|
|
|
|
let scheduled = false;
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
let lastWidth = 0;
|
|
|
|
/**
|
|
* Calculate textarea height based on content.
|
|
* Only updates the reactive `height` and `isOverflowing` refs.
|
|
* The actual DOM height is controlled via :style binding in the template.
|
|
*/
|
|
function recalculate(): void {
|
|
const el = textareaRef.value;
|
|
if (!el) return;
|
|
|
|
// Temporarily set height to 'auto' to get accurate scrollHeight
|
|
// Save current height to minimize visual flicker
|
|
const currentHeight = el.style.height;
|
|
el.style.height = 'auto';
|
|
|
|
const contentHeight = el.scrollHeight;
|
|
const clampedHeight = Math.min(maxHeight, Math.max(minHeight, contentHeight));
|
|
|
|
// Restore height immediately (the actual height is controlled by Vue binding)
|
|
el.style.height = currentHeight;
|
|
|
|
// Update reactive state
|
|
height.value = clampedHeight;
|
|
// Add small tolerance (1px) to account for rounding
|
|
isOverflowing.value = contentHeight > maxHeight + 1;
|
|
}
|
|
|
|
/**
|
|
* Schedule height recalculation using requestAnimationFrame.
|
|
* Batches multiple calls within the same frame for performance.
|
|
*/
|
|
function scheduleRecalculate(): void {
|
|
if (scheduled) return;
|
|
scheduled = true;
|
|
requestAnimationFrame(() => {
|
|
scheduled = false;
|
|
recalculate();
|
|
});
|
|
}
|
|
|
|
// Watch value changes
|
|
watch(
|
|
value,
|
|
async () => {
|
|
await nextTick();
|
|
scheduleRecalculate();
|
|
},
|
|
{ flush: 'post' },
|
|
);
|
|
|
|
// Watch textarea ref changes (in case it's replaced)
|
|
watch(
|
|
textareaRef,
|
|
async (newEl, oldEl) => {
|
|
// Cleanup old observer
|
|
if (resizeObserver && oldEl) {
|
|
resizeObserver.unobserve(oldEl);
|
|
}
|
|
|
|
if (!newEl) return;
|
|
|
|
await nextTick();
|
|
scheduleRecalculate();
|
|
|
|
// Setup new observer for width changes
|
|
if (resizeObserver) {
|
|
lastWidth = newEl.offsetWidth;
|
|
resizeObserver.observe(newEl);
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
onMounted(() => {
|
|
const el = textareaRef.value;
|
|
if (!el) return;
|
|
|
|
// Initial calculation
|
|
scheduleRecalculate();
|
|
|
|
// Setup ResizeObserver for width changes
|
|
// Width changes affect line wrapping, which affects scrollHeight
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
lastWidth = el.offsetWidth;
|
|
resizeObserver = new ResizeObserver(() => {
|
|
const current = textareaRef.value;
|
|
if (!current) return;
|
|
|
|
const currentWidth = current.offsetWidth;
|
|
if (currentWidth !== lastWidth) {
|
|
lastWidth = currentWidth;
|
|
scheduleRecalculate();
|
|
}
|
|
});
|
|
resizeObserver.observe(el);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
resizeObserver?.disconnect();
|
|
resizeObserver = null;
|
|
});
|
|
|
|
return {
|
|
height,
|
|
isOverflowing,
|
|
recalculate,
|
|
};
|
|
}
|