Merge pull request #11 from mohamedatef24/extention
Browse files- extension/background.js +1 -0
- extension/content-inline.js +65 -2
- extension/sidepanel/sidepanel.js +20 -8
extension/background.js
CHANGED
|
@@ -196,6 +196,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
| 196 |
text: message.text,
|
| 197 |
mode: message.mode || 'replaceAll',
|
| 198 |
source: message.source,
|
|
|
|
| 199 |
}, (resp) => sendResponse(resp || { ok: false }));
|
| 200 |
});
|
| 201 |
return true; // async
|
|
|
|
| 196 |
text: message.text,
|
| 197 |
mode: message.mode || 'replaceAll',
|
| 198 |
source: message.source,
|
| 199 |
+
find: message.find || '',
|
| 200 |
}, (resp) => sendResponse(resp || { ok: false }));
|
| 201 |
});
|
| 202 |
return true; // async
|
extension/content-inline.js
CHANGED
|
@@ -748,12 +748,75 @@
|
|
| 748 |
try { captureSelection(e.target); } catch { pendingSelection = null; }
|
| 749 |
}, true);
|
| 750 |
|
| 751 |
-
function writeTextToField(field, text, mode, source) {
|
| 752 |
if (!field || typeof text !== 'string') return;
|
| 753 |
|
| 754 |
const tag = field.tagName.toLowerCase();
|
| 755 |
const suppress = NON_CORRECTION_SOURCES.includes(source);
|
| 756 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
// Resolve the effective mode. 'auto' = replace the captured selection if we
|
| 758 |
// have one for THIS field, otherwise replace the whole field.
|
| 759 |
const sel = (pendingSelection && pendingSelection.field === field) ? pendingSelection : null;
|
|
@@ -834,7 +897,7 @@
|
|
| 834 |
// so it can't win the response race against the frame that does.
|
| 835 |
if (!field) return false;
|
| 836 |
try {
|
| 837 |
-
writeTextToField(field, msg.text, msg.mode, msg.source);
|
| 838 |
sendResponse({ ok: true });
|
| 839 |
} catch (err) {
|
| 840 |
console.warn('[Bayan] Write-back error:', err.message);
|
|
|
|
| 748 |
try { captureSelection(e.target); } catch { pendingSelection = null; }
|
| 749 |
}, true);
|
| 750 |
|
| 751 |
+
function writeTextToField(field, text, mode, source, find) {
|
| 752 |
if (!field || typeof text !== 'string') return;
|
| 753 |
|
| 754 |
const tag = field.tagName.toLowerCase();
|
| 755 |
const suppress = NON_CORRECTION_SOURCES.includes(source);
|
| 756 |
|
| 757 |
+
// ── find anchor: if the caller passed the original selected text, try to
|
| 758 |
+
// locate it inside the field and replace ONLY that occurrence. This is the
|
| 759 |
+
// most reliable way to scope a context-menu correction to the user's
|
| 760 |
+
// selection — it survives focus loss and timing gaps.
|
| 761 |
+
if (find && typeof find === 'string' && find.length > 0) {
|
| 762 |
+
if (tag === 'textarea' || tag === 'input') {
|
| 763 |
+
const idx = field.value.indexOf(find);
|
| 764 |
+
if (idx !== -1) {
|
| 765 |
+
const before = field.value.slice(0, idx);
|
| 766 |
+
const after = field.value.slice(idx + find.length);
|
| 767 |
+
field.value = before + text + after;
|
| 768 |
+
const caret = idx + text.length;
|
| 769 |
+
try { field.setSelectionRange(caret, caret); } catch {}
|
| 770 |
+
if (suppress) analysisSuppressed = true;
|
| 771 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 772 |
+
pendingSelection = null;
|
| 773 |
+
try { delete field.dataset.bayanSource; } catch {}
|
| 774 |
+
return;
|
| 775 |
+
}
|
| 776 |
+
} else if (field.isContentEditable) {
|
| 777 |
+
const content = field.innerText || field.textContent || '';
|
| 778 |
+
const idx = content.indexOf(find);
|
| 779 |
+
if (idx !== -1) {
|
| 780 |
+
field.focus();
|
| 781 |
+
const treeWalker = document.createTreeWalker(field, NodeFilter.SHOW_TEXT);
|
| 782 |
+
let charCount = 0;
|
| 783 |
+
let startNode = null, startOffset = 0, endNode = null, endOffset = 0;
|
| 784 |
+
while (treeWalker.nextNode()) {
|
| 785 |
+
const node = treeWalker.currentNode;
|
| 786 |
+
const nodeLen = node.textContent.length;
|
| 787 |
+
if (!startNode && charCount + nodeLen > idx) {
|
| 788 |
+
startNode = node;
|
| 789 |
+
startOffset = idx - charCount;
|
| 790 |
+
}
|
| 791 |
+
if (startNode && charCount + nodeLen >= idx + find.length) {
|
| 792 |
+
endNode = node;
|
| 793 |
+
endOffset = idx + find.length - charCount;
|
| 794 |
+
break;
|
| 795 |
+
}
|
| 796 |
+
charCount += nodeLen;
|
| 797 |
+
}
|
| 798 |
+
if (startNode && endNode) {
|
| 799 |
+
try {
|
| 800 |
+
const range = document.createRange();
|
| 801 |
+
range.setStart(startNode, startOffset);
|
| 802 |
+
range.setEnd(endNode, endOffset);
|
| 803 |
+
const winSel = window.getSelection();
|
| 804 |
+
winSel.removeAllRanges();
|
| 805 |
+
winSel.addRange(range);
|
| 806 |
+
range.deleteContents();
|
| 807 |
+
range.insertNode(document.createTextNode(text));
|
| 808 |
+
winSel.collapseToEnd();
|
| 809 |
+
if (suppress) analysisSuppressed = true;
|
| 810 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 811 |
+
pendingSelection = null;
|
| 812 |
+
try { delete field.dataset.bayanSource; } catch {}
|
| 813 |
+
return;
|
| 814 |
+
} catch {}
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
}
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
// Resolve the effective mode. 'auto' = replace the captured selection if we
|
| 821 |
// have one for THIS field, otherwise replace the whole field.
|
| 822 |
const sel = (pendingSelection && pendingSelection.field === field) ? pendingSelection : null;
|
|
|
|
| 897 |
// so it can't win the response race against the frame that does.
|
| 898 |
if (!field) return false;
|
| 899 |
try {
|
| 900 |
+
writeTextToField(field, msg.text, msg.mode, msg.source, msg.find);
|
| 901 |
sendResponse({ ok: true });
|
| 902 |
} catch (err) {
|
| 903 |
console.warn('[Bayan] Write-back error:', err.message);
|
extension/sidepanel/sidepanel.js
CHANGED
|
@@ -65,6 +65,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 65 |
let isAnalyzing = false;
|
| 66 |
let contextConsumed = false;
|
| 67 |
let debounceTimer = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
const SCORE_CIRCUMFERENCE = 440;
|
| 70 |
const DEBOUNCE_MS = 500;
|
|
@@ -155,14 +159,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 155 |
// The side panel is a separate document and cannot touch page DOM
|
| 156 |
// directly; it relays through background.js. `source` lets the content
|
| 157 |
// script decide whether to re-analyze (correct) or suppress (Change 3).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
// ══════════════════════════════════════════════════════════
|
| 159 |
-
function writeBackToPage(text, mode = 'auto', source = 'correct') {
|
| 160 |
try {
|
| 161 |
chrome.runtime.sendMessage(
|
| 162 |
-
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source },
|
| 163 |
(resp) => {
|
| 164 |
-
if (resp && resp.ok)
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
);
|
| 168 |
} catch {
|
|
@@ -353,6 +363,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 353 |
btnClear.addEventListener('click', () => {
|
| 354 |
inputText.value = '';
|
| 355 |
analyzedText = '';
|
|
|
|
| 356 |
updateCounts(inputText, charCount, wordCount);
|
| 357 |
scoreSection.classList.add('is-hidden');
|
| 358 |
resultSection.classList.add('is-hidden');
|
|
@@ -379,7 +390,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 379 |
updateScore(0, 0, 0);
|
| 380 |
renderSuggestions([]);
|
| 381 |
saveState();
|
| 382 |
-
writeBackToPage(analyzedText, 'auto', 'correct');
|
| 383 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 384 |
});
|
| 385 |
|
|
@@ -393,7 +404,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 393 |
const finalText = currentSuggestions.length > 0
|
| 394 |
? applyAllPatches(analyzedText, currentSuggestions)
|
| 395 |
: analyzedText;
|
| 396 |
-
writeBackToPage(finalText, 'auto', 'correct');
|
| 397 |
});
|
| 398 |
}
|
| 399 |
|
|
@@ -588,7 +599,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 588 |
btnApplyQuranTranslation.addEventListener('click', () => {
|
| 589 |
const text = (quranTranslationText.textContent || '').trim();
|
| 590 |
if (!text) { showToast('لا توجد ترجمة للتطبيق'); return; }
|
| 591 |
-
writeBackToPage(text, 'auto', 'quran');
|
| 592 |
});
|
| 593 |
}
|
| 594 |
|
|
@@ -706,7 +717,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 706 |
btn.addEventListener('click', () => {
|
| 707 |
const text = (getText() || '').trim();
|
| 708 |
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 709 |
-
writeBackToPage(text, 'auto', source);
|
| 710 |
});
|
| 711 |
anchorBtn.parentElement.appendChild(btn);
|
| 712 |
}
|
|
@@ -744,6 +755,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 744 |
// the matching tab's input, switching to it, and auto-running its model.
|
| 745 |
// Declared after all element refs so dialect/quran handles are in scope.
|
| 746 |
function runContextAction(action, text) {
|
|
|
|
| 747 |
if (action === TAB.CORRECT) {
|
| 748 |
inputText.value = text;
|
| 749 |
updateCounts(inputText, charCount, wordCount);
|
|
|
|
| 65 |
let isAnalyzing = false;
|
| 66 |
let contextConsumed = false;
|
| 67 |
let debounceTimer = null;
|
| 68 |
+
// The exact text the user had selected on the page when this action started
|
| 69 |
+
// (from the right-click context menu). Used as a precise find/replace anchor
|
| 70 |
+
// so write-back replaces ONLY that selection, never the whole field.
|
| 71 |
+
let sourceSelectionText = '';
|
| 72 |
|
| 73 |
const SCORE_CIRCUMFERENCE = 440;
|
| 74 |
const DEBOUNCE_MS = 500;
|
|
|
|
| 159 |
// The side panel is a separate document and cannot touch page DOM
|
| 160 |
// directly; it relays through background.js. `source` lets the content
|
| 161 |
// script decide whether to re-analyze (correct) or suppress (Change 3).
|
| 162 |
+
//
|
| 163 |
+
// `find` (optional) is the exact original selected text. When present, the
|
| 164 |
+
// content script replaces ONLY that occurrence in the field — the most
|
| 165 |
+
// reliable way to scope the replacement to the user's selection.
|
| 166 |
// ══════════════════════════════════════════════════════════
|
| 167 |
+
function writeBackToPage(text, mode = 'auto', source = 'correct', find = '') {
|
| 168 |
try {
|
| 169 |
chrome.runtime.sendMessage(
|
| 170 |
+
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source, find },
|
| 171 |
(resp) => {
|
| 172 |
+
if (resp && resp.ok) {
|
| 173 |
+
sourceSelectionText = text;
|
| 174 |
+
showToast('✓ تم تطبيق التغييرات في الصفحة');
|
| 175 |
+
} else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
|
| 176 |
}
|
| 177 |
);
|
| 178 |
} catch {
|
|
|
|
| 363 |
btnClear.addEventListener('click', () => {
|
| 364 |
inputText.value = '';
|
| 365 |
analyzedText = '';
|
| 366 |
+
sourceSelectionText = '';
|
| 367 |
updateCounts(inputText, charCount, wordCount);
|
| 368 |
scoreSection.classList.add('is-hidden');
|
| 369 |
resultSection.classList.add('is-hidden');
|
|
|
|
| 390 |
updateScore(0, 0, 0);
|
| 391 |
renderSuggestions([]);
|
| 392 |
saveState();
|
| 393 |
+
writeBackToPage(analyzedText, 'auto', 'correct', sourceSelectionText);
|
| 394 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 395 |
});
|
| 396 |
|
|
|
|
| 404 |
const finalText = currentSuggestions.length > 0
|
| 405 |
? applyAllPatches(analyzedText, currentSuggestions)
|
| 406 |
: analyzedText;
|
| 407 |
+
writeBackToPage(finalText, 'auto', 'correct', sourceSelectionText);
|
| 408 |
});
|
| 409 |
}
|
| 410 |
|
|
|
|
| 599 |
btnApplyQuranTranslation.addEventListener('click', () => {
|
| 600 |
const text = (quranTranslationText.textContent || '').trim();
|
| 601 |
if (!text) { showToast('لا توجد ترجمة للتطبيق'); return; }
|
| 602 |
+
writeBackToPage(text, 'auto', 'quran', sourceSelectionText);
|
| 603 |
});
|
| 604 |
}
|
| 605 |
|
|
|
|
| 717 |
btn.addEventListener('click', () => {
|
| 718 |
const text = (getText() || '').trim();
|
| 719 |
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 720 |
+
writeBackToPage(text, 'auto', source, sourceSelectionText);
|
| 721 |
});
|
| 722 |
anchorBtn.parentElement.appendChild(btn);
|
| 723 |
}
|
|
|
|
| 755 |
// the matching tab's input, switching to it, and auto-running its model.
|
| 756 |
// Declared after all element refs so dialect/quran handles are in scope.
|
| 757 |
function runContextAction(action, text) {
|
| 758 |
+
sourceSelectionText = text;
|
| 759 |
if (action === TAB.CORRECT) {
|
| 760 |
inputText.value = text;
|
| 761 |
updateCounts(inputText, charCount, wordCount);
|