Fix model selection dropdown
This commit is contained in:
parent
29cddbcb57
commit
23b4ca4571
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user