Mohamed Atef commited on
Commit ·
e45ef70
1
Parent(s): f8feb00
fix 3 bugs : Bug 1 — No "apply to page" button in the correct tab
Browse filesBug 2 — Apply replaces the whole field instead of the selected text
Bug 3 — Quran translation (14 languages)
- extension/content-inline.js +99 -10
- extension/sidepanel/sidepanel.css +43 -0
- extension/sidepanel/sidepanel.html +36 -0
- extension/sidepanel/sidepanel.js +77 -4
extension/content-inline.js
CHANGED
|
@@ -56,6 +56,7 @@
|
|
| 56 |
|
| 57 |
let activeField = null;
|
| 58 |
let lastInteractedField = null; // persists after focus moves to side panel (Change 1)
|
|
|
|
| 59 |
let analysisSuppressed = false; // true after a non-correction model write-back (Change 3)
|
| 60 |
let lastAnalyzedText = '';
|
| 61 |
let suggestions = [];
|
|
@@ -111,6 +112,8 @@
|
|
| 111 |
// event at all — neither should count as a manual edit (Change 3).
|
| 112 |
const isUserKeystroke = !!e && e.isTrusted === true;
|
| 113 |
if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
|
|
|
|
|
|
|
| 114 |
|
| 115 |
const text = getFieldText(activeField);
|
| 116 |
|
|
@@ -563,6 +566,9 @@
|
|
| 563 |
}
|
| 564 |
if (suggestions.length > 0) {
|
| 565 |
try {
|
|
|
|
|
|
|
|
|
|
| 566 |
// Tag the source field so write-back can re-find it after focus
|
| 567 |
// moves to the side panel (Change 1).
|
| 568 |
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
|
@@ -663,16 +669,81 @@
|
|
| 663 |
|
| 664 |
const NON_CORRECTION_SOURCES = ['summarize', 'dialect', 'quran'];
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
function writeTextToField(field, text, mode, source) {
|
| 667 |
if (!field || typeof text !== 'string') return;
|
| 668 |
|
| 669 |
const tag = field.tagName.toLowerCase();
|
| 670 |
const suppress = NON_CORRECTION_SOURCES.includes(source);
|
| 671 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
if (tag === 'textarea' || tag === 'input') {
|
| 673 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
&& typeof field.selectionStart === 'number'
|
| 675 |
&& field.selectionStart !== field.selectionEnd) {
|
|
|
|
| 676 |
const start = field.selectionStart;
|
| 677 |
const end = field.selectionEnd;
|
| 678 |
field.value = field.value.slice(0, start) + text + field.value.slice(end);
|
|
@@ -689,14 +760,31 @@
|
|
| 689 |
} else {
|
| 690 |
// contenteditable — mirror the overlay-safe approach used by applyFix.
|
| 691 |
field.focus();
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
} else {
|
| 701 |
field.textContent = text;
|
| 702 |
if (suppress) analysisSuppressed = true;
|
|
@@ -704,7 +792,8 @@
|
|
| 704 |
}
|
| 705 |
}
|
| 706 |
|
| 707 |
-
//
|
|
|
|
| 708 |
try { delete field.dataset.bayanSource; } catch {}
|
| 709 |
}
|
| 710 |
|
|
|
|
| 56 |
|
| 57 |
let activeField = null;
|
| 58 |
let lastInteractedField = null; // persists after focus moves to side panel (Change 1)
|
| 59 |
+
let pendingSelection = null; // selection captured on right-click, for selection-only write-back
|
| 60 |
let analysisSuppressed = false; // true after a non-correction model write-back (Change 3)
|
| 61 |
let lastAnalyzedText = '';
|
| 62 |
let suggestions = [];
|
|
|
|
| 112 |
// event at all — neither should count as a manual edit (Change 3).
|
| 113 |
const isUserKeystroke = !!e && e.isTrusted === true;
|
| 114 |
if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
|
| 115 |
+
// A real edit invalidates any selection captured at right-click time.
|
| 116 |
+
if (isUserKeystroke) pendingSelection = null;
|
| 117 |
|
| 118 |
const text = getFieldText(activeField);
|
| 119 |
|
|
|
|
| 566 |
}
|
| 567 |
if (suggestions.length > 0) {
|
| 568 |
try {
|
| 569 |
+
// FAB = whole-field intent. Drop any selection captured by an earlier
|
| 570 |
+
// right-click so write-back replaces the entire field, not a range.
|
| 571 |
+
pendingSelection = null;
|
| 572 |
// Tag the source field so write-back can re-find it after focus
|
| 573 |
// moves to the side panel (Change 1).
|
| 574 |
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
|
|
|
| 669 |
|
| 670 |
const NON_CORRECTION_SOURCES = ['summarize', 'dialect', 'quran'];
|
| 671 |
|
| 672 |
+
// Capture the field + selection range at right-click time. The context menu
|
| 673 |
+
// flow always acts on the selected text, so we remember exactly what was
|
| 674 |
+
// selected and splice the result into THAT range on write-back (Bug 2).
|
| 675 |
+
function captureSelection(target) {
|
| 676 |
+
const winSel = window.getSelection();
|
| 677 |
+
|
| 678 |
+
// Prefer the already-tracked editable field (it's what write-back resolves
|
| 679 |
+
// via lastInteractedField); fall back to the right-clicked target. For
|
| 680 |
+
// contenteditable this avoids latching onto a child node of the root.
|
| 681 |
+
let field = null;
|
| 682 |
+
const tracked = lastInteractedField
|
| 683 |
+
|| (isEditableField(document.activeElement) ? document.activeElement : null);
|
| 684 |
+
if (tracked) {
|
| 685 |
+
const ttag = tracked.tagName.toLowerCase();
|
| 686 |
+
if (ttag === 'textarea' || ttag === 'input') field = tracked;
|
| 687 |
+
else if (tracked.isContentEditable && winSel && winSel.rangeCount > 0
|
| 688 |
+
&& tracked.contains(winSel.anchorNode)) field = tracked;
|
| 689 |
+
}
|
| 690 |
+
if (!field && isEditableField(target)) field = target;
|
| 691 |
+
if (!field) { pendingSelection = null; return; }
|
| 692 |
+
|
| 693 |
+
const tag = field.tagName.toLowerCase();
|
| 694 |
+
if (tag === 'textarea' || tag === 'input') {
|
| 695 |
+
const start = field.selectionStart;
|
| 696 |
+
const end = field.selectionEnd;
|
| 697 |
+
if (typeof start === 'number' && typeof end === 'number' && start !== end) {
|
| 698 |
+
pendingSelection = { field, type: 'input', start, end };
|
| 699 |
+
} else {
|
| 700 |
+
pendingSelection = null;
|
| 701 |
+
}
|
| 702 |
+
} else if (field.isContentEditable) {
|
| 703 |
+
if (winSel && winSel.rangeCount > 0 && !winSel.isCollapsed && field.contains(winSel.anchorNode)) {
|
| 704 |
+
pendingSelection = { field, type: 'ce', range: winSel.getRangeAt(0).cloneRange() };
|
| 705 |
+
} else {
|
| 706 |
+
pendingSelection = null;
|
| 707 |
+
}
|
| 708 |
+
} else {
|
| 709 |
+
pendingSelection = null;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Persist a link to the field so write-back can re-find it after focus
|
| 713 |
+
// moves to the side panel, even when this was a plain right-click.
|
| 714 |
+
if (pendingSelection) {
|
| 715 |
+
lastInteractedField = field;
|
| 716 |
+
try { field.dataset.bayanSource = '1'; } catch {}
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
document.addEventListener('contextmenu', (e) => {
|
| 721 |
+
try { captureSelection(e.target); } catch { pendingSelection = null; }
|
| 722 |
+
}, true);
|
| 723 |
+
|
| 724 |
function writeTextToField(field, text, mode, source) {
|
| 725 |
if (!field || typeof text !== 'string') return;
|
| 726 |
|
| 727 |
const tag = field.tagName.toLowerCase();
|
| 728 |
const suppress = NON_CORRECTION_SOURCES.includes(source);
|
| 729 |
|
| 730 |
+
// Resolve the effective mode. 'auto' = replace the captured selection if we
|
| 731 |
+
// have one for THIS field, otherwise replace the whole field.
|
| 732 |
+
const sel = (pendingSelection && pendingSelection.field === field) ? pendingSelection : null;
|
| 733 |
+
let effectiveMode = mode;
|
| 734 |
+
if (mode === 'auto') effectiveMode = sel ? 'replaceSelection' : 'replaceAll';
|
| 735 |
+
|
| 736 |
if (tag === 'textarea' || tag === 'input') {
|
| 737 |
+
if (effectiveMode === 'replaceSelection' && sel && sel.type === 'input') {
|
| 738 |
+
const before = field.value.slice(0, sel.start);
|
| 739 |
+
const after = field.value.slice(sel.end);
|
| 740 |
+
field.value = before + text + after;
|
| 741 |
+
const caret = sel.start + text.length;
|
| 742 |
+
try { field.setSelectionRange(caret, caret); } catch {}
|
| 743 |
+
} else if (effectiveMode === 'replaceSelection'
|
| 744 |
&& typeof field.selectionStart === 'number'
|
| 745 |
&& field.selectionStart !== field.selectionEnd) {
|
| 746 |
+
// Live selection still present (no captured range) — splice into it.
|
| 747 |
const start = field.selectionStart;
|
| 748 |
const end = field.selectionEnd;
|
| 749 |
field.value = field.value.slice(0, start) + text + field.value.slice(end);
|
|
|
|
| 760 |
} else {
|
| 761 |
// contenteditable — mirror the overlay-safe approach used by applyFix.
|
| 762 |
field.focus();
|
| 763 |
+
if (effectiveMode === 'replaceSelection' && sel && sel.type === 'ce') {
|
| 764 |
+
// Restore the captured range, then replace its contents with the text.
|
| 765 |
+
try {
|
| 766 |
+
const winSel = window.getSelection();
|
| 767 |
+
winSel.removeAllRanges();
|
| 768 |
+
winSel.addRange(sel.range);
|
| 769 |
+
sel.range.deleteContents();
|
| 770 |
+
sel.range.insertNode(document.createTextNode(text));
|
| 771 |
+
winSel.collapseToEnd();
|
| 772 |
+
} catch {
|
| 773 |
+
field.textContent = text;
|
| 774 |
+
}
|
| 775 |
+
if (suppress) analysisSuppressed = true;
|
| 776 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 777 |
+
} else if (effectiveMode === 'replaceSelection') {
|
| 778 |
+
const winSel = window.getSelection();
|
| 779 |
+
const hasLive = winSel && winSel.rangeCount > 0 && !winSel.isCollapsed && field.contains(winSel.anchorNode);
|
| 780 |
+
if (hasLive) {
|
| 781 |
+
winSel.getRangeAt(0).deleteContents();
|
| 782 |
+
winSel.getRangeAt(0).insertNode(document.createTextNode(text));
|
| 783 |
+
} else {
|
| 784 |
+
field.textContent = text;
|
| 785 |
+
}
|
| 786 |
+
if (suppress) analysisSuppressed = true;
|
| 787 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 788 |
} else {
|
| 789 |
field.textContent = text;
|
| 790 |
if (suppress) analysisSuppressed = true;
|
|
|
|
| 792 |
}
|
| 793 |
}
|
| 794 |
|
| 795 |
+
// Consume the captured selection + source tag now that the write succeeded.
|
| 796 |
+
pendingSelection = null;
|
| 797 |
try { delete field.dataset.bayanSource; } catch {}
|
| 798 |
}
|
| 799 |
|
extension/sidepanel/sidepanel.css
CHANGED
|
@@ -378,6 +378,49 @@ body {
|
|
| 378 |
color: var(--sp-text-muted);
|
| 379 |
}
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
/* ══════════════════════════════════════════════
|
| 382 |
Highlighted text (from bayan-renderer.js)
|
| 383 |
══════════════════════════════════════════════ */
|
|
|
|
| 378 |
color: var(--sp-text-muted);
|
| 379 |
}
|
| 380 |
|
| 381 |
+
/* ══════════════════════════════════════════════
|
| 382 |
+
Quran verse translation
|
| 383 |
+
══════════════════════════════════════════════ */
|
| 384 |
+
.sp-quran-translate {
|
| 385 |
+
padding: 10px 12px 12px;
|
| 386 |
+
border-top: 1px solid var(--sp-border);
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.sp-quran-translate-row {
|
| 390 |
+
display: flex;
|
| 391 |
+
align-items: center;
|
| 392 |
+
gap: 8px;
|
| 393 |
+
flex-wrap: wrap;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.sp-quran-translate-label {
|
| 397 |
+
font-size: 12px;
|
| 398 |
+
font-weight: 600;
|
| 399 |
+
color: var(--sp-text-secondary);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.sp-select {
|
| 403 |
+
flex: 1;
|
| 404 |
+
min-width: 140px;
|
| 405 |
+
padding: 6px 10px;
|
| 406 |
+
font-size: 13px;
|
| 407 |
+
color: var(--sp-text);
|
| 408 |
+
background: var(--sp-surface);
|
| 409 |
+
border: 1px solid var(--sp-border);
|
| 410 |
+
border-radius: 8px;
|
| 411 |
+
cursor: pointer;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.sp-select:focus {
|
| 415 |
+
outline: none;
|
| 416 |
+
border-color: var(--sp-primary);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.sp-select option {
|
| 420 |
+
background: var(--sp-surface);
|
| 421 |
+
color: var(--sp-text);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
/* ══════════════════════════════════════════════
|
| 425 |
Highlighted text (from bayan-renderer.js)
|
| 426 |
══════════════════════════════════════════════ */
|
extension/sidepanel/sidepanel.html
CHANGED
|
@@ -112,6 +112,9 @@
|
|
| 112 |
<div class="sp-result-header">
|
| 113 |
<span>النص المصحح</span>
|
| 114 |
<div class="sp-result-actions">
|
|
|
|
|
|
|
|
|
|
| 115 |
<button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
|
| 116 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 117 |
</button>
|
|
@@ -246,6 +249,39 @@
|
|
| 246 |
</div>
|
| 247 |
<div class="sp-result-text" id="quran-text" dir="rtl"></div>
|
| 248 |
<div class="sp-summary-meta" id="quran-meta"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
</div>
|
| 250 |
</section>
|
| 251 |
|
|
|
|
| 112 |
<div class="sp-result-header">
|
| 113 |
<span>النص المصحح</span>
|
| 114 |
<div class="sp-result-actions">
|
| 115 |
+
<button class="sp-btn-icon" id="btn-apply-page" title="تطبيق في الصفحة">
|
| 116 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 117 |
+
</button>
|
| 118 |
<button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
|
| 119 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 120 |
</button>
|
|
|
|
| 249 |
</div>
|
| 250 |
<div class="sp-result-text" id="quran-text" dir="rtl"></div>
|
| 251 |
<div class="sp-summary-meta" id="quran-meta"></div>
|
| 252 |
+
|
| 253 |
+
<!-- Verse translation (14 languages) -->
|
| 254 |
+
<div class="sp-quran-translate is-hidden" id="quran-translate-section">
|
| 255 |
+
<div class="sp-quran-translate-row">
|
| 256 |
+
<span class="sp-quran-translate-label">ترجمة الآية</span>
|
| 257 |
+
<select id="quran-lang-select" class="sp-select" dir="ltr">
|
| 258 |
+
<option value="">— اختر لغة —</option>
|
| 259 |
+
<option value="english">🇬🇧 English</option>
|
| 260 |
+
<option value="french">🇫🇷 Français</option>
|
| 261 |
+
<option value="turkish">🇹🇷 Türkçe</option>
|
| 262 |
+
<option value="persian">🇮🇷 فارسی</option>
|
| 263 |
+
<option value="russian">🇷🇺 Русский</option>
|
| 264 |
+
<option value="spanish">🇪🇸 Español</option>
|
| 265 |
+
<option value="german">🇩🇪 Deutsch</option>
|
| 266 |
+
<option value="indonesian">🇮🇩 Indonesia</option>
|
| 267 |
+
<option value="malay">🇲🇾 Melayu</option>
|
| 268 |
+
<option value="bengali">🇧🇩 বাংলা</option>
|
| 269 |
+
<option value="bosnian">🇧🇦 Bosanski</option>
|
| 270 |
+
<option value="portuguese">🇵🇹 Português</option>
|
| 271 |
+
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 272 |
+
</select>
|
| 273 |
+
</div>
|
| 274 |
+
<div class="sp-result is-hidden" id="quran-translation-result" style="margin-top:8px;">
|
| 275 |
+
<div class="sp-result-header">
|
| 276 |
+
<span>الترجمة</span>
|
| 277 |
+
<button class="sp-btn-icon" id="btn-copy-quran-translation" title="نسخ الترجمة">
|
| 278 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
| 279 |
+
</button>
|
| 280 |
+
</div>
|
| 281 |
+
<div class="sp-result-text" id="quran-translation-text" dir="auto"></div>
|
| 282 |
+
<div class="sp-summary-meta" id="quran-translation-ref"></div>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
</div>
|
| 286 |
</section>
|
| 287 |
|
extension/sidepanel/sidepanel.js
CHANGED
|
@@ -27,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 27 |
const btnCorrect = document.getElementById('btn-correct');
|
| 28 |
const btnClear = document.getElementById('btn-clear');
|
| 29 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
|
|
|
| 30 |
const btnCopyResult = document.getElementById('btn-copy-result');
|
| 31 |
const scoreSection = document.getElementById('score-section');
|
| 32 |
const resultSection = document.getElementById('result-section');
|
|
@@ -155,7 +156,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 155 |
// directly; it relays through background.js. `source` lets the content
|
| 156 |
// script decide whether to re-analyze (correct) or suppress (Change 3).
|
| 157 |
// ══════════════════════════════════════════════════════════
|
| 158 |
-
function writeBackToPage(text, mode = '
|
| 159 |
try {
|
| 160 |
chrome.runtime.sendMessage(
|
| 161 |
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source },
|
|
@@ -234,7 +235,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 234 |
renderSuggestions(currentSuggestions);
|
| 235 |
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 236 |
saveState();
|
| 237 |
-
writeBackToPage(analyzedText, 'replaceAll', 'correct');
|
| 238 |
showToast('✓ تم التصحيح');
|
| 239 |
});
|
| 240 |
});
|
|
@@ -379,10 +379,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 379 |
updateScore(0, 0, 0);
|
| 380 |
renderSuggestions([]);
|
| 381 |
saveState();
|
| 382 |
-
writeBackToPage(analyzedText, '
|
| 383 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 384 |
});
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
btnCopyResult.addEventListener('click', () => {
|
| 387 |
const text = resultText.textContent || '';
|
| 388 |
navigator.clipboard.writeText(text)
|
|
@@ -478,6 +492,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 478 |
const quranText = document.getElementById('quran-text');
|
| 479 |
const quranMeta = document.getElementById('quran-meta');
|
| 480 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
if (quranInput) {
|
| 483 |
quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
|
|
@@ -490,6 +520,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 490 |
try {
|
| 491 |
const data = await bayanQuran(text);
|
| 492 |
quranResultSection.classList.remove('is-hidden');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
if (data.error) {
|
| 494 |
quranText.textContent = data.error;
|
| 495 |
quranMeta.textContent = '';
|
|
@@ -498,6 +532,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 498 |
quranMeta.textContent = data.matched_segment && data.full_verse
|
| 499 |
? `المقطع المطابق: ${data.matched_segment}`
|
| 500 |
: '';
|
|
|
|
|
|
|
|
|
|
| 501 |
showToast('✓ تم التدقيق');
|
| 502 |
}
|
| 503 |
} catch (error) {
|
|
@@ -508,6 +545,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 508 |
}
|
| 509 |
});
|
| 510 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
btnCopyQuran.addEventListener('click', () => {
|
| 512 |
navigator.clipboard.writeText(quranText.textContent || '')
|
| 513 |
.then(() => showToast('✓ تم النسخ'))
|
|
@@ -622,7 +695,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 622 |
btn.addEventListener('click', () => {
|
| 623 |
const text = (getText() || '').trim();
|
| 624 |
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 625 |
-
writeBackToPage(text, '
|
| 626 |
});
|
| 627 |
anchorBtn.parentElement.appendChild(btn);
|
| 628 |
}
|
|
|
|
| 27 |
const btnCorrect = document.getElementById('btn-correct');
|
| 28 |
const btnClear = document.getElementById('btn-clear');
|
| 29 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
| 30 |
+
const btnApplyPage = document.getElementById('btn-apply-page');
|
| 31 |
const btnCopyResult = document.getElementById('btn-copy-result');
|
| 32 |
const scoreSection = document.getElementById('score-section');
|
| 33 |
const resultSection = document.getElementById('result-section');
|
|
|
|
| 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 },
|
|
|
|
| 235 |
renderSuggestions(currentSuggestions);
|
| 236 |
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 237 |
saveState();
|
|
|
|
| 238 |
showToast('✓ تم التصحيح');
|
| 239 |
});
|
| 240 |
});
|
|
|
|
| 379 |
updateScore(0, 0, 0);
|
| 380 |
renderSuggestions([]);
|
| 381 |
saveState();
|
| 382 |
+
writeBackToPage(analyzedText, 'auto', 'correct');
|
| 383 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 384 |
});
|
| 385 |
|
| 386 |
+
// Explicit "apply corrected text to the page field" button (Bug 1).
|
| 387 |
+
// Writes the current corrected text — with any still-pending suggestions
|
| 388 |
+
// applied — back into the source field, honouring selection vs whole-field.
|
| 389 |
+
if (btnApplyPage) {
|
| 390 |
+
btnApplyPage.addEventListener('click', () => {
|
| 391 |
+
if (!analyzedText) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 392 |
+
if (isStale) { showToast('⚠ أعد التحليل أولاً — النص تغيّر'); return; }
|
| 393 |
+
const finalText = currentSuggestions.length > 0
|
| 394 |
+
? applyAllPatches(analyzedText, currentSuggestions)
|
| 395 |
+
: analyzedText;
|
| 396 |
+
writeBackToPage(finalText, 'auto', 'correct');
|
| 397 |
+
});
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
btnCopyResult.addEventListener('click', () => {
|
| 401 |
const text = resultText.textContent || '';
|
| 402 |
navigator.clipboard.writeText(text)
|
|
|
|
| 492 |
const quranText = document.getElementById('quran-text');
|
| 493 |
const quranMeta = document.getElementById('quran-meta');
|
| 494 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
| 495 |
+
// Translation sub-UI
|
| 496 |
+
const quranTranslateSection = document.getElementById('quran-translate-section');
|
| 497 |
+
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 498 |
+
const quranTranslationResult = document.getElementById('quran-translation-result');
|
| 499 |
+
const quranTranslationText = document.getElementById('quran-translation-text');
|
| 500 |
+
const quranTranslationRef = document.getElementById('quran-translation-ref');
|
| 501 |
+
const btnCopyQuranTranslation = document.getElementById('btn-copy-quran-translation');
|
| 502 |
+
let lastQuranQuery = '';
|
| 503 |
+
|
| 504 |
+
// Parse the API's "(verse text) 【surah:ayah】" segment into {text, ref}.
|
| 505 |
+
function parseQuranSegment(seg) {
|
| 506 |
+
seg = seg || '';
|
| 507 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 508 |
+
const text = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 509 |
+
return { text, ref: refMatch ? refMatch[1] : '' };
|
| 510 |
+
}
|
| 511 |
|
| 512 |
if (quranInput) {
|
| 513 |
quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
|
|
|
|
| 520 |
try {
|
| 521 |
const data = await bayanQuran(text);
|
| 522 |
quranResultSection.classList.remove('is-hidden');
|
| 523 |
+
// Reset translation UI for the new query.
|
| 524 |
+
if (quranTranslateSection) quranTranslateSection.classList.add('is-hidden');
|
| 525 |
+
if (quranTranslationResult) quranTranslationResult.classList.add('is-hidden');
|
| 526 |
+
if (quranLangSelect) quranLangSelect.value = '';
|
| 527 |
if (data.error) {
|
| 528 |
quranText.textContent = data.error;
|
| 529 |
quranMeta.textContent = '';
|
|
|
|
| 532 |
quranMeta.textContent = data.matched_segment && data.full_verse
|
| 533 |
? `المقطع المطابق: ${data.matched_segment}`
|
| 534 |
: '';
|
| 535 |
+
// Enable translation now that we have a verified verse.
|
| 536 |
+
lastQuranQuery = text;
|
| 537 |
+
if (quranTranslateSection) quranTranslateSection.classList.remove('is-hidden');
|
| 538 |
showToast('✓ تم التدقيق');
|
| 539 |
}
|
| 540 |
} catch (error) {
|
|
|
|
| 545 |
}
|
| 546 |
});
|
| 547 |
|
| 548 |
+
// Translate the verified verse into the chosen language.
|
| 549 |
+
if (quranLangSelect) {
|
| 550 |
+
quranLangSelect.addEventListener('change', async () => {
|
| 551 |
+
const lang = quranLangSelect.value;
|
| 552 |
+
if (!lang || !lastQuranQuery) return;
|
| 553 |
+
|
| 554 |
+
setLoading(true, 'جارٍ الترجمة...');
|
| 555 |
+
try {
|
| 556 |
+
const data = await bayanQuran(lastQuranQuery, lang);
|
| 557 |
+
quranTranslationResult.classList.remove('is-hidden');
|
| 558 |
+
if (data.error) {
|
| 559 |
+
quranTranslationText.textContent = data.error;
|
| 560 |
+
quranTranslationRef.textContent = '';
|
| 561 |
+
} else {
|
| 562 |
+
const parsed = parseQuranSegment(data.matched_segment || data.full_verse || '');
|
| 563 |
+
quranTranslationText.textContent = parsed.text || data.full_verse || '';
|
| 564 |
+
quranTranslationRef.textContent = parsed.ref ? `[${parsed.ref}]` : '';
|
| 565 |
+
showToast('✓ تمت الترجمة');
|
| 566 |
+
}
|
| 567 |
+
} catch (error) {
|
| 568 |
+
console.error('[Bayan SP] Quran translation error:', error);
|
| 569 |
+
showToast('خطأ في الاتصال — تحقق من الإنترنت');
|
| 570 |
+
} finally {
|
| 571 |
+
setLoading(false);
|
| 572 |
+
}
|
| 573 |
+
});
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
if (btnCopyQuranTranslation) {
|
| 577 |
+
btnCopyQuranTranslation.addEventListener('click', () => {
|
| 578 |
+
navigator.clipboard.writeText(quranTranslationText.textContent || '')
|
| 579 |
+
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 580 |
+
.catch(() => showToast('تعذّر النسخ'));
|
| 581 |
+
});
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
btnCopyQuran.addEventListener('click', () => {
|
| 585 |
navigator.clipboard.writeText(quranText.textContent || '')
|
| 586 |
.then(() => showToast('✓ تم النسخ'))
|
|
|
|
| 695 |
btn.addEventListener('click', () => {
|
| 696 |
const text = (getText() || '').trim();
|
| 697 |
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 698 |
+
writeBackToPage(text, 'auto', source);
|
| 699 |
});
|
| 700 |
anchorBtn.parentElement.appendChild(btn);
|
| 701 |
}
|