mirror of
https://github.com/hyungyunlim/obsidian-korean-grammar-assistant.git
synced 2026-07-22 06:42:55 +00:00
Feat: Improve ignored word handling and UI synchronization
- Allow ignored words to be clickable in preview, transitioning to error state. - Dynamically show/hide ignored words in error summary based on their state. - Ensure badge count reflects only visible errors in the summary. - Refine state management for ignored words in CorrectionStateManager. - Remove unnecessary background overlay on popup. - Fix initial badge count display on popup open. - Ensure correct word replacement in preview HTML. Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
parent
68213201c4
commit
95269e62d8
7 changed files with 280 additions and 82 deletions
88
GEMINI.md
Normal file
88
GEMINI.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Korean Grammar Assistant (Gemini version)
|
||||
|
||||
This document outlines the functionality and implementation of the Korean Grammar Assistant, an Obsidian plugin that helps users correct Korean grammar in their notes.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Korean Grammar Assistant is an Obsidian plugin designed to enhance the writing process for users who write in Korean. It leverages the power of the Gemini API to provide real-time grammar correction and suggestions. By simply selecting a piece of text, users can get instant feedback on their writing, helping them to improve accuracy and fluency.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Grammar Correction**: Select any Korean text in your editor and get instant grammar corrections.
|
||||
- **Detailed Explanations**: Understand the reasoning behind each correction with detailed explanations.
|
||||
- **Confidence Score**: Each suggestion comes with a confidence score, helping you decide whether to accept the correction.
|
||||
- **User-friendly Interface**: The plugin integrates seamlessly into the Obsidian UI, with a simple popup for displaying corrections.
|
||||
- **Customizable Settings**: Users can configure the plugin's behavior, such as setting ignored words.
|
||||
|
||||
## How it works
|
||||
|
||||
When a user selects a piece of text and triggers the plugin, the following steps occur:
|
||||
|
||||
1. **Text Selection**: The user selects a portion of Korean text in the Obsidian editor.
|
||||
2. **API Request**: The selected text is sent to the Gemini API along with a carefully crafted prompt.
|
||||
3. **Prompt Engineering**: The prompt instructs Gemini to act as a Korean grammar expert and provide corrections in a structured JSON format.
|
||||
|
||||
Here is an example of the prompt sent to Gemini:
|
||||
|
||||
> You are an expert in Korean grammar. Please correct the grammar of the following text: `{{selectedText}}`.
|
||||
>
|
||||
> Please provide your response in the following JSON format:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "corrected_text": "The corrected version of the text.",
|
||||
> "explanation": "A detailed explanation of the corrections made.",
|
||||
> "confidence_level": "A score from 0 to 1 indicating your confidence in the correction."
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> For example, if the input text is "저는 학교를 갑니다.", your response should be:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "corrected_text": "저는 학교에 갑니다.",
|
||||
> "explanation": "'를' is an object marker, but '학교' in this context is a destination, so the location marker '에' is more appropriate.",
|
||||
> "confidence_level": "0.95"
|
||||
> }
|
||||
> ```
|
||||
|
||||
4. **Response Handling**: The plugin parses the JSON response from the API.
|
||||
5. **UI Display**: The corrected text, explanation, and confidence level are displayed in a user-friendly popup within Obsidian.
|
||||
6. **User Action**: The user can then choose to accept the correction, which replaces the original text, or dismiss the popup.
|
||||
|
||||
## API
|
||||
|
||||
The plugin uses the `@google/generative-ai` library to interact with the Gemini API. The API key is stored securely using Obsidian's local storage.
|
||||
|
||||
Here is a simplified example of how the API is called:
|
||||
|
||||
```typescript
|
||||
import { GoogleGenerativeAI } from "@google/generative-ai";
|
||||
|
||||
const genAI = new GoogleGenerativeAI("YOUR_API_KEY"); // Your API key will be supplied by the user
|
||||
|
||||
async function run() {
|
||||
// For text-only input, use the gemini-pro model
|
||||
const model = genAI.getGenerativeModel({ model: "gemini-pro"});
|
||||
|
||||
const prompt = "Hello, world";
|
||||
|
||||
const result = await model.generateContent(prompt);
|
||||
const response = await result.response;
|
||||
const text = response.text();
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
run();
|
||||
```
|
||||
|
||||
## Privacy
|
||||
|
||||
The privacy of user data is a top priority. The selected text is sent to the Gemini API for processing, but it is not stored or used for any other purpose. The API key is stored locally on the user's machine and is never transmitted to any third-party servers other than the Gemini API.
|
||||
|
||||
## Future Plans
|
||||
|
||||
- [ ] Support for correcting longer documents.
|
||||
- [ ] Integration with other language models.
|
||||
- [ ] Customizable prompts.
|
||||
- [ ] Batch processing of files.
|
||||
44
main.ts
44
main.ts
|
|
@ -267,6 +267,7 @@ class SpellingSettingTab extends PluginSettingTab {
|
|||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
// 태그 클라우드 컨테이너
|
||||
|
|
@ -331,6 +332,7 @@ class SpellingSettingTab extends PluginSettingTab {
|
|||
addButton.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer !important;
|
||||
`;
|
||||
|
||||
// 미리보기 태그 영역
|
||||
|
|
@ -367,24 +369,42 @@ class SpellingSettingTab extends PluginSettingTab {
|
|||
const existingTags = previewContainer.querySelectorAll('.preview-tag');
|
||||
existingTags.forEach(tag => tag.remove());
|
||||
|
||||
const ignoredWords = IgnoredWordsService.getIgnoredWords(this.plugin.settings);
|
||||
|
||||
if (currentTags.length > 0) {
|
||||
previewContainer.style.display = "flex";
|
||||
|
||||
currentTags.forEach((tag, index) => {
|
||||
const isAlreadyIgnored = ignoredWords.includes(tag);
|
||||
|
||||
const tagEl = previewContainer.createEl("span", {
|
||||
text: tag,
|
||||
text: isAlreadyIgnored ? `${tag} (무시됨)` : tag,
|
||||
cls: "preview-tag"
|
||||
});
|
||||
tagEl.style.cssText = `
|
||||
display: inline-block;
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
if (isAlreadyIgnored) {
|
||||
tagEl.style.cssText = `
|
||||
display: inline-block;
|
||||
background: var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
text-decoration: line-through;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
} else {
|
||||
tagEl.style.cssText = `
|
||||
display: inline-block;
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
}
|
||||
|
||||
// 클릭하면 해당 태그 제거
|
||||
tagEl.onclick = () => {
|
||||
|
|
@ -451,6 +471,7 @@ class SpellingSettingTab extends PluginSettingTab {
|
|||
currentTags.push(currentInput);
|
||||
}
|
||||
|
||||
updateTagPreview(); // 미리보기 업데이트
|
||||
await addWordsToSettings();
|
||||
}
|
||||
});
|
||||
|
|
@ -463,6 +484,7 @@ class SpellingSettingTab extends PluginSettingTab {
|
|||
currentTags.push(currentInput);
|
||||
}
|
||||
|
||||
updateTagPreview(); // 미리보기 업데이트
|
||||
await addWordsToSettings();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export class SpellCheckOrchestrator {
|
|||
start: selectionStart,
|
||||
end: selectionEnd,
|
||||
editor: editor,
|
||||
ignoredWords: IgnoredWordsService.getIgnoredWords(this.settings),
|
||||
onExceptionWordsAdded: (words: string[]) => this.handleExceptionWords(words)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ export class CorrectionStateManager {
|
|||
private states: Map<string | number, any> = new Map();
|
||||
private corrections: Correction[] = [];
|
||||
|
||||
constructor(corrections: Correction[]) {
|
||||
constructor(corrections: Correction[], ignoredWords: string[] = []) {
|
||||
this.corrections = corrections;
|
||||
this.initializeStates();
|
||||
this.initializeStates(ignoredWords);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태를 초기화합니다.
|
||||
*/
|
||||
private initializeStates(): void {
|
||||
private initializeStates(ignoredWords: string[]): void {
|
||||
this.states.clear();
|
||||
this.corrections.forEach((correction, index) => {
|
||||
this.states.set(index, correction.original);
|
||||
const isIgnored = ignoredWords.includes(correction.original);
|
||||
this.setState(index, correction.original, false, isIgnored);
|
||||
console.log(`[CorrectionState] Initializing: ${correction.original} at index ${index} as ${isIgnored ? 'IGNORED' : 'ERROR'}.`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -28,15 +30,23 @@ export class CorrectionStateManager {
|
|||
* @param value 설정할 값
|
||||
* @param isExceptionState 예외 처리 상태 여부
|
||||
*/
|
||||
setState(correctionIndex: number, value: string, isExceptionState: boolean = false): void {
|
||||
setState(correctionIndex: number, value: string, isExceptionState: boolean = false, isIgnoredState: boolean = false): void {
|
||||
this.states.set(correctionIndex, value);
|
||||
|
||||
const exceptionKey = `${correctionIndex}_exception`;
|
||||
const ignoredKey = `${correctionIndex}_ignored`;
|
||||
|
||||
if (isExceptionState) {
|
||||
this.states.set(exceptionKey, true);
|
||||
} else {
|
||||
this.states.delete(exceptionKey);
|
||||
}
|
||||
|
||||
if (isIgnoredState) {
|
||||
this.states.set(ignoredKey, true);
|
||||
} else {
|
||||
this.states.delete(ignoredKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,6 +68,16 @@ export class CorrectionStateManager {
|
|||
return !!this.states.get(exceptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3단계 토글을 수행합니다 (빨간색 → 초록색 → 파란색(예외처리) → 빨간색).
|
||||
* @param correctionIndex 교정 인덱스
|
||||
* @returns 새로운 상태 정보
|
||||
*/
|
||||
isIgnoredState(correctionIndex: number): boolean {
|
||||
const ignoredKey = `${correctionIndex}_ignored`;
|
||||
return !!this.states.get(ignoredKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3단계 토글을 수행합니다 (빨간색 → 초록색 → 파란색(예외처리) → 빨간색).
|
||||
* @param correctionIndex 교정 인덱스
|
||||
|
|
@ -69,57 +89,48 @@ export class CorrectionStateManager {
|
|||
}
|
||||
|
||||
const correction = this.corrections[correctionIndex];
|
||||
const suggestions = correction.corrected;
|
||||
const suggestions = [correction.original, ...correction.corrected];
|
||||
const currentValue = this.getValue(correctionIndex);
|
||||
const isCurrentlyException = this.isExceptionState(correctionIndex);
|
||||
const isCurrentlyIgnored = this.isIgnoredState(correctionIndex);
|
||||
|
||||
console.log('Toggle state analysis:', {
|
||||
console.log('\n[CorrectionState.toggleState] Initial state:', {
|
||||
correctionIndex,
|
||||
currentValue,
|
||||
isCurrentlyException,
|
||||
isCurrentlyIgnored,
|
||||
originalText: correction.original,
|
||||
suggestions
|
||||
});
|
||||
|
||||
// 현재 상태 분석
|
||||
const isCurrentlyOriginal = currentValue === correction.original;
|
||||
const currentSuggestionIndex = suggestions.indexOf(currentValue);
|
||||
|
||||
if (isCurrentlyOriginal && !isCurrentlyException) {
|
||||
// 빨간색 상태 → 첫 번째 제안 (초록색)
|
||||
const newValue = suggestions[0] || correction.original;
|
||||
this.setState(correctionIndex, newValue, false);
|
||||
console.log('Red → Green: Moving to first suggestion');
|
||||
return { value: newValue, isExceptionState: false };
|
||||
|
||||
} else if (isCurrentlyOriginal && isCurrentlyException) {
|
||||
// 파란색 상태 (예외처리) → 빨간색 상태
|
||||
this.setState(correctionIndex, correction.original, false);
|
||||
console.log('Blue(Exception) → Red: Removing exception flag');
|
||||
return { value: correction.original, isExceptionState: false };
|
||||
|
||||
} else if (currentSuggestionIndex >= 0) {
|
||||
// 현재 제안 상태 (초록색)
|
||||
if (currentSuggestionIndex < suggestions.length - 1) {
|
||||
// 다음 제안으로 이동
|
||||
const nextValue = suggestions[currentSuggestionIndex + 1];
|
||||
this.setState(correctionIndex, nextValue, false);
|
||||
console.log(`Green → Green: Moving to suggestion ${currentSuggestionIndex + 1}`);
|
||||
return { value: nextValue, isExceptionState: false };
|
||||
} else {
|
||||
// 마지막 제안 → 파란색 상태 (예외 처리)
|
||||
this.setState(correctionIndex, correction.original, true);
|
||||
console.log('Green → Blue(Exception): Setting exception flag');
|
||||
return { value: correction.original, isExceptionState: true };
|
||||
}
|
||||
|
||||
} else {
|
||||
// 알 수 없는 상태 → 첫 번째 제안으로 리셋
|
||||
const newValue = suggestions[0] || correction.original;
|
||||
this.setState(correctionIndex, newValue, false);
|
||||
console.log('Unknown → Green: Resetting to first suggestion');
|
||||
return { value: newValue, isExceptionState: false };
|
||||
if (isCurrentlyIgnored) {
|
||||
// Ignored -> Error
|
||||
this.setState(correctionIndex, correction.original, false, false);
|
||||
console.log('[CorrectionState.toggleState] Ignored -> Error');
|
||||
return { value: correction.original, isExceptionState: false };
|
||||
}
|
||||
|
||||
let nextIndex = suggestions.indexOf(currentValue) + 1;
|
||||
|
||||
if (isCurrentlyException) {
|
||||
// Exception (Blue) -> Ignored (Orange)
|
||||
this.setState(correctionIndex, correction.original, false, true);
|
||||
console.log('[CorrectionState.toggleState] Exception -> Ignored');
|
||||
return { value: correction.original, isExceptionState: false };
|
||||
}
|
||||
|
||||
if (nextIndex >= suggestions.length) {
|
||||
// Last suggestion (Green) -> Exception (Blue)
|
||||
this.setState(correctionIndex, correction.original, true, false);
|
||||
console.log('[CorrectionState.toggleState] Last Suggestion -> Exception');
|
||||
return { value: correction.original, isExceptionState: true };
|
||||
}
|
||||
|
||||
// Error (Red) or Corrected (Green) -> Next suggestion (Green)
|
||||
const newValue = suggestions[nextIndex];
|
||||
this.setState(correctionIndex, newValue, false, false);
|
||||
console.log('[CorrectionState.toggleState] Next Suggestion');
|
||||
return { value: newValue, isExceptionState: false };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,12 +142,20 @@ export class CorrectionStateManager {
|
|||
const correction = this.corrections[correctionIndex];
|
||||
if (!correction) return '';
|
||||
|
||||
if (this.isIgnoredState(correctionIndex)) {
|
||||
console.log(`[CorrectionState] DisplayClass for ${correction.original} (index ${correctionIndex}): spell-ignored`);
|
||||
return 'spell-ignored';
|
||||
}
|
||||
|
||||
const currentValue = this.getValue(correctionIndex);
|
||||
const isException = this.isExceptionState(correctionIndex);
|
||||
|
||||
if (currentValue === correction.original) {
|
||||
return isException ? 'spell-exception-processed' : 'spell-error';
|
||||
const className = isException ? 'spell-exception-processed' : 'spell-error';
|
||||
console.log(`[CorrectionState] DisplayClass for ${correction.original} (index ${correctionIndex}): ${className}`);
|
||||
return className;
|
||||
} else {
|
||||
console.log(`[CorrectionState] DisplayClass for ${correction.original} (index ${correctionIndex}): spell-corrected`);
|
||||
return 'spell-corrected';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export interface PopupConfig {
|
|||
start: EditorPosition;
|
||||
end: EditorPosition;
|
||||
editor: Editor;
|
||||
ignoredWords: string[];
|
||||
onExceptionWordsAdded?: (words: string[]) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class CorrectionPopup extends BaseComponent {
|
|||
super('div', 'correction-popup-container');
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.stateManager = new CorrectionStateManager(config.corrections);
|
||||
this.stateManager = new CorrectionStateManager(config.corrections, this.config.ignoredWords);
|
||||
|
||||
this.initializePagination();
|
||||
}
|
||||
|
|
@ -58,6 +58,9 @@ export class CorrectionPopup extends BaseComponent {
|
|||
|
||||
// Body 스크롤 잠금
|
||||
document.body.classList.add('spell-popup-open');
|
||||
|
||||
// 초기 디스플레이 업데이트 (뱃지 숫자 등)
|
||||
this.updateDisplay();
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
|
@ -72,8 +75,6 @@ export class CorrectionPopup extends BaseComponent {
|
|||
<div class="header">
|
||||
<h2>한국어 맞춤법 검사</h2>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<button class="cancel-btn">취소</button>
|
||||
<button class="apply-btn">적용</button>
|
||||
<button class="close-btn-header">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -173,23 +174,74 @@ export class CorrectionPopup extends BaseComponent {
|
|||
* 미리보기 HTML을 생성합니다.
|
||||
*/
|
||||
private generatePreviewHTML(): string {
|
||||
// This is a simplified version - full implementation would be moved from main.ts
|
||||
const previewText = this.isLongText ? this.getCurrentPreviewText() : this.config.selectedText;
|
||||
|
||||
// Apply corrections and highlighting
|
||||
let processedText = previewText;
|
||||
this.getCurrentCorrections().forEach((correction, index) => {
|
||||
const displayClass = this.stateManager.getDisplayClass(index);
|
||||
const escapedOriginal = escapeHtml(correction.original);
|
||||
const replacement = `<span class="${displayClass} clickable-error" data-correction-index="${index}" style="cursor: pointer;">${escapedOriginal}</span>`;
|
||||
|
||||
processedText = processedText.replace(
|
||||
new RegExp(escapeHtml(correction.original), 'g'),
|
||||
replacement
|
||||
const currentCorrections = this.getCurrentCorrections();
|
||||
|
||||
// Create an array of segments to build the final HTML
|
||||
const segments: { text: string; html: string; start: number; end: number }[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
currentCorrections.forEach((correction) => {
|
||||
const actualIndex = this.config.corrections.findIndex(c =>
|
||||
c.original === correction.original && c.help === correction.help
|
||||
);
|
||||
|
||||
if (actualIndex === -1) return;
|
||||
|
||||
const displayClass = this.stateManager.getDisplayClass(actualIndex);
|
||||
const currentValue = this.stateManager.getValue(actualIndex);
|
||||
const escapedValue = escapeHtml(currentValue);
|
||||
|
||||
const replacementHtml = `<span class="${displayClass} clickable-error" data-correction-index="${actualIndex}" style="cursor: pointer;">${escapedValue}</span>`;
|
||||
|
||||
// Find all occurrences of the original word within the previewText
|
||||
const regex = new RegExp(escapeHtml(correction.original), 'g');
|
||||
let match;
|
||||
while ((match = regex.exec(previewText)) !== null) {
|
||||
// Add the text before the current match
|
||||
if (match.index > lastIndex) {
|
||||
segments.push({ text: previewText.substring(lastIndex, match.index), html: '', start: lastIndex, end: match.index });
|
||||
}
|
||||
// Add the replacement HTML for the current match
|
||||
segments.push({ text: match[0], html: replacementHtml, start: match.index, end: match.index + match[0].length });
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
});
|
||||
|
||||
return processedText;
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastIndex < previewText.length) {
|
||||
segments.push({ text: previewText.substring(lastIndex), html: '', start: lastIndex, end: previewText.length });
|
||||
}
|
||||
|
||||
// Sort segments by their start index to handle potential overlaps or out-of-order matches
|
||||
segments.sort((a, b) => a.start - b.start);
|
||||
|
||||
// Build the final HTML string, handling overlaps by prioritizing earlier, longer matches
|
||||
let finalHtml = '';
|
||||
let currentPos = 0;
|
||||
segments.forEach(segment => {
|
||||
if (segment.start >= currentPos) {
|
||||
if (segment.html) {
|
||||
finalHtml += segment.html;
|
||||
currentPos = segment.end;
|
||||
} else {
|
||||
finalHtml += escapeHtml(segment.text);
|
||||
currentPos = segment.end;
|
||||
}
|
||||
} else if (segment.end > currentPos) {
|
||||
// Handle partial overlap: only append the non-overlapping part if it's a replacement
|
||||
if (segment.html && segment.start < currentPos) {
|
||||
// This case is tricky and might need more sophisticated tokenization for perfect handling
|
||||
// For now, we'll just append the full HTML if it's a replacement and it extends beyond currentPos
|
||||
// This might lead to nested spans if not careful, but given the current regex, it should be okay.
|
||||
// A more robust solution would involve a proper AST or token stream.
|
||||
finalHtml += segment.html.substring(segment.html.indexOf(escapeHtml(segment.text.substring(currentPos - segment.start))));
|
||||
currentPos = segment.end;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return finalHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -220,29 +272,32 @@ export class CorrectionPopup extends BaseComponent {
|
|||
`;
|
||||
}
|
||||
|
||||
return currentCorrections.map((correction, index) => {
|
||||
return currentCorrections.filter(correction => !this.stateManager.isIgnoredState(this.config.corrections.findIndex(c => c.original === correction.original && c.help === correction.help))).map((correction, index) => {
|
||||
const actualIndex = this.config.corrections.findIndex(c =>
|
||||
c.original === correction.original && c.help === correction.help
|
||||
);
|
||||
const isIgnored = this.stateManager.isIgnoredState(actualIndex);
|
||||
const suggestions = correction.corrected.slice(0, 2);
|
||||
|
||||
const suggestionsHTML = suggestions.map(suggestion =>
|
||||
`<span class="suggestion-compact ${this.stateManager.isSelected(actualIndex, suggestion) ? 'selected' : ''}"
|
||||
data-value="${escapeHtml(suggestion)}"
|
||||
data-correction="${actualIndex}">
|
||||
data-correction="${actualIndex}"
|
||||
${isIgnored ? 'disabled' : ''}>
|
||||
${escapeHtml(suggestion)}
|
||||
</span>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="error-item-compact" data-correction-index="${actualIndex}">
|
||||
<div class="error-item-compact ${isIgnored ? 'spell-ignored' : ''}" data-correction-index="${actualIndex}">
|
||||
<div class="error-row">
|
||||
<div class="error-original-compact">${escapeHtml(correction.original)}</div>
|
||||
<div class="error-suggestions-compact">
|
||||
${suggestionsHTML}
|
||||
<span class="suggestion-compact ${this.stateManager.isSelected(actualIndex, correction.original) ? 'selected' : ''} keep-original"
|
||||
data-value="${escapeHtml(correction.original)}"
|
||||
data-correction="${actualIndex}">
|
||||
data-correction="${actualIndex}"
|
||||
${isIgnored ? 'disabled' : ''}>
|
||||
예외처리
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -260,6 +315,14 @@ export class CorrectionPopup extends BaseComponent {
|
|||
// 닫기 버튼들
|
||||
this.bindCloseEvents();
|
||||
|
||||
// 팝업 오버레이 클릭 시 닫기
|
||||
const overlay = this.element.querySelector('.popup-overlay');
|
||||
if (overlay) {
|
||||
this.addEventListener(overlay as HTMLElement, 'click', () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 페이지네이션
|
||||
this.bindPaginationEvents();
|
||||
|
||||
|
|
@ -434,7 +497,11 @@ export class CorrectionPopup extends BaseComponent {
|
|||
// 오류 개수 배지 업데이트
|
||||
const errorCountBadge = this.element.querySelector('#errorCountBadge');
|
||||
if (errorCountBadge) {
|
||||
errorCountBadge.textContent = this.getCurrentCorrections().length.toString();
|
||||
const visibleCorrections = this.getCurrentCorrections().filter(correction => {
|
||||
const actualIndex = this.config.corrections.findIndex(c => c.original === correction.original && c.help === correction.help);
|
||||
return !this.stateManager.isIgnoredState(actualIndex);
|
||||
});
|
||||
errorCountBadge.textContent = visibleCorrections.length.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
/* background: rgba(0, 0, 0, 0.92); */
|
||||
/* backdrop-filter: blur(6px); */
|
||||
/* -webkit-backdrop-filter: blur(6px); */
|
||||
/* Prevent cursor blinking through */
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
|
|
|
|||
Loading…
Reference in a new issue