mirror of
https://github.com/abort-retry-ignore/joplock.git
synced 2026-10-19 10:13:51 +00:00
fix(mobile): keep the markdown toolbar visible while editing
Tapping a line in markdown mode hid the B/I/U/S toolbar for good. The viewport handler concluded the keyboard had gone away when innerHeight - visualViewport.height < 80 and asked to hide it, but iOS home-screen apps shrink innerHeight together with the visual viewport, so an open keyboard looked closed (seen in /diag: 483 vs 409, 409 vs 409). The rich editor's blur handler could do the same 200ms after a switch to markdown, and nothing ever re-showed the toolbar. setMobileToolbarVisible now ignores hide requests in markdown mode; only a locked vault hides it. Unit tests for the function, and a mobile Playwright test that shrinks the window as the keyboard does (it fails on the previous build). Bumps ASSET_VERSION and the service worker cache.
This commit is contained in:
parent
b511f7b685
commit
54c4c46ad3
6 changed files with 113 additions and 3 deletions
|
|
@ -659,6 +659,7 @@ These screens are shown/hidden by inline JS in `layoutPage()` using class change
|
|||
- Mobile note creation uses dedicated fragment endpoints and server headers to drive the next UI step
|
||||
- The floating action button is only a mobile affordance; desktop should stay unaffected
|
||||
- FAB visibility should follow screen state directly (`folders` / `notes` visible, `editor` hidden), not only htmx swap side effects
|
||||
- In markdown mode the mobile formatting toolbar (`#editor-toolbar`) is permanent: `setMobileToolbarVisible(false)` is ignored there (only a locked vault hides it). Callers ask to hide it when the viewport handler thinks the keyboard went away, which is wrong on iOS home-screen apps (`innerHeight` shrinks with the visual viewport, so an open keyboard looks closed), and from the rich editor's blur handler after a switch to markdown. Do not reintroduce `innerHeight - visualViewport.height` keyboard guesses; see `/diag` for real-device numbers. Covered by `tests/mobileToolbarVisibility.test.js` and `playwright-tests/mobile-toolbar-keyboard.spec.js`.
|
||||
- The markdown editor's word-count / heading-outline bar (`.cm-jl-status`, `mdStatusBar` setting) is desktop-only: `_cmExtraExtensions` passes `statusBar: _mdStatusBar && !isMobileShellMode()`, so it is never created in the mobile shell (it costs a row under the editor and recounts words on every keystroke). The Outline jump-to-heading popup lives in that bar, so it is not available on mobile either. Covered by `tests/mobileStatusBar.test.js` and `playwright-tests/md-extras.spec.js`.
|
||||
- `#mobile-folders-body` / `#mobile-notes-body` carry `padding-bottom: calc(96px + env(safe-area-inset-bottom))` so the last row (or the notes list's "Load more" button) scrolls clear of the FAB (56px + 20px offset) and the home indicator. Scope it to those two ids: `#mobile-editor-body` shares `.mobile-screen-body` and must not get it. Covered by `playwright-tests/mobile-list-clearance.spec.js`.
|
||||
- Mobile folder rows can include a vault lock button and it must stay inline with the row actions
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const {
|
|||
} = require('./shared');
|
||||
const { noteMetaFragment } = require('./fragments');
|
||||
|
||||
const ASSET_VERSION = '20261012nosb';
|
||||
const ASSET_VERSION = '20261013tbkeep';
|
||||
|
||||
const noteFontFamilyCSS = (settings) => {
|
||||
const f = settings.noteFontFamily || 'sans';
|
||||
|
|
|
|||
41
playwright-tests/mobile-toolbar-keyboard.spec.js
Normal file
41
playwright-tests/mobile-toolbar-keyboard.spec.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { ensureMobileFoldersScreen, login, openMobileFolder } = require('./helpers');
|
||||
|
||||
// In markdown mode the mobile formatting toolbar (B I U S H3 ...) is shown when a note opens and
|
||||
// must stay while the soft keyboard is up. It used to disappear the moment a line was tapped:
|
||||
// the viewport handler decided "keyboard gone" when innerHeight - visualViewport.height < 80,
|
||||
// and iOS home-screen apps shrink innerHeight together with the visual viewport, so an open
|
||||
// keyboard looked closed. Chromium reproduces that by shrinking the window, which resizes both.
|
||||
// Read-only: opens a note and focuses the editor, types nothing.
|
||||
|
||||
test.describe('Mobile markdown toolbar and the keyboard', () => {
|
||||
test('the toolbar stays visible when the keyboard opens and closes', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'mobile');
|
||||
await login(page);
|
||||
await ensureMobileFoldersScreen(page);
|
||||
await openMobileFolder(page, 'All Notes');
|
||||
await page.locator('#mobile-notes-body .mobile-note-row').first().click();
|
||||
await expect(page.locator('#mobile-editor-screen.mobile-screen-active')).toBeVisible();
|
||||
|
||||
const content = page.locator('#mobile-editor-body #cm-host .cm-content');
|
||||
const toolbar = page.locator('#mobile-editor-body #editor-toolbar');
|
||||
// the editor mounts a moment after the screen switches; a note that opens rendered has no #cm-host
|
||||
const markdown = await content.waitFor({ state: 'visible', timeout: 10000 }).then(() => true, () => false);
|
||||
test.skip(!markdown, 'note opened in rendered mode, not markdown');
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
const size = page.viewportSize();
|
||||
await content.click();
|
||||
// the keyboard: both innerHeight and the visual viewport shrink, as in an iOS home-screen app
|
||||
await page.setViewportSize({ width: size.width, height: Math.round(size.height * 0.55) });
|
||||
await page.waitForTimeout(700);
|
||||
await expect(toolbar, 'toolbar while the keyboard is up').toBeVisible();
|
||||
expect(await toolbar.evaluate(el => getComputedStyle(el).display)).not.toBe('none');
|
||||
|
||||
await page.setViewportSize(size);
|
||||
await page.waitForTimeout(700);
|
||||
await expect(toolbar, 'toolbar after the keyboard closes').toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -5950,8 +5950,14 @@ function confirmLogout(event){
|
|||
positionTinyMCEHost();
|
||||
return;
|
||||
}
|
||||
// Markdown mode: this is the formatting bar (B I U S H3 ...). It is up when a note opens and
|
||||
// stays up while editing, so "hide" requests are ignored: the viewport handler asks for one
|
||||
// when it thinks the keyboard went away (wrong on iOS home-screen apps, where innerHeight
|
||||
// shrinks together with the visual viewport, so an OPEN keyboard looks closed), and the rich
|
||||
// editor's blur handler asks for one 200ms after a switch to markdown. Only a locked vault hides it.
|
||||
var form=activeEditorForm();
|
||||
if(form&&form.dataset.encrypted==='1'&&form.dataset.vaultUnlocked!=='1')show=false;
|
||||
var locked=!!(form&&form.dataset.encrypted==='1'&&form.dataset.vaultUnlocked!=='1');
|
||||
show=!locked;
|
||||
tb.style.display=show?'flex':'none';
|
||||
// Reposition immediately, then again after a tick so the toolbar has a
|
||||
// rendered height before positionTinyMCEHost measures the anchor bottom.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const CACHE_NAME = 'joplock-shell-v88-20261012nosb';
|
||||
const CACHE_NAME = 'joplock-shell-v89-20261013tbkeep';
|
||||
const STATIC_ASSETS = ['/styles.css', '/htmx.min.js', '/turndown.min.js', '/codemirror.min.js', '/cm-extras.js', '/prism.min.js', '/hljs.min.js', '/manifest.webmanifest', '/icon.svg', '/icon-192.png', '/icon-512.png', '/maskable-icon-192.png', '/maskable-icon-512.png', '/apple-touch-icon.png', '/fonts/CascadiaMono-Regular.woff2', '/fonts/CascadiaMono-Bold.woff2'];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
|
|
|
|||
62
tests/mobileToolbarVisibility.test.js
Normal file
62
tests/mobileToolbarVisibility.test.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
// setMobileToolbarVisible(false) is called when the viewport handler thinks the keyboard went
|
||||
// away and 200ms after the rich editor blurs. In markdown mode that used to hide the formatting
|
||||
// toolbar for good (nothing re-showed it), so it vanished the moment a line was tapped. It must
|
||||
// ignore "hide" in markdown mode; only a locked vault hides it.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const vm = require('node:vm');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const appSrc = fs.readFileSync(path.join(__dirname, '../public/app.js'), 'utf8');
|
||||
|
||||
function extractFn(name) {
|
||||
const start = appSrc.indexOf(`function ${name}(`);
|
||||
assert.ok(start !== -1, `function ${name} not found in app.js`);
|
||||
let depth = 0;
|
||||
for (let i = start; i < appSrc.length; i++) {
|
||||
if (appSrc[i] === '{') depth++;
|
||||
else if (appSrc[i] === '}') { depth--; if (depth === 0) return appSrc.slice(start, i + 1); }
|
||||
}
|
||||
throw new Error(`no closing brace for ${name}`);
|
||||
}
|
||||
|
||||
function run({ mode, show, form }) {
|
||||
const tb = { style: { display: '' } };
|
||||
let repositioned = 0;
|
||||
const ctx = vm.createContext({
|
||||
_editorMode: mode,
|
||||
mobileEditorToolbar: () => tb,
|
||||
activeEditorForm: () => form || null,
|
||||
positionTinyMCEHost: () => { repositioned++; },
|
||||
setTimeout: fn => fn(),
|
||||
});
|
||||
vm.runInContext(`${extractFn('setMobileToolbarVisible')}; setMobileToolbarVisible(${show});`, ctx);
|
||||
return { display: tb.style.display, repositioned };
|
||||
}
|
||||
|
||||
test('markdown mode keeps the toolbar when asked to hide it', () => {
|
||||
for (const mode of ['markdown', 'md']) {
|
||||
assert.equal(run({ mode, show: false }).display, 'flex');
|
||||
assert.equal(run({ mode, show: true }).display, 'flex');
|
||||
}
|
||||
});
|
||||
|
||||
test('a locked vault hides the markdown toolbar whatever is asked', () => {
|
||||
const locked = { dataset: { encrypted: '1', vaultUnlocked: '0' } };
|
||||
assert.equal(run({ mode: 'markdown', show: true, form: locked }).display, 'none');
|
||||
assert.equal(run({ mode: 'markdown', show: false, form: locked }).display, 'none');
|
||||
const unlocked = { dataset: { encrypted: '1', vaultUnlocked: '1' } };
|
||||
assert.equal(run({ mode: 'markdown', show: false, form: unlocked }).display, 'flex');
|
||||
});
|
||||
|
||||
test('rich mode leaves the toolbar to CSS (TinyMCE has its own) and just repositions the host', () => {
|
||||
for (const show of [true, false]) {
|
||||
const r = run({ mode: 'rich', show });
|
||||
assert.equal(r.display, '');
|
||||
assert.equal(r.repositioned, 1);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue