Mohamed Atef commited on
Commit
84cce80
·
unverified ·
2 Parent(s): 3d6289eb017d7f

Merge pull request #11 from mohamedatef24/extention

Browse files
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) showToast('✓ تم تطبيق التغييرات في الصفحة');
165
- else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
 
 
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);