| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| (function () { |
| 'use strict'; |
|
|
| if (window.location.protocol === 'chrome-extension:') return; |
|
|
| if (typeof BayanController === 'undefined') { |
| console.error('[Bayan] BayanController not loaded.'); |
| return; |
| } |
|
|
| |
| |
| |
| let ttPolicy = null; |
| try { |
| if (window.trustedTypes?.createPolicy) { |
| ttPolicy = window.trustedTypes.createPolicy('bayan-inline-policy', { |
| createHTML: (input) => input, |
| }); |
| } |
| } catch {} |
|
|
| function safeHTML(el, html) { |
| try { |
| el.innerHTML = ttPolicy ? ttPolicy.createHTML(html) : html; |
| } catch { |
| el.textContent = ''; |
| const t = document.createElement('template'); |
| t.innerHTML = html; |
| el.appendChild(t.content.cloneNode(true)); |
| } |
| } |
|
|
| |
| |
| |
| const IS_PROTECTED = BayanController.isProtectedSite(); |
|
|
| let activeField = null; |
| let lastAnalyzedText = ''; |
| let suggestions = []; |
| let paused = false; |
| let floatingBtn = null; |
| let tooltip = null; |
| let overlayContainer = null; |
| let badgeCount = null; |
| let observer = null; |
|
|
| |
| |
| |
|
|
| function isEditableField(el) { |
| if (!el?.tagName) return false; |
| const tag = el.tagName.toLowerCase(); |
| if (tag === 'textarea') return true; |
| if (tag === 'input') { |
| const type = (el.type || '').toLowerCase(); |
| return ['text', 'search', 'url', ''].includes(type); |
| } |
| |
| if (el.isContentEditable && !IS_PROTECTED) return true; |
| return false; |
| } |
|
|
| function getFieldText(field) { |
| if (!field) return ''; |
| const tag = field.tagName.toLowerCase(); |
| if (tag === 'textarea' || tag === 'input') return field.value || ''; |
| return field.innerText || field.textContent || ''; |
| } |
|
|
| |
| |
| |
|
|
| function onFieldInput() { |
| if (paused || !activeField) return; |
|
|
| const text = getFieldText(activeField); |
|
|
| if (!BayanController.hasArabic(text)) { |
| clearHighlights(); |
| updateBadge(0); |
| return; |
| } |
|
|
| updateBadge(-1); |
|
|
| BayanController.scheduleAnalysis( |
| text, |
| (result) => { |
| try { onAnalysisResult(result); } catch (err) { |
| console.warn('[Bayan] Callback error:', err.message); |
| handleFailure('callback_error'); |
| } |
| }, |
| () => getFieldText(activeField) |
| ); |
| } |
|
|
| function onAnalysisResult(data) { |
| if (!activeField) return; |
|
|
| if (!data) { |
| updateBadge(0); |
| return; |
| } |
|
|
| suggestions = data.suggestions || []; |
| lastAnalyzedText = data.original || getFieldText(activeField); |
| updateBadge(suggestions.length); |
|
|
| if (suggestions.length > 0) { |
| renderOverlay(activeField, lastAnalyzedText, suggestions); |
| } else { |
| clearHighlights(); |
| } |
| } |
|
|
| |
| |
| |
|
|
| function handleFailure(reason) { |
| paused = true; |
| clearHighlights(); |
| BayanController.cancelAll(); |
| if (badgeCount) { |
| badgeCount.textContent = '⏸'; |
| badgeCount.className = 'bayan-il-badge bayan-il-badge--paused'; |
| } |
| console.warn(`[Bayan] Inline paused: ${reason}. Side Panel still works.`); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| function renderOverlay(field, originalText, suggs) { |
| clearHighlights(); |
| if (!field || !suggs?.length) return; |
|
|
| try { |
| const rect = field.getBoundingClientRect(); |
| const cs = window.getComputedStyle(field); |
|
|
| overlayContainer = document.createElement('div'); |
| overlayContainer.className = 'bayan-il-overlay'; |
| overlayContainer.style.cssText = `position:absolute;top:${rect.top + window.scrollY}px;` |
| + `left:${rect.left + window.scrollX}px;width:${rect.width}px;height:${rect.height}px;` |
| + `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};` |
| + `padding:${cs.padding};border:${cs.border};border-color:transparent;` |
| + `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;` |
| + `pointer-events:none;z-index:2147483645;box-sizing:border-box;` |
| + `white-space:pre-wrap;word-wrap:break-word;color:transparent;`; |
|
|
| const sorted = [...suggs].sort((a, b) => a.start - b.start); |
| let html = ''; |
| let pos = 0; |
|
|
| sorted.forEach((s) => { |
| if (pos < s.start) html += esc(originalText.slice(pos, s.start)); |
| html += `<span class="${errCls(s.type)} bayan-il-error bayan-il-overlay-mark" ` |
| + `data-bayan-sid="${s.id || ''}" data-bayan-original="${esc(s.original)}" ` |
| + `data-bayan-correction="${esc(s.correction)}" data-bayan-type="${s.type}" ` |
| + `data-bayan-start="${s.start}" data-bayan-end="${s.end}" ` |
| + `style="pointer-events:auto;cursor:pointer;">` |
| + `${esc(originalText.slice(s.start, s.end))}</span>`; |
| pos = s.end; |
| }); |
| if (pos < originalText.length) html += esc(originalText.slice(pos)); |
|
|
| safeHTML(overlayContainer, html); |
| overlayContainer.scrollTop = field.scrollTop; |
| overlayContainer.scrollLeft = field.scrollLeft; |
| document.body.appendChild(overlayContainer); |
|
|
| overlayContainer.addEventListener('mousedown', (e) => e.preventDefault()); |
|
|
| field.addEventListener('scroll', syncOverlay); |
|
|
| overlayContainer.querySelectorAll('.bayan-il-overlay-mark').forEach((mark) => { |
| mark.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| showTooltip(mark, activeField); |
| }); |
| }); |
| } catch (err) { |
| console.warn('[Bayan] Overlay error:', err.message); |
| handleFailure('render_error'); |
| } |
| } |
|
|
| function syncOverlay() { |
| if (overlayContainer && activeField) { |
| overlayContainer.scrollTop = activeField.scrollTop; |
| overlayContainer.scrollLeft = activeField.scrollLeft; |
| } |
| } |
|
|
| function clearHighlights() { |
| if (overlayContainer) { overlayContainer.remove(); overlayContainer = null; } |
| hideTooltip(); |
| } |
|
|
| |
| |
| |
|
|
| function showTooltip(el, field) { |
| hideTooltip(); |
|
|
| const original = el.dataset.bayanOriginal; |
| const correction = el.dataset.bayanCorrection; |
| const type = el.dataset.bayanType; |
| const start = parseInt(el.dataset.bayanStart, 10); |
| const end = parseInt(el.dataset.bayanEnd, 10); |
| const sid = el.dataset.bayanSid; |
|
|
| if (!original || !correction) return; |
|
|
| const suggestion = { id: sid, original, correction, type, start, end }; |
| const typeLabels = { spelling: 'إملائي', grammar: 'نحوي', punctuation: 'ترقيم' }; |
|
|
| tooltip = document.createElement('div'); |
| tooltip.className = 'bayan-il-tooltip'; |
| tooltip.dir = 'rtl'; |
|
|
| safeHTML(tooltip, ` |
| <div class="bayan-il-tooltip-header"> |
| <span class="bayan-il-tooltip-badge bayan-il-badge-${type}">${typeLabels[type] || type}</span> |
| <button class="bayan-il-tooltip-close" title="إغلاق">✕</button> |
| </div> |
| <div class="bayan-il-tooltip-body"> |
| <span class="bayan-il-tooltip-original">${esc(original)}</span> |
| <span class="bayan-il-tooltip-arrow">←</span> |
| <span class="bayan-il-tooltip-correction">${correction ? esc(correction) : '<s style="opacity:0.5">حذف</s>'}</span> |
| </div> |
| <div class="bayan-il-tooltip-actions"> |
| <button class="bayan-il-tooltip-apply" data-action="apply">تطبيق</button> |
| <button class="bayan-il-tooltip-ignore" data-action="ignore">تجاهل</button> |
| </div> |
| `); |
|
|
| document.body.appendChild(tooltip); |
|
|
| const r = el.getBoundingClientRect(); |
| tooltip.style.top = `${r.bottom + window.scrollY + 6}px`; |
| tooltip.style.left = `${r.left + window.scrollX}px`; |
|
|
| requestAnimationFrame(() => { |
| if (!tooltip) return; |
| const tr = tooltip.getBoundingClientRect(); |
| if (tr.right > window.innerWidth) tooltip.style.left = `${window.innerWidth - tr.width - 8}px`; |
| tooltip.classList.add('bayan-il-tooltip--visible'); |
| }); |
|
|
| tooltip.querySelector('[data-action="apply"]').addEventListener('click', () => { |
| applyFix(field, suggestion); |
| hideTooltip(); |
| }); |
|
|
| tooltip.querySelector('[data-action="ignore"]').addEventListener('click', () => { |
| dismissSuggestion(suggestion); |
| hideTooltip(); |
| }); |
|
|
| tooltip.querySelector('.bayan-il-tooltip-close').addEventListener('click', () => hideTooltip()); |
|
|
| setTimeout(() => document.addEventListener('click', outsideClick, { once: true }), 100); |
| } |
|
|
| function outsideClick(e) { |
| if (tooltip && !tooltip.contains(e.target)) hideTooltip(); |
| } |
|
|
| function hideTooltip() { |
| if (tooltip) { tooltip.remove(); tooltip = null; } |
| } |
|
|
| |
| |
| |
|
|
| function applyFix(field, suggestion) { |
| if (!field) return; |
|
|
| const isInput = field.tagName.toLowerCase() === 'textarea' || field.tagName.toLowerCase() === 'input'; |
| const text = isInput ? field.value : getFieldText(field); |
| const before = text.substring(0, suggestion.start); |
| const after = text.substring(suggestion.end); |
| const newText = before + suggestion.correction + after; |
|
|
| const delta = suggestion.correction.length - (suggestion.end - suggestion.start); |
| suggestions = suggestions |
| .filter((s) => s.id !== suggestion.id) |
| .map((s) => s.start >= suggestion.end ? { ...s, start: s.start + delta, end: s.end + delta } : s); |
|
|
| lastAnalyzedText = newText; |
|
|
| if (isInput) { |
| field.value = newText; |
| field.dispatchEvent(new Event('input', { bubbles: true })); |
| } else { |
| |
| |
| field.textContent = newText; |
| } |
|
|
| if (suggestions.length > 0) { |
| renderOverlay(field, newText, suggestions); |
| } else { |
| clearHighlights(); |
| |
| |
| setTimeout(() => onFieldInput(), 500); |
| } |
| updateBadge(suggestions.length); |
| } |
|
|
| function dismissSuggestion(suggestion) { |
| suggestions = suggestions.filter((s) => s.id !== suggestion.id); |
| if (suggestions.length > 0) { |
| renderOverlay(activeField, lastAnalyzedText, suggestions); |
| } else { |
| clearHighlights(); |
| } |
| updateBadge(suggestions.length); |
| } |
|
|
| |
| |
| |
|
|
| function createFloatingBtn() { |
| if (floatingBtn) return; |
|
|
| floatingBtn = document.createElement('div'); |
| floatingBtn.className = 'bayan-il-fab'; |
| safeHTML(floatingBtn, ` |
| <svg width="18" height="18" viewBox="0 0 100 100" fill="none"> |
| <circle cx="50" cy="50" r="46" fill="url(#blGrad)" /> |
| <path d="M30 55 Q35 35, 50 30 Q65 35, 70 55 Q65 65, 50 70 Q35 65, 30 55Z" fill="rgba(255,255,255,0.9)" /> |
| <circle cx="50" cy="42" r="4" fill="url(#blGrad)" /> |
| <defs> |
| <linearGradient id="blGrad" x1="0" y1="0" x2="100" y2="100"> |
| <stop offset="0%" stop-color="#6366f1"/> |
| <stop offset="100%" stop-color="#8b5cf6"/> |
| </linearGradient> |
| </defs> |
| </svg> |
| <span class="bayan-il-badge">0</span> |
| `); |
| floatingBtn.title = 'Bayan — بيان'; |
| document.body.appendChild(floatingBtn); |
| badgeCount = floatingBtn.querySelector('.bayan-il-badge'); |
|
|
| floatingBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| if (paused) { |
| paused = false; |
| updateBadge(0); |
| onFieldInput(); |
| return; |
| } |
| if (suggestions.length > 0) { |
| try { |
| chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText }); |
| } catch {} |
| } |
| }); |
| } |
|
|
| function positionFab(field) { |
| if (!floatingBtn || !field) return; |
| const rect = field.getBoundingClientRect(); |
| floatingBtn.style.top = `${Math.max(4, rect.top + window.scrollY + 6)}px`; |
| floatingBtn.style.left = `${Math.max(4, rect.left + window.scrollX + 6)}px`; |
| floatingBtn.classList.add('bayan-il-fab--visible'); |
| } |
|
|
| function updateBadge(count) { |
| if (!badgeCount) return; |
| if (count === -1) { |
| badgeCount.textContent = '…'; |
| badgeCount.className = 'bayan-il-badge bayan-il-badge--analyzing'; |
| } else if (count === 0) { |
| badgeCount.textContent = '✓'; |
| badgeCount.className = 'bayan-il-badge bayan-il-badge--clean'; |
| } else { |
| badgeCount.textContent = String(count); |
| badgeCount.className = 'bayan-il-badge bayan-il-badge--errors'; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function esc(text) { |
| const m = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; |
| return String(text).replace(/[&<>"']/g, (c) => m[c]); |
| } |
|
|
| function errCls(type) { |
| return { spelling: 'bayan-il-spelling', grammar: 'bayan-il-grammar', punctuation: 'bayan-il-punctuation' }[type] || 'bayan-il-spelling'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
|
|
| function attachField(field) { |
| if (activeField === field) return; |
| detachField(); |
|
|
| activeField = field; |
| suggestions = []; |
| if (paused) paused = false; |
|
|
| createFloatingBtn(); |
| positionFab(field); |
|
|
| field.addEventListener('input', onFieldInput); |
| |
|
|
| if (BayanController.hasArabic(getFieldText(field))) { |
| onFieldInput(); |
| } |
| } |
|
|
| function detachField() { |
| if (activeField) { |
| activeField.removeEventListener('input', onFieldInput); |
| |
| activeField.removeEventListener('scroll', syncOverlay); |
| } |
| clearHighlights(); |
| BayanController.cancelAll(); |
| activeField = null; |
| suggestions = []; |
| if (floatingBtn) floatingBtn.classList.remove('bayan-il-fab--visible'); |
| } |
|
|
| |
| |
| |
|
|
| document.addEventListener('focusin', (e) => { |
| if (isEditableField(e.target)) attachField(e.target); |
| }, true); |
|
|
| document.addEventListener('focusout', () => { |
| setTimeout(() => { |
| if (!activeField) return; |
| if (document.activeElement === activeField) return; |
| const a = document.activeElement; |
| if (tooltip?.contains(a)) return; |
| if (floatingBtn?.contains(a)) return; |
| if (overlayContainer?.contains(a)) return; |
| if (document.querySelector('.bayan-il-tooltip')) return; |
| detachField(); |
| }, 300); |
| }, true); |
|
|
| window.addEventListener('scroll', () => { |
| if (activeField && floatingBtn) positionFab(activeField); |
| if (overlayContainer && activeField) { |
| const rect = activeField.getBoundingClientRect(); |
| overlayContainer.style.top = `${rect.top + window.scrollY}px`; |
| overlayContainer.style.left = `${rect.left + window.scrollX}px`; |
| } |
| }, { passive: true }); |
|
|
| window.addEventListener('resize', () => { |
| if (activeField && floatingBtn) positionFab(activeField); |
| }, { passive: true }); |
|
|
| |
| observer = new MutationObserver(() => { |
| if (activeField && !document.body.contains(activeField)) detachField(); |
| }); |
| observer.observe(document.body, { childList: true, subtree: true }); |
|
|
| |
| const mode = IS_PROTECTED ? 'protected' : 'full'; |
| console.log(`[Bayan] Inline engine v7.1 (mode: ${mode}, TT: ${ttPolicy ? 'yes' : 'no'})`); |
| })(); |
|
|