Skip to content

Instantly share code, notes, and snippets.

@rbreaves
Last active November 9, 2025 05:25
Show Gist options
  • Select an option

  • Save rbreaves/ae07b384e3a2a16a0be6844eb644737f to your computer and use it in GitHub Desktop.

Select an option

Save rbreaves/ae07b384e3a2a16a0be6844eb644737f to your computer and use it in GitHub Desktop.
One-handed keyboard w/ touchpad for Chrome Remote Desktop on Mobile Devices
// use site to convert to bookmarklet for mobile devices
// https://infocatcher.github.io/Bookmarklets/scriptToBookmarklet.html
(() => {
// === LocalStorage helpers ===
const STORAGE_KEY = 'crd_layout_settings';
function loadSettings() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
} catch {
return {};
}
}
function saveSetting(key, value) {
const s = loadSettings();
s[key] = value;
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
}
const settings = loadSettings();
// ===== Locate CRD Elements (tolerant selectors) =====
const s = document.querySelector('.xHUvi.qr01Jb') ||
document.querySelector('.xHUvi.REbh3b.qr01Jb') ||
document.querySelector('.xHUvi.REbh3b.FKhztc.qr01Jb');
const sp = document.querySelector('section.MboOLb[aria-label="Displays"]');
const c = document.querySelector('canvas.sshIvb');
const vc = document.querySelector('div.VCqmx');
const crdSurface = document.querySelector('.SaCab');
const landscapeDiv = document.querySelector('.n9VB4d.LgVU8b'); // landscape keyboard container
if (!(sp && c && vc && crdSurface)) {
console.warn('⚠️ Missing required CRD elements.');
return;
}
// --- Ensure keyboard is expanded before proceeding ---
let k = document.querySelector('.V9Tle.JZG3Fc.hdWt0');
if (!k) {
const collapsed = document.querySelector('.V9Tle.JZG3Fc');
if (collapsed) {
console.log('🟡 Expanding collapsed keyboard…');
collapsed.classList.add('hdWt0');
const start = Date.now();
while (Date.now() - start < 500) {} // half-second delay for DOM update
}
}
k = document.querySelector('.V9Tle.JZG3Fc.hdWt0');
if (!k) {
console.error('❌ Could not find or expand keyboard — aborting.');
return;
}
const baseKeyboardHeight = k?.offsetHeight || 0;
console.log(`🎹 Keyboard height detected: ${baseKeyboardHeight}px`);
const originalParent = k?.parentElement || null;
const originalNextSibling = k?.nextSibling || null;
let ns = null;
const insertKeyboard = () => {
if (!k || ns) return;
ns = document.createElement('section');
ns.className = 'MboOLb injected-keyboard-section';
ns.setAttribute('role', 'region');
ns.setAttribute('aria-label', 'Keyboard');
ns.appendChild(k);
sp.insertAdjacentElement('afterend', ns);
Object.assign(k.style, {
width: '100%',
background: 'rgba(0,0,0,0.25)',
position: 'relative',
transform: 'none',
left: '0',
bottom: '0',
zIndex: 'auto'
});
console.log('✅ Keyboard relocated (sidebar).');
};
const removeKeyboard = () => {
if (!k) return;
if (originalParent && k.parentElement !== originalParent) {
if (originalNextSibling) originalParent.insertBefore(k, originalNextSibling);
else originalParent.appendChild(k);
console.log('↩️ Keyboard restored to original (main area).');
}
if (ns) { ns.remove(); ns = null; }
k.removeAttribute('style');
};
// ===== Touchpad Section (sidebar pad) =====
const padSection = document.createElement('section');
padSection.className = 'MboOLb injected-pad-section';
padSection.setAttribute('role', 'region');
padSection.setAttribute('aria-label', 'Touchpad Section');
const padContainer = document.createElement('div');
Object.assign(padContainer.style, {
height: '400px',
background: 'transparent',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: '12px'
});
const padLabel = document.createElement('div');
padLabel.textContent = 'Touchpad Overlay';
Object.assign(padLabel.style, {
color: '#fff',
fontSize: '16px',
marginBottom: '8px'
});
const touchpad = document.createElement('div');
Object.assign(touchpad.style, {
width: '90%',
maxWidth: '360px',
height: '240px',
background: 'rgba(255,255,255,0.08)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '8px',
touchAction: 'none',
position: 'relative',
overflow: 'hidden',
backdropFilter: 'blur(4px)',
transition: 'background 0.15s'
});
padContainer.append(padLabel, touchpad);
padSection.append(padContainer);
insertKeyboard();
const kbSection = document.querySelector('.injected-keyboard-section');
if (kbSection) kbSection.insertAdjacentElement('afterend', padSection);
else sp.insertAdjacentElement('afterend', padSection);
console.log('✅ Touchpad overlay inserted below keyboard (sidebar mode).');
// ===== Offset / Controls Section (below touchpad) =====
const o = document.createElement('section');
o.className = 'MboOLb injected-offset-section';
o.setAttribute('role', 'region');
o.setAttribute('aria-label', 'Sidebar Offset');
const w = document.createElement('div');
w.style.padding = '6px 8px';
const l = document.createElement('label');
l.textContent = 'Sidebar Offset (px):';
l.style.display = 'block';
l.style.marginBottom = '4px';
const i = document.createElement('input');
i.type = 'number';
i.placeholder = 'e.g. 100 or -100';
i.value = settings.sidebarOffset ?? '';
i.addEventListener('input', () => saveSetting('sidebarOffset', i.value));
Object.assign(i.style, { width: '100%', boxSizing: 'border-box', padding: '4px' });
w.append(l, i);
// Landscape offset
const l2 = document.createElement('label');
l2.textContent = 'Landscape Offset (px):';
Object.assign(l2.style, { display: 'block', marginTop: '8px', marginBottom: '4px' });
const i2 = document.createElement('input');
i2.type = 'number';
i2.placeholder = 'e.g. 50 or -50';
i2.value = settings.landscapeOffset ?? '';
i2.addEventListener('input', () => saveSetting('landscapeOffset', i2.value));
Object.assign(i2.style, { width: '100%', boxSizing: 'border-box', padding: '4px' });
const chkWrap = document.createElement('div');
chkWrap.style.marginTop = '6px';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.id = 'useRightMargin';
const chkLabel = document.createElement('label');
chkLabel.htmlFor = 'useRightMargin';
chkLabel.textContent = ' Use Right Margin';
chk.checked = settings.useRightMargin ? true : false;
chk.addEventListener('change', () => saveSetting('useRightMargin', chk.checked));
chkWrap.append(chk, chkLabel);
// Keyboard Height Override
const l3 = document.createElement('label');
l3.textContent = 'Landscape Keyboard Height (px):';
Object.assign(l3.style, { display: 'block', marginTop: '8px', marginBottom: '4px' });
const i3 = document.createElement('input');
i3.type = 'number';
// i3.value = baseKeyboardHeight;
i3.value = settings.keyboardHeight ?? baseKeyboardHeight;
i3.addEventListener('input', () => saveSetting('keyboardHeight', i3.value));
Object.assign(i3.style, { width: '100%', boxSizing: 'border-box', padding: '4px' });
const resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset Height';
Object.assign(resetBtn.style, {
marginTop: '6px',
padding: '4px 8px',
background: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
cursor: 'pointer'
});
resetBtn.addEventListener('click', () => {
if (!k) return;
k.style.height = '';
if (landscapeDiv) {
landscapeDiv.style.removeProperty('--keyboard-height');
landscapeDiv.style.height = '';
}
const currentHeight = k.offsetHeight || baseKeyboardHeight;
i3.value = currentHeight;
const inlinePad = document.querySelector('.touchpad-inline');
if (inlinePad) inlinePad.style.height = `${currentHeight}px`;
console.log(`↩️ Keyboard height reset to default (${currentHeight}px).`);
updateDockLayout();
});
// Placement selector
const layoutLabel = document.createElement('label');
layoutLabel.textContent = 'Touchpad & Keyboard Placement:';
Object.assign(layoutLabel.style, { display: 'block', marginTop: '10px', marginBottom: '4px' });
const layoutSelect = document.createElement('select');
Object.assign(layoutSelect.style, {
width: '100%', boxSizing: 'border-box', padding: '4px', marginBottom: '6px'
});
layoutSelect.innerHTML = `
<option value="sidebar" selected>Sidebar (default)</option>
<option value="land-left">Left Landscape (keyboard left, touchpad right)</option>
<option value="land-right">Right Landscape (keyboard right, touchpad left)</option>
<option value="land-kb-only">Landscape Keyboard Only (no touchpad)</option>
`;
const layoutNote = document.createElement('div');
layoutNote.textContent = '(Landscape options require the landscape keyboard to be visible)';
Object.assign(layoutNote.style, {
fontSize: '12px',
color: '#aaa',
marginTop: '-2px',
marginBottom: '6px'
});
if (!landscapeDiv) {
Array.from(layoutSelect.options).forEach(opt => {
if (opt.value.startsWith('land-')) opt.disabled = true;
});
layoutNote.style.color = '#f77';
}
// Restore saved layout value (or default to 'sidebar')
layoutSelect.value = settings.layoutMode ? settings.layoutMode : 'sidebar';
// Save to localStorage when changed
layoutSelect.addEventListener('change', () => {
saveSetting('layoutMode', layoutSelect.value);
});
// Landscape touchpad width
const l4 = document.createElement('label');
l4.textContent = 'Landscape Touchpad Width (px):';
Object.assign(l4.style, { display: 'block', marginTop: '6px', marginBottom: '4px' });
const i4 = document.createElement('input');
i4.type = 'number';
// i4.value = 320;
i4.value = settings.touchpadWidth ?? 320;
i4.addEventListener('input', () => saveSetting('touchpadWidth', i4.value));
Object.assign(i4.style, { width: '100%', boxSizing: 'border-box', padding: '4px' });
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear Saved Settings';
Object.assign(clearBtn.style, {
marginTop: '6px',
padding: '4px 8px',
background: '#552222',
color: '#fff',
border: '1px solid #844',
borderRadius: '4px',
cursor: 'pointer'
});
clearBtn.addEventListener('click', () => {
localStorage.removeItem('crd_layout_settings');
console.log('🧹 LocalStorage cleared for crd_layout_settings');
alert('Saved layout settings cleared. Reload or re-run the script to reset.');
});
w.append(l, i, l2, i2, chkWrap, l3, i3, resetBtn, l4, i4, layoutLabel, layoutSelect, layoutNote, clearBtn);
o.append(w);
padSection.insertAdjacentElement('afterend', o);
// ===== Dock Layout Handler =====
let dockPad = null;
const GAP = 12;
const ensureDockPad = () => {
if (dockPad && document.body.contains(dockPad)) return dockPad;
dockPad = touchpad.cloneNode(true);
dockPad.classList.add('touchpad-inline');
Object.assign(dockPad.style, {
position: 'absolute',
top: '0',
bottom: '0',
width: `${Math.max(120, parseInt(i4.value || '320', 10))}px`,
margin: '0',
maxWidth: 'none',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '8px',
background: 'rgba(255,255,255,0.08)'
});
['pointerdown','pointermove','pointerup','touchstart','touchmove','touchend'].forEach(evt => {
dockPad.addEventListener(evt, e => {
const clone = new e.constructor(evt, e);
crdSurface.dispatchEvent(clone);
e.preventDefault();
}, { passive: false });
});
return dockPad;
};
const removeDockPad = () => {
if (dockPad && dockPad.parentElement) dockPad.remove();
dockPad = null;
};
function updateDockLayout() {
const mode = layoutSelect.value;
if (!landscapeDiv || mode === 'sidebar') {
padSection.style.display = '';
touchpad.style.height = '240px';
removeDockPad();
insertKeyboard();
k.style.width = '100%';
k.style.height = '';
landscapeDiv.style.marginLeft = '';
landscapeDiv.style.marginRight = '';
landscapeDiv.style.width = '';
console.log('↩️ Sidebar layout restored.');
return;
}
// Landscape Keyboard Only mode
if (mode === 'land-kb-only') {
// padSection.style.display = 'none';
landscapeDiv.style.marginLeft = '';
landscapeDiv.style.marginRight = '';
removeDockPad();
removeKeyboard();
console.log('⌨️ Landscape keyboard only mode enabled.');
return;
}
// Landscape with touchpad modes
padSection.style.display = 'none';
touchpad.style.height = '100%';
removeKeyboard();
const container = landscapeDiv.parentElement;
if (!container) return;
if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
const dp = ensureDockPad();
if (!dp.parentElement) container.appendChild(dp);
const padW = Math.max(120, parseInt(i4.value || '320', 10));
dp.style.width = `${padW}px`;
landscapeDiv.style.marginLeft = '';
landscapeDiv.style.marginRight = '';
if (mode === 'land-right') {
dp.style.left = '0'; dp.style.right = '';
landscapeDiv.style.marginLeft = `${padW + GAP}px`;
} else if (mode === 'land-left') {
dp.style.right = '0'; dp.style.left = '';
landscapeDiv.style.marginRight = `${padW + GAP}px`;
}
landscapeDiv.style.width = '';
}
// ===== Show docked touchpad again when keyboard icon is clicked =====
const showKeyboardBtn = document.querySelector('div[role="button"][aria-label="Show remote keyboard"]');
if (showKeyboardBtn) {
showKeyboardBtn.addEventListener('click', () => {
setTimeout(() => {
const dock = document.querySelector('.touchpad-inline');
if (!dock) return;
dock.style.display = '';
console.log('⌨️ Keyboard reopened — touchpad shown.');
}, 300); // small delay to let CRD render keyboard
});
}
// ===== Auto-hide docked touchpad when "Compose text locally" is toggled =====
const textInputBtn = document.querySelector('div[role="button"][data-key="TextInput"]');
if (textInputBtn) {
textInputBtn.addEventListener('click', () => {
// Wait a tiny bit for CRD to resize/minify keyboard
setTimeout(() => {
const dock = document.querySelector('.touchpad-inline');
if (!dock) return;
// Determine if keyboard is minimized by checking height or visibility
const kbHeight = landscapeDiv ? landscapeDiv.offsetHeight : 0;
if (kbHeight < 100) {
dock.style.display = 'none';
console.log('📉 Keyboard minimized — touchpad hidden.');
} else {
dock.style.display = '';
console.log('📈 Keyboard expanded — touchpad restored.');
}
}, 200);
});
}
// ===== Apply Keyboard Height (syncs landscape + inline pad) =====
const applyKeyboardHeight = () => {
const val = parseInt(i3.value, 10);
if (!Number.isNaN(val) && val > 0) {
if (k) k.style.height = `${val}px`;
if (landscapeDiv) {
landscapeDiv.style.setProperty('--keyboard-height', `${val}px`);
landscapeDiv.style.height = `${val}px`;
}
const inlinePad = document.querySelector('.touchpad-inline');
if (inlinePad) inlinePad.style.height = `${val}px`;
console.log(`🎛️ Keyboard height overridden: ${val}px`);
} else {
if (k) k.style.height = '';
if (landscapeDiv) {
landscapeDiv.style.removeProperty('--keyboard-height');
landscapeDiv.style.height = '';
}
const inlinePad = document.querySelector('.touchpad-inline');
if (inlinePad) inlinePad.style.height = '';
}
updateDockLayout();
};
i3.addEventListener('input', applyKeyboardHeight);
layoutSelect.addEventListener('change', updateDockLayout);
i4.addEventListener('input', updateDockLayout);
// === Apply saved values on load ===
if (settings.keyboardHeight && settings.layoutMode=='land-kb-only') {
i3.value = settings.keyboardHeight;
applyKeyboardHeight();
}
if (settings.layoutMode) layoutSelect.value = settings.layoutMode;
if (settings.touchpadWidth) i4.value = settings.touchpadWidth;
if (settings.useRightMargin) chk.checked = true;
if (settings.sidebarOffset) i.value = settings.sidebarOffset;
if (settings.landscapeOffset) i2.value = settings.landscapeOffset;
updateDockLayout();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment