- On mobile, notes always open in rendered read-only view even when the
saved note-open preference is markdown; the raw markdown view is only
shown after entering edit mode.
- The edit (pencil) toggle honors the saved preference: entering edit
switches to the markdown editor if that's the preference, and leaving
edit returns to rendered read-only.
- View/mode switches (pencil + header MD/eye buttons) are now transient
and no longer persist noteOpenMode; the preference is changed only in
Settings.
Change the default newlineBehavior from 'block' (paragraphs) to
'linebreak' for both mobile and desktop; still user-changeable in
Settings. Rendered ('preview') note-open mode remains the default.
Update the Settings dropdown default label and the hardcoded fallbacks
accordingly.
- jop_edit button now uses context:'any' so it stays enabled in
read-only mode (previously disabled by default, so tapping did
nothing on mobile where notes open read-only).
- Block edits while read-only: keydown, onEdit save, code-block modal
(click/touch long-press), and checkbox toggling all no-op.
Mobile notes now open the rich editor in read-only mode so tapping
scrolls/reads without accidental edits. A new jop_edit (pencil) toggle
button on the toolbar switches to edit mode; desktop opens editable as
before. Read-only is applied via TinyMCE mode.set and reset per note.
Tapping/scrolling a code block on mobile no longer opens the code
editor mid-scroll. Touch now requires a 500ms hold (movement <10px) to
open; desktop click behavior is unchanged. Synthetic touch-click is
suppressed via _lpFired and sourceCapabilities.firesTouchEvents.
The rendered-mode click handler logged 'tinymce click: pre found? ...'
on every click in the note body (leftover debug output). Remove the
unconditional console.log calls; behavior is unchanged.
Squash-merge of the `tinymce` branch: replaces the previous editor with a
dual-mode setup backed by the hidden #note-body textarea.
- Rendered mode = TinyMCE 8 (persistent singleton over #tinymce-slot),
markdown round-trip via Turndown (tinymceToMarkdown).
- Markdown mode = CodeMirror 6 mounted in #cm-host (initCM/getCM/
cmSyncToTA/cmSetVal/mountMarkdownEditor); codemirror.min.js loaded
before app.js. Full-screen CM6 code modal + language picker.
- Code highlighting: hljs in preview/markdown, Prism (codesample) in
rendered mode.
Text expander + AI prose completion wired for both modes:
- Text triggers expand in CM6 and in TinyMCE.
- AI-action triggers and Ctrl/Cmd-Space run prose completion; in rendered
mode the completion is offered in the same accept/dismiss popup as
markdown mode (Enter/Tab insert, Esc discard), positioned at the iframe
caret, inserted as DOM text nodes and synced to #note-body.
Uploads: upload modal + direct drag/paste into TinyMCE and CM6, admin
maxUploadMb limit with fast 413 pre-check, dropped images get a trailing
blank line, upload-reload sync.
Also included: per-user spellcheck toggle, runtime-toggleable debug
logging, settings-page Esc dismiss, separate render/markdown font sizes,
persisted editor mode, blank-line/image-spacing fixes across mode
switches.
Tests: cm6MarkdownMode, expanderRuntime, appRuntime, markdownToolbarClicks
unit suites (558 total, all passing) + playwright specs (desktop,
ai-rendered live provider E2E, upload-reload, resource-lifecycle).
- Replace fixed height:1em on .md-blank-line with margin:0.75em to match
paragraph margins; neutralize load-bearing <br> inside blank-line divs
via CSS so spacing is purely margin-driven
- Unify browser-inserted blank paragraph rules (p:empty, div:not([class]))
to use margin:0.75em instead of height:1em
- Add text-decoration:underline to tags.url highlight style and .cm-link/.cm-url
CSS rule so hyperlinks are always underlined in markdown mode
- Add highlightActiveLine bool setting (default true) wired through
settingsService → api allowedKeys → settings UI → _joplockConfig → app.js
- Fix CM theme fontSize to use var(--font-size-note) instead of hardcoded 14px
- Fix CM lineHeight to 1.7 to match .editor-preview
- Fix mobile CSS: remove hardcoded font-size/line-height from .cm-host .cm-content
- Fix Turndown br option: '' instead of '\n' to prevent double-spacing on
render-mode edits (Turndown appends its own \n, so br:'\n' produced \n\n)
- Guard flushSave() syncPV() call behind _previewDirty to prevent Turndown
corruption during autosave when user hasn't edited in preview mode
- Mark ensureEditableAfterPre injected nodes with data-pv-trail and strip them
in htmlToMarkdown clone to prevent extra blank lines after trailing code blocks
- Fix setEditorMode('markdown') to use content comparison for cmSetVal decision
- Increase .editor-preview p margin to 0.75em for better paragraph spacing
- Add 5 new round-trip tests (426 total passing)
The mobile htmx:afterSettle handler was destroying _cmView right after
the global afterSettle handler created it via initEditorPanel/initCM.
Event flow when opening a note on mobile:
1. htmx:afterSwap fires -> destroys any prior _cmView (correct)
2. Global htmx:afterSettle fires -> initEditorPanel() -> initCM() -> _cmView created
3. Mobile htmx:afterSettle fires -> _cmView.destroy() kills the just-created
CM view -> initEditorPanel() returns early (guard already set) -> blank
The destroy in the mobile afterSettle was redundant (afterSwap already
handles it before afterSettle fires) and destructive in markdown-default
mode where initCM runs during afterSettle phase.
Switching to preview then back to markdown worked because setEditorMode
re-creates _cmView via initCM when _cmView is null.
When the same note is open in multiple browsers, changes made in one
browser are now detected and surfaced in the other.
Server:
- itemService.noteFreshnessByUserIdAndJopId: cheap single-row probe
returning {updatedTime, deletedTime} without decoding the note body
- GET /api/web/notes/:id/freshness: serves the probe, marked
isHeartbeat:true so polling does not reset the session idle timer
- PUT /fragments/editor/:id conflict response now sends X-Note-Conflict:1
header so the client can distinguish it from a real save success
Client:
- checkNoteFreshness() runs on every 30s connectivity ping tick and
immediately on visibilitychange (tab focus)
- Clean editor: silently reloads the note fragment
- Dirty editor: shows a banner (same pattern as search-nav-bar) with
'Use server version' and 'Overwrite with mine' buttons
- Deleted note: shows a 'deleted in another window' banner
- _activeEditorIsDirty() uses durable signals (_previewDirty,
_pvSyncTimer, title contenteditable vs hidden input, formHash vs
_savedHash) not the transient autosave-status text
- Banner is server-rendered inside the form (hidden attribute), reuses
search-nav-bar CSS and buttons — no dynamic DOM creation
- htmx:afterRequest skips snapshotHash/setSaved when X-Note-Conflict:1
is present, keeping the editor dirty and the conflict UI visible
- Service worker bumped to v25
Browser-inserted <p><br></p> and <div><br></div> created by Enter in
contenteditable mode rendered at full line-height (~25px), while the
same markdown re-rendered server-side via /fragments/preview produced
.md-blank-line divs at 1em (~15px). Result: visible spacing drift
between live edits and post-toggle rendering.
Add CSS rules to give empty/<br>-only paragraphs and unclassed divs
the same 1em height as .md-blank-line, so contenteditable rendering
stays visually consistent with the canonical renderer.
- add app/proxy/vaultProxyGuard.js: inspects proxied note writes and
deletes before forwarding to upstream Joplin Server
- covers single PUT /api/items/root:/<id>.md:/content, batch PUT
/api/batch_items, single DELETE, and batch DELETE
- rejects with 403 when a vault note body lacks the encrypted marker,
or when a vault note is deleted via the sync proxy
- bodies over 10 MB stream through without inspection (resource blobs)
- unauthenticated requests stream through (upstream handles 401)
- wire guard into createServer.js proxy entry point; replay buffered
body via Readable.from() on allow
- 34 new unit tests, all 394 tests passing
Register BASIC with highlight.js (preview mode) and add it to the
code modal language picker. CodeMirror falls back to plain text in
the modal editor as no CM6 BASIC parser is available.