stackchain-dashboard/frontend/mobile-composer-viewport.js
timmy 21e0a49d3c
All checks were successful
CI / lint (pull_request) Successful in 31s
CI / build-frontend (pull_request) Successful in 4s
feat: keep mobile conversation replies keyboard-safe (#323)
2026-08-08 17:34:11 +00:00

93 lines
3.1 KiB
JavaScript

(function (root, factory) {
const exported = factory();
if (typeof module === 'object' && module.exports) module.exports = exported;
else root.createMobileComposerViewport = exported;
})(typeof self !== 'undefined' ? self : this, function () {
return function createMobileComposerViewport(options) {
const entries = options.entries || [];
const viewport = options.viewport;
const mediaQuery = options.mediaQuery;
const documentRef = options.document || document;
const schedule = options.schedule || (callback => requestAnimationFrame(callback));
let active = null;
let started = false;
function reveal() {
if (!active) return;
active.workspace.scrollIntoView({ block:'end', inline:'nearest' });
}
function applyGeometry() {
if (!active || !viewport || !mediaQuery.matches) return;
active.panel.style.setProperty('--composer-viewport-top', `${viewport.offsetTop || 0}px`);
active.panel.style.setProperty('--composer-viewport-height', `${viewport.height}px`);
reveal();
}
function onViewportChange() {
schedule(applyGeometry);
}
function activate(entry) {
if (!viewport || !mediaQuery.matches) return;
if (active === entry) {
schedule(applyGeometry);
return;
}
if (active) deactivate(active, false);
active = entry;
entry.scrollTop = entry.panel.scrollTop;
entry.panel.classList?.add('composer-keyboard-active');
viewport.addEventListener('resize', onViewportChange);
viewport.addEventListener('scroll', onViewportChange);
applyGeometry();
}
function deactivate(entry, restore = true) {
if (active !== entry) return;
viewport?.removeEventListener('resize', onViewportChange);
viewport?.removeEventListener('scroll', onViewportChange);
entry.panel.classList?.remove('composer-keyboard-active');
entry.panel.style.removeProperty('--composer-viewport-top');
entry.panel.style.removeProperty('--composer-viewport-height');
active = null;
if (restore) schedule(() => { entry.panel.scrollTop = entry.scrollTop; });
}
function bind(entry) {
entry.onFocus = () => activate(entry);
entry.onFocusOut = () => schedule(() => {
if (!entry.workspace.contains(documentRef.activeElement)) deactivate(entry);
});
entry.composer.addEventListener('focus', entry.onFocus);
entry.workspace.addEventListener('focusout', entry.onFocusOut);
}
function unbind(entry) {
entry.composer.removeEventListener('focus', entry.onFocus);
entry.workspace.removeEventListener('focusout', entry.onFocusOut);
}
return {
start() {
if (started) return;
started = true;
entries.forEach(bind);
},
close(panel) {
const entry = entries.find(candidate => candidate.panel === panel);
if (entry) deactivate(entry, false);
},
stop() {
if (!started) return;
if (active) deactivate(active, false);
entries.forEach(unbind);
started = false;
},
revealActive() {
schedule(applyGeometry);
},
};
};
});