Security Remediation: All Phases Complete (84 fixes) #348
@@ -53,6 +53,8 @@ export function LinkAutocomplete({
|
|||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const mirrorRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const cursorSpanRef = useRef<HTMLSpanElement | null>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for knowledge entries matching the query.
|
* Search for knowledge entries matching the query.
|
||||||
@@ -132,15 +134,37 @@ export function LinkAutocomplete({
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate dropdown position relative to cursor
|
* Calculate dropdown position relative to cursor.
|
||||||
|
* Uses a persistent off-screen mirror element (via refs) to avoid
|
||||||
|
* creating and removing DOM nodes on every keystroke, which causes
|
||||||
|
* layout thrashing.
|
||||||
*/
|
*/
|
||||||
const calculateDropdownPosition = useCallback(
|
const calculateDropdownPosition = useCallback(
|
||||||
(textarea: HTMLTextAreaElement, cursorIndex: number): { top: number; left: number } => {
|
(textarea: HTMLTextAreaElement, cursorIndex: number): { top: number; left: number } => {
|
||||||
// Create a mirror div to measure text position
|
// Lazily create the mirror element once, then reuse it
|
||||||
const mirror = document.createElement("div");
|
if (!mirrorRef.current) {
|
||||||
const styles = window.getComputedStyle(textarea);
|
const mirror = document.createElement("div");
|
||||||
|
mirror.style.position = "absolute";
|
||||||
|
mirror.style.visibility = "hidden";
|
||||||
|
mirror.style.height = "auto";
|
||||||
|
mirror.style.whiteSpace = "pre-wrap";
|
||||||
|
mirror.style.pointerEvents = "none";
|
||||||
|
document.body.appendChild(mirror);
|
||||||
|
mirrorRef.current = mirror;
|
||||||
|
|
||||||
// Copy relevant styles
|
const span = document.createElement("span");
|
||||||
|
span.textContent = "|";
|
||||||
|
cursorSpanRef.current = span;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mirror = mirrorRef.current;
|
||||||
|
const cursorSpan = cursorSpanRef.current;
|
||||||
|
if (!cursorSpan) {
|
||||||
|
return { top: 0, left: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync styles from the textarea so measurement is accurate
|
||||||
|
const styles = window.getComputedStyle(textarea);
|
||||||
const stylesToCopy = [
|
const stylesToCopy = [
|
||||||
"fontFamily",
|
"fontFamily",
|
||||||
"fontSize",
|
"fontSize",
|
||||||
@@ -161,31 +185,19 @@ export function LinkAutocomplete({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mirror.style.position = "absolute";
|
|
||||||
mirror.style.visibility = "hidden";
|
|
||||||
mirror.style.width = `${String(textarea.clientWidth)}px`;
|
mirror.style.width = `${String(textarea.clientWidth)}px`;
|
||||||
mirror.style.height = "auto";
|
|
||||||
mirror.style.whiteSpace = "pre-wrap";
|
|
||||||
|
|
||||||
// Get text up to cursor
|
// Update content: text before cursor + cursor marker span
|
||||||
const textBeforeCursor = textarea.value.substring(0, cursorIndex);
|
const textBeforeCursor = textarea.value.substring(0, cursorIndex);
|
||||||
mirror.textContent = textBeforeCursor;
|
mirror.textContent = textBeforeCursor;
|
||||||
|
|
||||||
// Create a span for the cursor position
|
|
||||||
const cursorSpan = document.createElement("span");
|
|
||||||
cursorSpan.textContent = "|";
|
|
||||||
mirror.appendChild(cursorSpan);
|
mirror.appendChild(cursorSpan);
|
||||||
|
|
||||||
document.body.appendChild(mirror);
|
|
||||||
|
|
||||||
const textareaRect = textarea.getBoundingClientRect();
|
const textareaRect = textarea.getBoundingClientRect();
|
||||||
const cursorSpanRect = cursorSpan.getBoundingClientRect();
|
const cursorSpanRect = cursorSpan.getBoundingClientRect();
|
||||||
|
|
||||||
const top = cursorSpanRect.top - textareaRect.top + textarea.scrollTop + 20;
|
const top = cursorSpanRect.top - textareaRect.top + textarea.scrollTop + 20;
|
||||||
const left = cursorSpanRect.left - textareaRect.left + textarea.scrollLeft;
|
const left = cursorSpanRect.left - textareaRect.left + textarea.scrollLeft;
|
||||||
|
|
||||||
document.body.removeChild(mirror);
|
|
||||||
|
|
||||||
return { top, left };
|
return { top, left };
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -346,7 +358,8 @@ export function LinkAutocomplete({
|
|||||||
}, [textareaRef, handleInput, handleKeyDown]);
|
}, [textareaRef, handleInput, handleKeyDown]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup timeout and abort in-flight requests on unmount
|
* Cleanup timeout, abort in-flight requests, and remove the
|
||||||
|
* persistent mirror element on unmount
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
@@ -356,6 +369,11 @@ export function LinkAutocomplete({
|
|||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort();
|
||||||
}
|
}
|
||||||
|
if (mirrorRef.current) {
|
||||||
|
document.body.removeChild(mirrorRef.current);
|
||||||
|
mirrorRef.current = null;
|
||||||
|
cursorSpanRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user