Fix model selection dropdown

This commit is contained in:
dimitrievgs 2025-09-28 23:34:54 +03:00
parent 29cddbcb57
commit 23b4ca4571

View File

@ -4,7 +4,6 @@
*/ */
import { ChatHistoryItem } from './models/ChatHistoryItem'; import { ChatHistoryItem } from './models/ChatHistoryItem';
import { AutocompleteManager } from '../references/AutocompleteManager'; import { AutocompleteManager } from '../references/AutocompleteManager';
import { ReferenceAutocomplete } from '../references/ReferenceAutocomplete'; import { ReferenceAutocomplete } from '../references/ReferenceAutocomplete';
import { ReferenceExpander } from '../references/ReferenceExpander'; import { ReferenceExpander } from '../references/ReferenceExpander';
@ -81,30 +80,35 @@ export class ChatPanel {
// Показываем дропдаун, чтобы получить его размеры и положение // Показываем дропдаун, чтобы получить его размеры и положение
this.modelDropdown.style.display = 'block'; this.modelDropdown.style.display = 'block';
const modelSelectButtonRect = this.modelSelectButton.getBoundingClientRect(); const container = this.modelSelectButton.closest('.chat-input-container') as HTMLElement;
const modelDropdownRect = this.modelDropdown.getBoundingClientRect();
const chatPanelRect = this.container.getBoundingClientRect(); // Общие границы чат-панели
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
// ----------------------------------------------- Секция логики позиционирования дропдауна ---------------------------------------------------- const modelSelectButtonRect = this.modelSelectButton.getBoundingClientRect();
// Позиция относительно контейнера chatPanel (или inputElement) const containerRect = container.getBoundingClientRect(); // Общие границы чат-панели
let top = modelSelectButtonRect.bottom - chatPanelRect.top + 4; // Отступ 4px от кнопки const dropdownRect = this.modelDropdown.getBoundingClientRect();
let left = modelSelectButtonRect.left - chatPanelRect.left; const viewportHeight = window.innerHeight;
// Позиция относительно контейнера chatPanel
// Левый край дропдауна совпадает с левым краем кнопки выбора модели
let left = modelSelectButtonRect.left - containerRect.left - 5;
// Дефолтное положение - под кнопкой
let top = modelSelectButtonRect.height;
// Проверяем, помещается ли dropdown снизу // Проверяем, помещается ли dropdown снизу
const spaceBelow = viewportHeight - modelSelectButtonRect.bottom; const spaceBelow = viewportHeight - modelSelectButtonRect.bottom;
const spaceAbove = modelSelectButtonRect.top; const spaceAbove = modelSelectButtonRect.top;
if (spaceBelow < modelDropdownRect.height + 10 && spaceAbove > modelDropdownRect.height + 10) { if (spaceBelow < dropdownRect.height + 10 && spaceAbove > dropdownRect.height + 10) {
// Если снизу мало места и сверху достаточно, размещаем сверху // Если снизу мало места и сверху достаточно, размещаем сверху
top = modelSelectButtonRect.top - chatPanelRect.top - modelDropdownRect.height - 4; // Отступ 4px от кнопки top = - dropdownRect.height;
} }
// Убедимся, что дропдаун не выходит за границы контейнера справа // Убедимся, что дропдаун не выходит за границы контейнера справа
const maxRight = chatPanelRect.width; const maxRight = containerRect.right - containerRect.left;
if (left + modelDropdownRect.width > maxRight) { if (left + dropdownRect.width > maxRight) {
left = maxRight - modelDropdownRect.width; left = maxRight - dropdownRect.width - 5;
} }
// Убедимся, что дропдаун не выходит за границы контейнера слева // Убедимся, что дропдаун не выходит за границы контейнера слева
if (left < 0) { if (left < 0) {
left = 0; left = 0;
@ -113,7 +117,11 @@ export class ChatPanel {
this.modelDropdown.style.top = `${top}px`; this.modelDropdown.style.top = `${top}px`;
this.modelDropdown.style.left = `${left}px`; this.modelDropdown.style.left = `${left}px`;
this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно
// ----------------------------------------------- End of Секция логики позиционирования дропдауна ---------------------------------------------
// ДОБАВЛЕНО: Устанавливаем минимальную ширину дропдауна, равную ширине кнопки
let minWidth = Math.min(containerRect.width / 2, dropdownRect.width);
this.modelDropdown.style.minWidth = `${minWidth}px`;
this.modelDropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth
} }
updateModelDropdown(): void { updateModelDropdown(): void {
@ -435,232 +443,228 @@ export class ChatPanel {
/** /**
* Заменяет текст ссылки на визуальный бокс * Заменяет текст ссылки на визуальный бокс
*/ */
private replaceReferenceWithBox(match: ReferenceMatch, item: AutocompleteItem): void { private replaceReferenceWithBox(match: ReferenceMatch, item: AutocompleteItem): void {
this.isProcessingReference = true; this.isProcessingReference = true;
const reference: FileReference = { const reference: FileReference = {
type: item.type, type: item.type,
name: item.name, name: item.name,
path: item.path, path: item.path,
permanent: match.prefix.length === 2 // @@ или ## permanent: match.prefix.length === 2 // @@ или ##
}; };
// Создаем бокс // Создаем бокс
const box = ReferenceBox.createBox(reference); const box = ReferenceBox.createBox(reference);
// Используем текущий Range для замены // Используем текущий Range для замены
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
this.isProcessingReference = false; this.isProcessingReference = false;
return; return;
} }
const currentRange = selection.getRangeAt(0);
// Создаем новый диапазон для замены, основываясь на match.startIndex и match.endIndex
// Необходимо получить текстовые узлы, соответствующие этим индексам
const { startNode, startOffset, endNode, endOffset } = this.findNodesAndOffsetsForMatch(match);
if (!startNode || !endNode) { const currentRange = selection.getRangeAt(0);
console.error("Не удалось найти узлы для замены ссылки", match);
this.isProcessingReference = false;
return;
}
const replacementRange = document.createRange(); // Создаем новый диапазон для замены, основываясь на match.startIndex и match.endIndex
replacementRange.setStart(startNode, startOffset); // Необходимо получить текстовые узлы, соответствующие этим индексам
replacementRange.setEnd(endNode, endOffset); const { startNode, startOffset, endNode, endOffset } = this.findNodesAndOffsetsForMatch(match);
replacementRange.deleteContents(); // Удаляем текст ссылки if (!startNode || !endNode) {
replacementRange.insertNode(box); // Вставляем бокс console.error('Не удалось найти узлы для замены ссылки', match);
this.isProcessingReference = false;
return;
}
// Очищаем выделение const replacementRange = document.createRange();
selection.removeAllRanges(); replacementRange.setStart(startNode, startOffset);
replacementRange.setEnd(endNode, endOffset);
// Добавляем ссылку в список текущих ссылок replacementRange.deleteContents(); // Удаляем текст ссылки
this.currentReferences.push(reference); replacementRange.insertNode(box); // Вставляем бокс
this.isProcessingReference = false; // Очищаем выделение
selection.removeAllRanges();
// Вставляем пробел после бокса и перемещаем курсор за него // Добавляем ссылку в список текущих ссылок
this.insertSpaceAndSetCursorAfterElement(box); this.currentReferences.push(reference);
}
/** this.isProcessingReference = false;
* Находит узлы и смещения для заданного match в contenteditable элементе.
* @param match - объект ReferenceMatch.
* @returns Объект с startNode, startOffset, endNode, endOffset.
*/
private findNodesAndOffsetsForMatch(match: ReferenceMatch): {
startNode: Node | null,
startOffset: number,
endNode: Node | null,
endOffset: number
} {
let startNode: Node | null = null;
let startOffset = 0;
let endNode: Node | null = null;
let endOffset = 0;
let currentLength = 0;
const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL, чтобы учитывать и элементы // Вставляем пробел после бокса и перемещаем курсор за него
let node; this.insertSpaceAndSetCursorAfterElement(box);
}
while ((node = walker.nextNode())) { /**
const nodeTextLength = this.getNodeEquivalentLengthForReferenceParsing(node); * Находит узлы и смещения для заданного match в contenteditable элементе.
* @param match - объект ReferenceMatch.
* @returns Объект с startNode, startOffset, endNode, endOffset.
*/
private findNodesAndOffsetsForMatch(match: ReferenceMatch): {
startNode: Node | null;
startOffset: number;
endNode: Node | null;
endOffset: number;
} {
let startNode: Node | null = null;
let startOffset = 0;
let endNode: Node | null = null;
let endOffset = 0;
let currentLength = 0;
// Проверяем, начинается ли ссылка в этом узле const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL, чтобы учитывать и элементы
if (currentLength <= match.startIndex && match.startIndex < currentLength + nodeTextLength) { let node;
startNode = node;
startOffset = match.startIndex - currentLength;
}
// Проверяем, заканчивается ли ссылка в этом узле while ((node = walker.nextNode())) {
if (currentLength <= match.endIndex && match.endIndex <= currentLength + nodeTextLength) { const nodeTextLength = this.getNodeEquivalentLengthForReferenceParsing(node);
endNode = node;
endOffset = match.endIndex - currentLength;
break; // Нашли оба, выходим
}
// Если ссылка охватывает несколько узлов, может быть, endNode еще не найден,
// но мы уже прошли startNode.
if (startNode && !endNode && match.endIndex <= currentLength + nodeTextLength) {
// Это случай, когда endMatch.endIndex может быть на границе или внутри следующего узла
endNode = node;
endOffset = match.endIndex - currentLength;
break;
}
// Проверяем, начинается ли ссылка в этом узле
if (currentLength <= match.startIndex && match.startIndex < currentLength + nodeTextLength) {
startNode = node;
startOffset = match.startIndex - currentLength;
}
currentLength += nodeTextLength; // Проверяем, заканчивается ли ссылка в этом узле
} if (currentLength <= match.endIndex && match.endIndex <= currentLength + nodeTextLength) {
endNode = node;
endOffset = match.endIndex - currentLength;
break; // Нашли оба, выходим
}
// Если endNode не найден, возможно, match.endIndex указывает на конец inputElement // Если ссылка охватывает несколько узлов, может быть, endNode еще не найден,
if (!endNode && startNode && currentLength <= match.endIndex) { // но мы уже прошли startNode.
if (this.messageInput.lastChild) { if (startNode && !endNode && match.endIndex <= currentLength + nodeTextLength) {
endNode = this.messageInput.lastChild; // Это случай, когда endMatch.endIndex может быть на границе или внутри следующего узла
endOffset = this.messageInput.lastChild.textContent?.length || 0; // Или конец всего контента endNode = node;
} endOffset = match.endIndex - currentLength;
} break;
}
currentLength += nodeTextLength;
}
return { startNode, startOffset, endNode, endOffset }; // Если endNode не найден, возможно, match.endIndex указывает на конец inputElement
} if (!endNode && startNode && currentLength <= match.endIndex) {
if (this.messageInput.lastChild) {
endNode = this.messageInput.lastChild;
endOffset = this.messageInput.lastChild.textContent?.length || 0; // Или конец всего контента
}
}
/** return { startNode, startOffset, endNode, endOffset };
* Вычисляет эквивалентную текстовую длину узла для целей парсинга ссылок. }
* Это отличается от getNodeEquivalentLength, так как здесь reference-box уже считается как его текстовое представление,
* а не 0, так как мы ищем где находится match.
* @param node - узел DOM.
* @returns эквивалентная текстовая длина узла.
*/
private getNodeEquivalentLengthForReferenceParsing(node: Node): number {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent?.length || 0;
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) {
const reference = ReferenceBox.extractReference(node as HTMLElement);
if (reference) {
// Если это reference-box, его "текстовая длина" для парсинга - это его исходная @/@@#name
return (reference.permanent ? 2 : 1) + reference.name.length;
}
}
return 0; // Другие элементы не влияют на текстовую длину для этого контекста
}
/** /**
* Вставляет пробел после указанного элемента и устанавливает курсор. * Вычисляет эквивалентную текстовую длину узла для целей парсинга ссылок.
* @param element - HTML элемент, после которого нужно вставить пробел и установить курсор. * Это отличается от getNodeEquivalentLength, так как здесь reference-box уже считается как его текстовое представление,
*/ * а не 0, так как мы ищем где находится match.
private insertSpaceAndSetCursorAfterElement(element: HTMLElement): void { * @param node - узел DOM.
const selection = window.getSelection(); * @returns эквивалентная текстовая длина узла.
if (!selection) return; */
private getNodeEquivalentLengthForReferenceParsing(node: Node): number {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent?.length || 0;
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) {
const reference = ReferenceBox.extractReference(node as HTMLElement);
if (reference) {
// Если это reference-box, его "текстовая длина" для парсинга - это его исходная @/@@#name
return (reference.permanent ? 2 : 1) + reference.name.length;
}
}
return 0; // Другие элементы не влияют на текстовую длину для этого контекста
}
const range = document.createRange(); /**
range.setStartAfter(element); * Вставляет пробел после указанного элемента и устанавливает курсор.
range.collapse(true); // Схлопываем диапазон до одной точки * @param element - HTML элемент, после которого нужно вставить пробел и установить курсор.
*/
private insertSpaceAndSetCursorAfterElement(element: HTMLElement): void {
const selection = window.getSelection();
if (!selection) return;
// Вставляем неразрывный пробел const range = document.createRange();
const spaceNode = document.createTextNode('\u00A0'); range.setStartAfter(element);
range.insertNode(spaceNode); range.collapse(true); // Схлопываем диапазон до одной точки
// Перемещаем курсор после вставленного пробела // Вставляем неразрывный пробел
range.setStartAfter(spaceNode); const spaceNode = document.createTextNode('\u00A0');
range.collapse(true); range.insertNode(spaceNode);
selection.removeAllRanges(); // Перемещаем курсор после вставленного пробела
selection.addRange(range); range.setStartAfter(spaceNode);
} range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
/** /**
* Заменяет текст на HTML элемент * Заменяет текст на HTML элемент
*/ */
private replaceTextWithElement(match: ReferenceMatch, element: HTMLElement): void { private replaceTextWithElement(match: ReferenceMatch, element: HTMLElement): void {
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return; if (!selection || selection.rangeCount === 0) return;
// Находим текстовый узел с совпадением // Находим текстовый узел с совпадением
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(this.messageInput); range.selectNodeContents(this.messageInput);
range.setEnd(selection.focusNode!, selection.focusOffset); // Устанавливаем конец диапазона на текущую позицию курсора range.setEnd(selection.focusNode!, selection.focusOffset); // Устанавливаем конец диапазона на текущую позицию курсора
let textBeforeMatch = ''; let textBeforeMatch = '';
let nodeBeforeMatch: Node | null = null; let nodeBeforeMatch: Node | null = null;
let offsetBeforeMatch = 0; let offsetBeforeMatch = 0;
// Проходим по дочерним элементам до match.startIndex // Проходим по дочерним элементам до match.startIndex
const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_TEXT); const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_TEXT);
let node: Node | null; let node: Node | null;
let currentLength = 0; let currentLength = 0;
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
const nodeText = node.textContent || ''; const nodeText = node.textContent || '';
if (currentLength + nodeText.length >= match.startIndex) { if (currentLength + nodeText.length >= match.startIndex) {
nodeBeforeMatch = node; nodeBeforeMatch = node;
offsetBeforeMatch = match.startIndex - currentLength; offsetBeforeMatch = match.startIndex - currentLength;
break; break;
} }
currentLength += nodeText.length; currentLength += nodeText.length;
} }
if (!nodeBeforeMatch) { if (!nodeBeforeMatch) {
console.warn("Не удалось найти текстовый узел для начала замены."); console.warn('Не удалось найти текстовый узел для начала замены.');
return; return;
} }
// Удаляем диапазон, соответствующий тексту ссылки // Удаляем диапазон, соответствующий тексту ссылки
const deleteRange = document.createRange(); const deleteRange = document.createRange();
deleteRange.setStart(nodeBeforeMatch, offsetBeforeMatch); deleteRange.setStart(nodeBeforeMatch, offsetBeforeMatch);
// Расширяем диапазон удаления до конца 'fullMatch' // Расширяем диапазон удаления до конца 'fullMatch'
let remainingLengthToDelete = match.fullMatch.length; let remainingLengthToDelete = match.fullMatch.length;
let currentNode = nodeBeforeMatch; let currentNode = nodeBeforeMatch;
let currentOffset = offsetBeforeMatch; let currentOffset = offsetBeforeMatch;
while (currentNode && remainingLengthToDelete > 0) { while (currentNode && remainingLengthToDelete > 0) {
const nodeText = currentNode.textContent || ''; const nodeText = currentNode.textContent || '';
const canDelete = Math.min(remainingLengthToDelete, nodeText.length - currentOffset); const canDelete = Math.min(remainingLengthToDelete, nodeText.length - currentOffset);
remainingLengthToDelete -= canDelete; remainingLengthToDelete -= canDelete;
currentOffset += canDelete; currentOffset += canDelete;
if (remainingLengthToDelete > 0) { if (remainingLengthToDelete > 0) {
// Если не всё удалено в текущем узле, переходим к следующему // Если не всё удалено в текущем узле, переходим к следующему
currentNode = walker.nextNode(); currentNode = walker.nextNode();
currentOffset = 0; // Начинаем с начала следующего узла currentOffset = 0; // Начинаем с начала следующего узла
} }
} }
deleteRange.setEnd(currentNode || nodeBeforeMatch, currentOffset); deleteRange.setEnd(currentNode || nodeBeforeMatch, currentOffset);
deleteRange.deleteContents(); deleteRange.deleteContents();
// Вставляем новый элемент на место удаленного текста
deleteRange.insertNode(element);
// Вставляем новый элемент на место удаленного текста // Очищаем выделение
deleteRange.insertNode(element); selection.removeAllRanges();
}
// Очищаем выделение /**
selection.removeAllRanges();
}
/**
* Вставляет пробел после указанного элемента. * Вставляет пробел после указанного элемента.
* @param element - HTML элемент, после которого нужно вставить пробел. * @param element - HTML элемент, после которого нужно вставить пробел.
*/ */
@ -681,28 +685,28 @@ export class ChatPanel {
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection) return; if (!selection) return;
// Проверяем, есть ли после элемента текстовый узел (который мы только что вставили) // Проверяем, есть ли после элемента текстовый узел (который мы только что вставили)
let targetNode = element.nextSibling; let targetNode = element.nextSibling;
if (targetNode?.nodeType === Node.TEXT_NODE) { if (targetNode?.nodeType === Node.TEXT_NODE) {
const range = document.createRange(); const range = document.createRange();
range.setStart(targetNode, targetNode.textContent!.length); range.setStart(targetNode, targetNode.textContent!.length);
range.collapse(true); range.collapse(true);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} else { } else {
// Если нет текстового узла (такого быть не должно после insertSpaceAfterElement), // Если нет текстового узла (такого быть не должно после insertSpaceAfterElement),
// ставим курсор максимально близко к элементу. // ставим курсор максимально близко к элементу.
const range = document.createRange(); const range = document.createRange();
range.setStartAfter(element); range.setStartAfter(element);
range.collapse(true); range.collapse(true);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} }
} }
/** /**
* Получает позицию курсора в тексте * Получает позицию курсора в тексте
* (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения) * (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения)
*/ */
private getCursorPositionInText(): number { private getCursorPositionInText(): number {
const selection = window.getSelection(); const selection = window.getSelection();
@ -710,90 +714,88 @@ export class ChatPanel {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
// Получаем "абсолютный" индекс курсора в контексте всего текстового содержимого `messageInput` // Получаем "абсолютный" индекс курсора в контексте всего текстового содержимого `messageInput`
// с учетом того, что reference-box'ы представлены их текстовыми аналогами в getTextContent() // с учетом того, что reference-box'ы представлены их текстовыми аналогами в getTextContent()
let currentTextOffset = 0; let currentTextOffset = 0;
const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL);
let node; let node;
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
if (node === range.endContainer) { if (node === range.endContainer) {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
currentTextOffset += range.endOffset; currentTextOffset += range.endOffset;
} else if (node === this.messageInput) { } else if (node === this.messageInput) {
// Cлучай, когда курсор в корневом элементе, но не в текстовом узле. // Cлучай, когда курсор в корневом элементе, но не в текстовом узле.
// Может быть, курсор находится между элементами или в конце. // Может быть, курсор находится между элементами или в конце.
// Нам нужно определить, сколько узлов до курсора. // Нам нужно определить, сколько узлов до курсора.
let nodeBeforeCursorElementCount = 0; let nodeBeforeCursorElementCount = 0;
for (let i = 0; i < range.endOffset; i++) { for (let i = 0; i < range.endOffset; i++) {
const childNode = this.messageInput.childNodes[i]; const childNode = this.messageInput.childNodes[i];
currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(childNode); currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(childNode);
} }
} else { } else {
// Если endContainer - это другой элемент (например, <br>), // Если endContainer - это другой элемент (например, <br>),
// то курсор находится до него. // то курсор находится до него.
// Мы не должны добавлять его длину, но должны учесть, сколько текста было до него. // Мы не должны добавлять его длину, но должны учесть, сколько текста было до него.
// В данном случае `range.endOffset` будет 0, если курсор перед ним. // В данном случае `range.endOffset` будет 0, если курсор перед ним.
// Если курсор внутри элемента, нужно по-другому считать. // Если курсор внутри элемента, нужно по-другому считать.
// В контексте `contenteditable` это обычно означает, что мы либо в текстовом узле, // В контексте `contenteditable` это обычно означает, что мы либо в текстовом узле,
// либо `endContainer` является родителем, а `endOffset` - индексом дочернего элемента. // либо `endContainer` является родителем, а `endOffset` - индексом дочернего элемента.
// Для `reference-box` курсор внутри быть не должен (т.к. он `<span contenteditable="false">`). // Для `reference-box` курсор внутри быть не должен (т.к. он `<span contenteditable="false">`).
// Для других элементов, если курсор внутри них, `range.endOffset` будет смещением. // Для других элементов, если курсор внутри них, `range.endOffset` будет смещением.
} }
break; break;
} else { } else {
currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(node); currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(node);
} }
} }
return currentTextOffset; return currentTextOffset;
} }
/** /**
* Вычисляет эквивалентную текстовую длину HTML-элемента, такую как reference-box. * Вычисляет эквивалентную текстовую длину HTML-элемента, такую как reference-box.
* @param node - HTML-элемент. * @param node - HTML-элемент.
* @returns эквивалентная текстовая длина элемента. * @returns эквивалентная текстовая длина элемента.
*/ */
private getNodeEquivalentLength(node: Node): number { private getNodeEquivalentLength(node: Node): number {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
return node.textContent?.length || 0; return node.textContent?.length || 0;
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) {
return 1; // Учитываем как 1 позицию, т.к. курсор не может быть внутри return 1; // Учитываем как 1 позицию, т.к. курсор не может быть внутри
} }
// Для других элементов, считаем 0, если они не содержат текст // Для других элементов, считаем 0, если они не содержат текст
return 0; return 0;
} }
/** /**
* Получает текстовое содержимое input элемента (без HTML), * Получает текстовое содержимое input элемента (без HTML),
* заменяя reference-box их текстовым представлением. * заменяя reference-box их текстовым представлением.
*/ */
private getTextContent(): string { private getTextContent(): string {
let result = ''; let result = '';
const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL для корректного обхода const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL для корректного обхода
let node; let node;
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
result += node.textContent || ''; result += node.textContent || '';
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) {
const reference = ReferenceBox.extractReference(node as HTMLElement); const reference = ReferenceBox.extractReference(node as HTMLElement);
if (reference) { if (reference) {
const prefix = reference.permanent const prefix = reference.permanent ? (reference.type === 'file' ? '@@' : '##') : reference.type === 'file' ? '@' : '#';
? reference.type === 'file' ? '@@' : '##' result += prefix + reference.name;
: reference.type === 'file' ? '@' : '#'; }
result += prefix + reference.name; } else if (node.nodeType === Node.ELEMENT_NODE) {
} // Для других элементов, которые могут быть в contenteditable (например, <br>),
} else if (node.nodeType === Node.ELEMENT_NODE) { // нужно решить, как их представлять в текстовом содержимом.
// Для других элементов, которые могут быть в contenteditable (например, <br>), // В данном случае, если это <br>, его можно игнорировать или заменить на \n.
// нужно решить, как их представлять в текстовом содержимом. // Для простоты, пока игнорируем, если это не бокс.
// В данном случае, если это <br>, его можно игнорировать или заменить на \n. // Если внутри inputElement есть другие элементы, они также будут частью getTextContent,
// Для простоты, пока игнорируем, если это не бокс. // поэтому walker.nextNode() будет их учитывать.
// Если внутри inputElement есть другие элементы, они также будут частью getTextContent, // Возможно, здесь нужно быть более точным, основываясь на ожидаемой структуре.
// поэтому walker.nextNode() будет их учитывать. }
// Возможно, здесь нужно быть более точным, основываясь на ожидаемой структуре. }
} return result;
}
return result;
} }
/** /**
@ -803,7 +805,7 @@ export class ChatPanel {
let result = ''; let result = '';
const childNodes = Array.from(this.messageInput.childNodes); const childNodes = Array.from(this.messageInput.childNodes);
for (const childNode of childNodes) { for (const childNode of childNodes) {
if (childNode.nodeType === Node.TEXT_NODE) { if (childNode.nodeType === Node.TEXT_NODE) {
result += childNode.textContent; result += childNode.textContent;