Files
SillyTavern/public/scripts/browser-fixes.js
T
Christoph 29e3136473 fix: Don't apply layout hack in Firefox Mobile while editing text (#5531)
This fixes the following bug in Firefox Mobile on Android:

**Steps to reproduce:**

1. Swipe a word into the chat input edit field in Firefox Mobile on Android, for examples "Test".
2. In GBoard (default Android keyboard), tap on a different suggestion, for examples "Rest".

**Expected behavior:** The word "Test" is replaced with "Rest".

**Actual behavior:** The word becomes "TestRest".

I confirmed with bisecting that the commit f897a4ab1a introduced the issue.

This change fixes the issue by disabling the layout hack while editing text.
Disabling the layout hack is limited to Firefox Mobile because

* I could not reproduce the bug in Chrome on Android
* This way, if it causes a new bug, only Firefox Mobile users are affected
2026-04-25 19:56:52 +03:00

95 lines
3.5 KiB
JavaScript

import { getParsedUA, isMobile } from './RossAscends-mods.js';
const isFirefox = () => /firefox/i.test(navigator.userAgent);
function sanitizeInlineQuotationOnCopy() {
// STRG+C, STRG+V on firefox leads to duplicate double quotes when inline quotation elements are copied.
// To work around this, take the selection and transform <q> to <span> before calling toString().
document.addEventListener('copy', function (event) {
if (document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement) {
return;
}
const selection = window.getSelection();
if (!selection.anchorNode?.parentElement.closest('.mes_text')) {
return;
}
const range = selection.getRangeAt(0).cloneContents();
const tempDOM = document.createDocumentFragment();
/**
* Process a node, transforming <q> elements to <span> elements and preserving children.
* @param {Node} node Input node
* @returns {Node} Processed node
*/
function processNode(node) {
if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === 'q') {
// Transform <q> to <span>, preserve children
const span = document.createElement('span');
[...node.childNodes].forEach(child => {
const processedChild = processNode(child);
span.appendChild(processedChild);
});
return span;
} else {
// Nested structures containing <q> elements are unlikely
return node.cloneNode(true);
}
}
[...range.childNodes].forEach(child => {
const processedChild = processNode(child);
tempDOM.appendChild(processedChild);
});
const newRange = document.createRange();
newRange.selectNodeContents(tempDOM);
event.preventDefault();
event.clipboardData.setData('text/plain', newRange.toString());
});
}
function addSafariPatch() {
const userAgent = getParsedUA();
console.debug('User Agent', userAgent);
const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isDesktopSafari = userAgent?.browser?.name === 'Safari' && userAgent?.platform?.type === 'desktop';
const isIOS = userAgent?.os?.name === 'iOS';
if (isIOS || isMobileSafari || isDesktopSafari) {
document.body.classList.add('safari');
}
}
function applyBrowserFixes() {
if (isFirefox()) {
sanitizeInlineQuotationOnCopy();
}
if (isMobile()) {
const fixFunkyPositioning = () => {
if (isFirefox()) {
const active = document.activeElement;
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
// The positioning hack below breaks GBoard candidate replacement
// in Firefox Mobile on Android.
return;
}
}
console.debug('[Mobile] Device viewport change detected.');
document.documentElement.style.position = 'fixed';
requestAnimationFrame(() => document.documentElement.style.position = '');
};
window.addEventListener('resize', fixFunkyPositioning);
window.addEventListener('orientationchange', fixFunkyPositioning);
}
addSafariPatch();
}
export { isFirefox, applyBrowserFixes };