first commit
This commit is contained in:
212
content.js
Normal file
212
content.js
Normal file
@@ -0,0 +1,212 @@
|
||||
// Prevent double-install (if injected by manifest + programmatically)
|
||||
if (window.__dewemoji_cs_installed) {
|
||||
// already installed in this page context
|
||||
// Optional: console.debug('[Dewemoji] content.js already installed');
|
||||
} else {
|
||||
window.__dewemoji_cs_installed = true;
|
||||
// (keep the rest of your file below as-is)
|
||||
|
||||
let lastEditable = null; // HTMLElement (input/textarea/contenteditable root)
|
||||
let lastRange = null; // Range for contenteditable
|
||||
let lastStart = 0, lastEnd = 0;
|
||||
|
||||
let lastInsertOK = null; // true | false | null
|
||||
let lastInsertMessage = ""; // short note like "Inserted into input"
|
||||
let _insertingLock = false;
|
||||
|
||||
// Find nearest contenteditable root
|
||||
function nearestEditableRoot(node) {
|
||||
let el = node instanceof Element ? node : node?.parentElement;
|
||||
while (el) {
|
||||
if (el.isContentEditable) return el;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isTextInput(el) {
|
||||
return el && (
|
||||
el.tagName === 'TEXTAREA' ||
|
||||
(el.tagName === 'INPUT' && /^(text|search|email|url|tel|password|number)?$/i.test(el.type))
|
||||
);
|
||||
}
|
||||
|
||||
function captureCaret() {
|
||||
const active = document.activeElement;
|
||||
if (isTextInput(active)) {
|
||||
lastEditable = active;
|
||||
lastStart = active.selectionStart ?? active.value.length;
|
||||
lastEnd = active.selectionEnd ?? active.value.length;
|
||||
lastRange = null; // not used for inputs
|
||||
return;
|
||||
}
|
||||
|
||||
const root = nearestEditableRoot(active);
|
||||
if (root) {
|
||||
lastEditable = root;
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
// clone so we can restore after panel steals focus
|
||||
lastRange = sel.getRangeAt(0).cloneRange();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not in an editable element
|
||||
lastEditable = null;
|
||||
lastRange = null;
|
||||
}
|
||||
|
||||
captureCaret();
|
||||
|
||||
document.addEventListener('focusin', captureCaret, true);
|
||||
document.addEventListener('keyup', captureCaret, true);
|
||||
document.addEventListener('mouseup', captureCaret, true);
|
||||
document.addEventListener('selectionchange', captureCaret, true);
|
||||
|
||||
// Attempt to insert text at the last saved position
|
||||
function insertAtCaret(text) {
|
||||
if (!text) return false;
|
||||
|
||||
// guard: ignore if a prior insert is still finishing
|
||||
if (_insertingLock) return false;
|
||||
_insertingLock = true;
|
||||
setTimeout(() => { _insertingLock = false; }, 180);
|
||||
|
||||
// --- RECOVERY: if we don't have a saved target, try to rebuild from current selection
|
||||
if (!lastEditable || !document.contains(lastEditable)) {
|
||||
const sel = window.getSelection && window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
const root = nearestEditableRoot(sel.anchorNode);
|
||||
if (root) {
|
||||
lastEditable = root;
|
||||
lastRange = sel.getRangeAt(0).cloneRange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastEditable || !document.contains(lastEditable)){
|
||||
lastInsertOK = false;
|
||||
lastInsertMessage = "No editable target";
|
||||
return false;
|
||||
};
|
||||
|
||||
// Re-focus target first (side panel blurred it)
|
||||
lastEditable.focus();
|
||||
|
||||
// (A) Plain inputs / textareas
|
||||
if (isTextInput(lastEditable)) {
|
||||
const el = lastEditable;
|
||||
const val = el.value ?? '';
|
||||
const start = el.selectionStart ?? lastStart ?? val.length;
|
||||
const end = el.selectionEnd ?? lastEnd ?? start;
|
||||
el.value = val.slice(0, start) + text + val.slice(end);
|
||||
const pos = start + text.length;
|
||||
try { el.setSelectionRange(pos, pos); } catch {}
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
// console.log('[Dewemoji DEBUG] Inserted into input/textarea');
|
||||
// when insertion succeeds in input/textarea
|
||||
lastInsertOK = true;
|
||||
lastInsertMessage = "Inserted into input/textarea";
|
||||
return true;
|
||||
}
|
||||
|
||||
// (B) contenteditable roots (Lexical/Draft/React)
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (lastRange) {
|
||||
// restore exact caret
|
||||
const r = lastRange.cloneRange();
|
||||
sel.addRange(r);
|
||||
} else {
|
||||
// fallback: caret at end of root
|
||||
const r = document.createRange();
|
||||
r.selectNodeContents(lastEditable);
|
||||
r.collapse(false);
|
||||
sel.addRange(r);
|
||||
}
|
||||
|
||||
// First try execCommand (still supported by many editors)
|
||||
let insertedCE = document.execCommand('insertText', false, text);
|
||||
if (!insertedCE) {
|
||||
// Manual Range insertion
|
||||
if (sel.rangeCount > 0) {
|
||||
const r = sel.getRangeAt(0);
|
||||
r.deleteContents();
|
||||
r.insertNode(document.createTextNode(text));
|
||||
// move caret to end
|
||||
r.setStart(r.endContainer, r.endOffset);
|
||||
r.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(r);
|
||||
insertedCE = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
lastEditable.dispatchEvent(new InputEvent('input', {
|
||||
data: text,
|
||||
inputType: 'insertText',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true
|
||||
}));
|
||||
} catch {
|
||||
lastEditable.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
captureCaret(); // refresh snapshot
|
||||
|
||||
// mark success if we inserted either way
|
||||
if (insertedCE) {
|
||||
lastInsertOK = true;
|
||||
lastInsertMessage = "Inserted into contenteditable";
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we got here, we didn't manage to insert
|
||||
lastInsertOK = false;
|
||||
lastInsertMessage = "Insert failed in contenteditable";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Message bridge
|
||||
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
|
||||
if (msg?.type === 'dewemoji_ping') {
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
if (msg?.type === 'dewemoji_insert') {
|
||||
const ok = insertAtCaret(msg.text);
|
||||
sendResponse({ ok });
|
||||
return;
|
||||
}
|
||||
if (msg?.type === 'dewemoji_diag') {
|
||||
// Report what we currently know
|
||||
const active = document.activeElement;
|
||||
const type =
|
||||
active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT')
|
||||
? active.tagName.toLowerCase()
|
||||
: (active && active.isContentEditable) ? 'contenteditable' : null;
|
||||
|
||||
let hasRange = false;
|
||||
if (type === 'contenteditable') {
|
||||
const sel = window.getSelection();
|
||||
hasRange = !!(sel && sel.rangeCount > 0);
|
||||
} else if (type) {
|
||||
hasRange = true; // inputs/textarea use selectionStart/End
|
||||
}
|
||||
|
||||
sendResponse({
|
||||
ok: true,
|
||||
activeType: type, // 'input' | 'textarea' | 'contenteditable' | null
|
||||
hasRange, // whether we have a caret/selection
|
||||
lastInsertOK, // last insertion result (or null if none yet)
|
||||
lastInsertMessage // text summary
|
||||
});
|
||||
return; // sync
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user