';
};
render();
// the Urdu keyboard and Roman typing work on the search box and on any input marked data-urdu
@@ -232,6 +233,7 @@ import { brand } from '../lib/brand';
kb.onclick = (ev) => {
const b = ev.target.closest('button'); if (!b) return;
if (b.dataset.act === 'shift') { shift = !shift; render(); return; }
+ if (b.dataset.act === 'close') { closeKb(); return; }
if (b.dataset.act === 'bs') {
live();
if (target.isContentEditable) { target.focus(); document.execCommand('delete'); return; }
@@ -244,7 +246,41 @@ import { brand } from '../lib/brand';
};
kbBtn.onclick = () => {
kb.hidden = !kb.hidden; kbBtn.setAttribute('aria-pressed', String(!kb.hidden)); target.focus();
+ if (kb.hidden) home(); else place();
};
+ // back to its place in the header (not floating)
+ const home = () => { kb.classList.remove('kb-float'); document.body.style.paddingBottom = ''; if (kb.parentElement !== kbHome[0]) kbHome[0].insertBefore(kb, kbHome[1]); };
+ const closeKb = () => {
+ kb.hidden = true; kbBtn.setAttribute('aria-pressed', 'false'); home();
+ document.querySelectorAll('[data-urdu-kb][aria-pressed=true]').forEach((x) => x.setAttribute('aria-pressed', 'false'));
+ };
+ // For the content editor the keyboard is docked at the bottom of the window (like a phone's) and the page follows
+ // the writing cursor: the line being written is kept in view just above the keyboard, never under it. The page
+ // gets room at the bottom while it is open, so the last lines can come up above it.
+ const caret = () => {
+ const sel = getSelection();
+ if (sel?.rangeCount && target.contains(sel.anchorNode)) {
+ const r = sel.getRangeAt(0), rect = r.getClientRects()[0] ?? r.getBoundingClientRect();
+ if (rect && (rect.width || rect.height)) return rect;
+ const el = sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement;
+ if (el) return el.getBoundingClientRect(); // an empty line has no caret box of its own
+ }
+ return null;
+ };
+ let placing = 0;
+ function place() {
+ if (kb.hidden || !kb.classList.contains('kb-float')) return;
+ cancelAnimationFrame(placing);
+ placing = requestAnimationFrame(() => {
+ document.body.style.paddingBottom = `${kb.offsetHeight + 24}px`;
+ const c = caret(); if (!c) return;
+ const room = kb.getBoundingClientRect().top - 16, top = 90; // keep the line between the header area and the keyboard
+ if (c.bottom > room) scrollBy({ top: c.bottom - room + 40, behavior: 'smooth' });
+ else if (c.top < top) scrollBy({ top: c.top - top - 40, behavior: 'smooth' });
+ });
+ }
+ document.addEventListener('selectionchange', place);
+ addEventListener('resize', place);
romBtn.onclick = () => { romBtn.setAttribute('aria-pressed', String(romBtn.getAttribute('aria-pressed') !== 'true')); target.focus(); };
// other inputs can open the same keyboard / Roman typing with their own buttons
document.addEventListener('click', (e) => {
@@ -252,14 +288,14 @@ import { brand } from '../lib/brand';
target = document.getElementById(b.dataset.urduKb ?? b.dataset.urduRom)?.querySelector('[contenteditable]') ?? document.getElementById(b.dataset.urduKb ?? b.dataset.urduRom);
// in an open dialog (the note box) the header can't be reached: the keyboard moves under the box, and back when it closes
const box = b.dataset.urduKb && target.closest('dialog');
- if (box) target.closest('.in-box-field')?.after(kb); else if (b.dataset.urduKb) kbHome[0].insertBefore(kb, kbHome[1]);
+ if (box) { kb.classList.remove('kb-float'); target.closest('.in-box-field')?.after(kb); }
+ else if (b.dataset.urduKb && target.isContentEditable) { kb.classList.add('kb-float'); document.body.append(kb); } // the editor: floating
+ else if (b.dataset.urduKb) home();
(b.dataset.urduKb ? kbBtn : romBtn).click();
b.setAttribute('aria-pressed', (b.dataset.urduKb ? kbBtn : romBtn).getAttribute('aria-pressed'));
});
document.addEventListener('close', (ev) => { // a dialog closing takes nothing of the header's with it
- if (!ev.target.contains?.(kb)) return;
- kb.hidden = true; kbBtn.setAttribute('aria-pressed', 'false'); kbHome[0].insertBefore(kb, kbHome[1]);
- ev.target.querySelectorAll('[data-urdu-kb]').forEach((x) => x.setAttribute('aria-pressed', 'false'));
+ if (ev.target.contains?.(kb)) closeKb();
}, true);
document.addEventListener('keydown', (ev) => {
if (!ev.target.matches?.('[data-urdu]')) return;
diff --git a/web/src/styles/global.css b/web/src/styles/global.css
index 35e7e6ba..d0f01a6a 100644
--- a/web/src/styles/global.css
+++ b/web/src/styles/global.css
@@ -249,6 +249,10 @@ h1 + .muted { text-align: center; margin-top: 0; }
.kb button { font-size: var(--fs-lg); min-width: 2.4em; padding: 2px 6px; background: var(--surface-raised); border-radius: var(--r-sm); }
.kb .wide { min-width: 12em; }
.kb .fn { min-width: 3.4em; background: var(--surface-sunken); }
+/* the content editor's keyboard: docked at the bottom of the window; the page keeps the writing line above it */
+.kb.kb-float { position: fixed; z-index: 40; inset-inline: 0; bottom: 0; margin: 0 auto; max-width: min(720px, 100vw); padding: 6px 10px 12px;
+ background: var(--surface-raised); border: 1.5px solid var(--border); border-bottom: 0; border-radius: var(--r-lg) var(--r-lg) 0 0;
+ box-shadow: 0 -8px 28px rgb(0 0 0 / .18); }
/* word sidebar (Wiktionary): left side on wide screens, bottom sheet on phones */
.dict { position: fixed; top: 0; left: 0; bottom: 0; width: min(340px, 92vw); z-index: 20; overflow-y: auto; padding: 14px 18px 24px;