Skip to content

Instantly share code, notes, and snippets.

@zengxs
Created November 3, 2025 07:27
Show Gist options
  • Select an option

  • Save zengxs/eb9ac78878daae1bb375bd3c2427ff65 to your computer and use it in GitHub Desktop.

Select an option

Save zengxs/eb9ac78878daae1bb375bd3c2427ff65 to your computer and use it in GitHub Desktop.
A polyfill to fix input method (IME) problems for Avalonia apps running on WebAssembly in macOS browsers.
(function () {
// 仅在 Apple 浏览器上启用
if (!navigator.userAgent.match(/(macintosh|macintel|macppc|mac68k|macos)/i)) return;
let stateMap = new WeakMap();
function getState(el) {
let s = stateMap.get(el);
if (!s) {
s = {composing: false, lastData: null};
stateMap.set(el, s);
}
return s;
}
function isAvaloniaImeElement(t) {
return t instanceof HTMLElement && t.classList && t.classList.contains('avalonia-input-element');
}
function isPrintableKey(e) {
return typeof e.key === 'string' && e.key.length === 1;
}
function isDeleteKey(e) {
return e.key === 'Backspace' || e.key === 'Delete';
}
function handleCompositionStart(e) {
if (!isAvaloniaImeElement(e.target)) return;
let st = getState(e.target);
st.composing = true;
st.lastData = null;
}
function handleBeforeInput(e) {
if (!isAvaloniaImeElement(e.target)) return;
let st = getState(e.target);
let it = e.inputType || '';
let data = (typeof e.data === 'string') ? e.data : '';
// 统一用 beforeinput 的插入型 inputType 提交字符
if ((it === 'insertText' || it === 'insertFromComposition' || it === 'insertReplacementText') && data) {
st.lastData = data;
queueMicrotask(function () {
let cur = getState(e.target);
let commit = cur.lastData || '';
let ev = new CompositionEvent('compositionend', {data: commit, bubbles: true, cancelable: true});
e.target.dispatchEvent(ev);
// 结束(或保持非组合)状态,清理缓存,避免重复提交
cur.composing = false;
cur.lastData = null;
});
}
// 删除型 inputType(例如 deleteCompositionText)无需我们做额外动作,
// 浏览器会通过 compositionupdate 更新预编辑文本;关键是不要让 keydown 传到 Avalonia。
}
function handleCompositionEnd(e) {
if (!isAvaloniaImeElement(e.target)) return;
let st = getState(e.target);
// 无论原生还是合成的 end,统一清理状态,交由 Avalonia 监听器处理提交
st.composing = false;
st.lastData = null;
}
// 捕获阶段拦截 keydown:避免首字符泄漏;组合中也拦截删除键,避免删实际内容
function keydownCapture(e) {
let active = document.activeElement;
if (!isAvaloniaImeElement(active)) return;
// 有 Ctrl/Alt/Meta 时不拦截,保留快捷键
if (e.ctrlKey || e.altKey || e.metaKey) return;
let st = getState(active);
// 1) 始终拦截可打印字符,避免 Avalonia 的 RawTextEvent 回退(首键泄漏)
if (isPrintableKey(e)) {
e.stopImmediatePropagation();
return;
}
// 2) 仅在组合进行中,拦截删除键,避免删实际内容;让 IME 自己处理预编辑删除
if (st.composing && isDeleteKey(e)) {
e.stopImmediatePropagation();
return;
}
}
function attach(root) {
root.addEventListener('compositionstart', handleCompositionStart, true);
root.addEventListener('beforeinput', handleBeforeInput, true);
root.addEventListener('compositionend', handleCompositionEnd, true);
root.addEventListener('keydown', keydownCapture, true);
}
function waitAndAttach() {
if (document.querySelector('.avalonia-input-element')) {
attach(document);
return;
}
let obs = new MutationObserver(function () {
if (document.querySelector('.avalonia-input-element')) {
try {
attach(document);
} finally {
obs.disconnect();
}
}
});
obs.observe(document.documentElement, {childList: true, subtree: true});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitAndAttach, {once: true});
} else {
waitAndAttach();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment