Editor keyboard: docked at the bottom, following the writing cursor #79
@ -211,7 +211,8 @@ import { brand } from '../lib/brand';
|
|||||||
kb.innerHTML = `<div>${L[0].map((c) => key(c)).join('')}</div><div>${L[1].map((c) => key(c)).join('')}</div>` +
|
kb.innerHTML = `<div>${L[0].map((c) => key(c)).join('')}</div><div>${L[1].map((c) => key(c)).join('')}</div>` +
|
||||||
`<div><button type="button" data-act="shift" class="fn${shift ? ' on' : ''}" aria-label="شفٹ">⇧</button>${L[2].map((c) => key(c)).join('')}` +
|
`<div><button type="button" data-act="shift" class="fn${shift ? ' on' : ''}" aria-label="شفٹ">⇧</button>${L[2].map((c) => key(c)).join('')}` +
|
||||||
'<button type="button" data-act="bs" class="fn" aria-label="مٹائیں">⌫</button></div>' +
|
'<button type="button" data-act="bs" class="fn" aria-label="مٹائیں">⌫</button></div>' +
|
||||||
`<div>${key('،')}<button type="button" data-ch=" " class="wide">فاصلہ</button>${key('۔')}${key('؟')}</div>`;
|
`<div>${key('،')}<button type="button" data-ch=" " class="wide">فاصلہ</button>${key('۔')}${key('؟')}` +
|
||||||
|
'<button type="button" data-act="close" class="fn" aria-label="کی بورڈ بند کریں" title="بند کریں">✕</button></div>';
|
||||||
};
|
};
|
||||||
render();
|
render();
|
||||||
// the Urdu keyboard and Roman typing work on the search box and on any input marked data-urdu
|
// 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) => {
|
kb.onclick = (ev) => {
|
||||||
const b = ev.target.closest('button'); if (!b) return;
|
const b = ev.target.closest('button'); if (!b) return;
|
||||||
if (b.dataset.act === 'shift') { shift = !shift; render(); return; }
|
if (b.dataset.act === 'shift') { shift = !shift; render(); return; }
|
||||||
|
if (b.dataset.act === 'close') { closeKb(); return; }
|
||||||
if (b.dataset.act === 'bs') {
|
if (b.dataset.act === 'bs') {
|
||||||
live();
|
live();
|
||||||
if (target.isContentEditable) { target.focus(); document.execCommand('delete'); return; }
|
if (target.isContentEditable) { target.focus(); document.execCommand('delete'); return; }
|
||||||
@ -244,7 +246,41 @@ import { brand } from '../lib/brand';
|
|||||||
};
|
};
|
||||||
kbBtn.onclick = () => {
|
kbBtn.onclick = () => {
|
||||||
kb.hidden = !kb.hidden; kbBtn.setAttribute('aria-pressed', String(!kb.hidden)); target.focus();
|
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(); };
|
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
|
// other inputs can open the same keyboard / Roman typing with their own buttons
|
||||||
document.addEventListener('click', (e) => {
|
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);
|
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
|
// 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');
|
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.dataset.urduKb ? kbBtn : romBtn).click();
|
||||||
b.setAttribute('aria-pressed', (b.dataset.urduKb ? kbBtn : romBtn).getAttribute('aria-pressed'));
|
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
|
document.addEventListener('close', (ev) => { // a dialog closing takes nothing of the header's with it
|
||||||
if (!ev.target.contains?.(kb)) return;
|
if (ev.target.contains?.(kb)) closeKb();
|
||||||
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'));
|
|
||||||
}, true);
|
}, true);
|
||||||
document.addEventListener('keydown', (ev) => {
|
document.addEventListener('keydown', (ev) => {
|
||||||
if (!ev.target.matches?.('[data-urdu]')) return;
|
if (!ev.target.matches?.('[data-urdu]')) return;
|
||||||
|
|||||||
@ -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 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 .wide { min-width: 12em; }
|
||||||
.kb .fn { min-width: 3.4em; background: var(--surface-sunken); }
|
.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 */
|
/* 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;
|
.dict { position: fixed; top: 0; left: 0; bottom: 0; width: min(340px, 92vw); z-index: 20; overflow-y: auto; padding: 14px 18px 24px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user