File size: 7,850 Bytes
e5d8d3a e9c9e16 e5d8d3a 419dfa0 bb35e88 e5d8d3a fc062b2 e5d8d3a 6284eeb e5d8d3a 605cef2 e5d8d3a 605cef2 e5d8d3a 605cef2 e5d8d3a 605cef2 e5d8d3a e9c9e16 e5d8d3a 6284eeb e5d8d3a 6284eeb e5d8d3a 6284eeb e5d8d3a 419dfa0 e5d8d3a 6284eeb 419dfa0 e5d8d3a 6284eeb 419dfa0 e5d8d3a 605cef2 e5d8d3a 605cef2 e5d8d3a bb35e88 e5d8d3a fc062b2 e5d8d3a fc062b2 e5d8d3a fc062b2 e5d8d3a e9c9e16 e5d8d3a e9c9e16 e5d8d3a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
import * as d3 from 'd3';
import type { TextStats } from '../utils/textStatistics';
import { calculateTextStats } from '../utils/textStatistics';
import { countTokenCharacters } from '../utils/Util';
import type { FrontendAnalyzeResult } from '../api/GLTR_API';
import { updateBasicMetrics, updateTotalSurprisal, updateModel, validateMetricsElements } from '../utils/textMetricsUpdater';
import { tr } from '../lang/i18n-lite';
/**
* 扩展的 Input 事件接口
* 用于在 input 事件中传递额外的标志信息
*/
export interface ExtendedInputEvent extends Event {
isMatchingAnalysis?: boolean;
}
export type TextInputControllerOptions = {
textField: d3.Selection<any, unknown, any, any>;
textCountValue: d3.Selection<any, unknown, any, any>;
textMetrics: d3.Selection<any, unknown, any, any>;
metricBytes: d3.Selection<any, unknown, any, any>;
metricChars: d3.Selection<any, unknown, any, any>;
metricTokens: d3.Selection<any, unknown, any, any>;
metricTotalSurprisal: d3.Selection<any, unknown, any, any>;
metricModel: d3.Selection<any, unknown, any, any>;
clearBtn: d3.Selection<any, unknown, any, any>;
submitBtn: d3.Selection<any, unknown, any, any>;
saveBtn: d3.Selection<any, unknown, any, any>;
pasteBtn: d3.Selection<any, unknown, any, any>;
totalSurprisalFormat: (value: number | null) => string;
showAlertDialog: (title: string, message: string) => void;
};
export class TextInputController {
private options: TextInputControllerOptions;
constructor(options: TextInputControllerOptions) {
this.options = options;
this.initialize();
}
private initialize(): void {
// 初始化时检查一次按钮状态
this.updateButtonStates();
// Clear 按钮状态完全由 TextInputController 内部管理
// 使用原生 addEventListener 监听 input 事件,避免被 D3 的 .on() 覆盖
// 这样可以允许多个监听器共存
const textFieldNode = this.options.textField.node() as HTMLTextAreaElement | null;
if (textFieldNode) {
textFieldNode.addEventListener('input', () => {
this.updateButtonStates();
});
}
// Clear 按钮点击事件
this.options.clearBtn.on('click', () => {
this.handleClear();
});
// Paste 按钮点击事件
this.options.pasteBtn.on('click', async () => {
await this.handlePaste();
});
}
/**
* 更新按钮有效性和字符计数(私有方法,仅内部使用)
* 只负责更新 Clear 按钮状态和字符计数
* 注意:submitBtn 和 saveBtn 的状态由外部状态系统统一管理
*/
private updateButtonStates(): void {
const textValue = this.options.textField.property('value') || '';
const hasText = textValue.trim().length > 0;
// Clear按钮:只在文本框有内容时有效
this.options.clearBtn.classed('inactive', !hasText);
// 注意:submitBtn 的状态现在由外部状态系统统一管理,不再在这里设置
if (!this.options.textCountValue.empty()) {
const charCount = countTokenCharacters(textValue);
this.options.textCountValue.text(charCount.toString());
}
}
/**
* 更新文本指标内容(包括模型显示,不控制显示/隐藏,显示/隐藏由 AppStateManager 统一管理)
* @param stats 统计数据,为 null 时不更新统计内容
* @param modelName 模型名称,始终显示以反映原始情况
*/
public updateTextMetrics(stats: TextStats | null, modelName?: string | null | undefined): void {
const {
metricBytes,
metricChars,
metricTokens,
metricTotalSurprisal,
metricModel,
totalSurprisalFormat
} = this.options;
// 检查必要的元素是否存在
if (!validateMetricsElements(metricBytes, metricChars, metricTokens, metricTotalSurprisal, metricModel)) {
return;
}
// 更新统计指标内容(如果有统计数据)
if (stats) {
updateBasicMetrics(metricBytes, metricChars, metricTokens, stats);
updateTotalSurprisal(metricTotalSurprisal, stats, totalSurprisalFormat);
}
// 更新模型显示(始终显示以反映原始情况)
// 显示/隐藏由 AppStateManager 通过 textMetrics 容器统一管理
updateModel(metricModel, modelName);
}
/**
* 处理清空文本
*/
private handleClear(): void {
const textValue = this.options.textField.property('value') || '';
if (!textValue.trim()) {
return; // 如果没有文本,直接返回
}
this.options.textField.property('value', '');
// 触发 input 事件,让外部统一处理状态更新
this.options.textField.node()?.dispatchEvent(new Event('input', { bubbles: true }));
}
/**
* 处理粘贴
*/
private async handlePaste(): Promise<void> {
try {
const text = await navigator.clipboard.readText();
if (text) {
const currentValue = this.options.textField.property('value') || '';
// 在光标位置插入,如果没有光标或光标在末尾,则追加
const textarea = this.options.textField.node() as HTMLTextAreaElement;
if (textarea) {
const start = textarea.selectionStart || currentValue.length;
const end = textarea.selectionEnd || currentValue.length;
const newValue = currentValue.substring(0, start) + text + currentValue.substring(end);
this.options.textField.property('value', newValue);
// 设置光标位置到粘贴内容的末尾
textarea.setSelectionRange(start + text.length, start + text.length);
} else {
this.options.textField.property('value', currentValue + text);
}
// 触发 input 事件,让外部统一处理状态更新
this.options.textField.node()?.dispatchEvent(new Event('input', { bubbles: true }));
}
} catch (error) {
console.error('粘贴失败:', error);
// 如果clipboard API不可用,提示用户手动粘贴
this.options.showAlertDialog(tr('Info'), tr('Failed to read clipboard, please paste manually'));
}
}
/**
* 获取当前文本框的值
*/
public getTextValue(): string {
return this.options.textField.property('value') || '';
}
/**
* 设置文本框的值
* @param value 要设置的文本值
* @param isMatchingAnalysis 如果为true,表示这是匹配分析结果的文本填入(如加载demo),不会清除hasValidData
* 如果为false或未提供,表示这是单方面的文本修改(如用户输入、预填充),会清除hasValidData
*/
public setTextValue(value: string, isMatchingAnalysis: boolean = false): void {
this.options.textField.property('value', value);
// 触发 input 事件,添加标志以区分两种场景
const event = new Event('input', { bubbles: true }) as ExtendedInputEvent;
event.isMatchingAnalysis = isMatchingAnalysis;
this.options.textField.node()?.dispatchEvent(event);
}
}
/**
* 计算文本统计信息(便捷函数)
*/
export const calculateTextStatsForController = (
result: FrontendAnalyzeResult,
originalText: string
): TextStats => {
return calculateTextStats(result, originalText);
};
|