mirror of
https://github.com/abort-retry-ignore/joplock.git
synced 2026-10-19 10:13:51 +00:00
feat(editor): markdown as default open mode; fix blank editor on vault unlock
Some checks are pending
Build and push Joplock image / build-and-push (push) Waiting to run
Some checks are pending
Build and push Joplock image / build-and-push (push) Waiting to run
- Default noteOpenMode is now 'markdown' (was 'preview'); explicit
'preview' choice is still respected. Settings dropdown reordered to
match.
- preferredEditorMode() reads the live window._joplockConfig instead of
a parse-time snapshot, so the preference can't stick at a stale
default if script order ever shifts. initEditorPanel no longer lets
mobile read-only override the open-mode preference.
- _joplockConfig moved into <head>, before app.js loads.
- Fix: unlocking an encrypted note with markdown as the preferred mode
showed a blank editor. Locked notes skip editor init, so TinyMCE
never held the note's content; _completeUnlock's old
setEditorMode('markdown') call ran tinyMCESyncToTA(), which
overwrote the just-decrypted plaintext with stale/empty iframe
content. Fix seeds CM6 directly from the decrypted plaintext and
makes tinyMCESyncToTA() refuse to write when #tinymce-host isn't
visible.
- Wire graft repo context graph into AGENTS.md; add joplock db schema
diagram (docs/).
Tests: tests/noteOpenMode.test.js, tests/vaultUnlockMarkdown.test.js
(new), plus updated settingsService/templates coverage. Full suite
836/836 passing.
This commit is contained in:
parent
b701e5098d
commit
0cbec48800
14 changed files with 642 additions and 37 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -7,3 +7,6 @@ docker-compose.dev.yml
|
|||
plans/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# graft's local graph cache — regenerable, not committed (run `graft build`).
|
||||
/graft/
|
||||
|
|
|
|||
5
.ignore
Normal file
5
.ignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# graft's cards are gitignored but should stay greppable: ripgrep reads
|
||||
# .ignore before .gitignore, so this re-admits the tree to search only.
|
||||
!graft/
|
||||
graft/.cache/
|
||||
graft/.graph/
|
||||
42
AGENTS.md
42
AGENTS.md
|
|
@ -1,3 +1,45 @@
|
|||
# Agents
|
||||
|
||||
Read [`AGENT_GUIDE.md`](./AGENT_GUIDE.md) first. It is the source of truth for repo conventions, architecture, editor model, mobile shell, settings, debugging workflow, and coding rules.
|
||||
|
||||
<!-- graft:start -->
|
||||
## Graft — repo context graph
|
||||
|
||||
This repo is indexed in `graft/`: small linked markdown nodes that explain each
|
||||
system and carry exact file:line spans, kept in sync with the code through git.
|
||||
|
||||
For ANY task here — understanding how something works, finding where code lives,
|
||||
or scoping a change — get context from the graph before grepping or opening
|
||||
source files. Re-ask freely (it's cheap) and reuse literal identifiers you
|
||||
already have (symbol, error string, file name) as the query. New to this repo?
|
||||
Run `graft map` first — a token-budgeted orientation (dir clusters, hubs,
|
||||
hotspots), no LLM, no key.
|
||||
|
||||
- Run `graft ask "<your question>" --source` → ranked nodes with the relevant
|
||||
code spans inlined (each hit's ≤8-line crux by default; `--full` for whole
|
||||
definitions when the crux isn't enough). Match the tool to the task shape:
|
||||
for understanding or editing, the top node IS the answer — cite its
|
||||
`covers:` file:line spans and edit straight from `--source`. For
|
||||
exhaustive tasks ("every occurrence / every caller of this pattern"), ranked
|
||||
results are top-N, not complete — run `graft grep "<literal>"` instead
|
||||
(exhaustive over indexed files, grouped by enclosing symbol), falling back
|
||||
to raw `grep -rn` only for unindexed files.
|
||||
- `graft skeleton <file>` → every definition's signature + span, ~10× cheaper
|
||||
than reading the file; use it to skim an API surface.
|
||||
- `graft callers <symbol>` gives precomputed, exact edges — who calls this.
|
||||
Add `--direction out` for what it calls, or `--depth N` to walk
|
||||
transitively for the full blast radius. For structural questions, skip
|
||||
ranking and use this directly.
|
||||
- Or browse: `graft/INDEX.md` lists every node; follow the links.
|
||||
- Monorepos and folders of multiple repos rank fairly across sub-projects —
|
||||
hits carry `[scope/]` labels naming which one they're from. Narrow with
|
||||
`graft ask "<task>" --in <scope>/` once you know where you're working.
|
||||
|
||||
If a returned span is truncated ("+N more lines"), open the file at that exact
|
||||
range before finalizing. Only open source files when a node genuinely lacks a
|
||||
needed detail, and then at the exact file:line the node points to — never
|
||||
re-read whole files.
|
||||
|
||||
After big code changes, refresh the graph with `graft build` (deterministic,
|
||||
no API key, $0).
|
||||
<!-- graft:end -->
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ Use this guide when working in this repository.
|
|||
- Browser crypto stays client-side only; server never receives vault passwords
|
||||
- A note inside a vault notebook must be treated as protected even if its stored body is still plaintext during transition states
|
||||
- Locked vault notes render the lock overlay plus hidden editor shells; do not remove the hidden editor DOM because unlock logic depends on it
|
||||
- Unlocking a vault note with markdown as the preferred open mode must seed CM6 from the decrypted `plaintext` argument. Do not call `setEditorMode('markdown')` on unlock: that path `tinyMCESyncToTA()`s leftover/empty TinyMCE content over the just-decrypted body (locked notes skip editor init, so TinyMCE never held this note). `tinyMCESyncToTA()` must also refuse to write when `#tinymce-host` is not visible.
|
||||
- Clicking a vault lock while unlocked should lock immediately and close the open note if it belongs to that vault
|
||||
- Startup/refresh must never auto-resume an encrypted note or a note inside a vault notebook
|
||||
|
||||
|
|
@ -290,7 +291,7 @@ Joplock lives outside Joplin monorepo. Keep standalone build, test, docs, Docker
|
|||
Joplock reads same Postgres database as Joplin Server. No data duplication. Writes still go through Joplin Server API for compatibility and validation.
|
||||
|
||||
### Configurable open mode
|
||||
Notes can open in rendered mode or markdown mode based on the per-user `noteOpenMode` setting. Desktop and mobile both respect the same setting.
|
||||
Notes can open in rendered mode or markdown mode based on the per-user `noteOpenMode` setting (default **markdown**). Explicit `preview` is kept. Desktop and mobile both respect the same setting. `initEditorPanel` / `_completeUnlock` must call `preferredEditorMode()` (live `_joplockConfig.noteOpenMode`). Do not let `_tinymceReadonlyDefault()` / mobile-shell read-only force rendered mode — that override made Settings → "Open notes in: Markdown" a no-op on tablet/narrow windows. Mobile read-only still applies, but only when the note actually opens in rendered mode. `_joplockConfig` is inlined in `<head>` *before* `app.js`.
|
||||
|
||||
### Shared editor fragment
|
||||
Desktop and mobile do not have separate editor implementations. Both use the same `editorFragment()` and client editor logic; mobile wraps it in a mobile-specific shell and screen navigation layer.
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const defaultSettings = Object.freeze({
|
|||
noteMonospace: false,
|
||||
noteFontFamily: 'sans',
|
||||
newlineBehavior: 'linebreak',
|
||||
noteOpenMode: 'preview',
|
||||
noteOpenMode: 'markdown',
|
||||
resumeLastNote: true,
|
||||
lastNoteId: '',
|
||||
lastNoteFolderId: '',
|
||||
|
|
@ -208,7 +208,7 @@ const normalizeSettings = settings => ({
|
|||
codeFontSize: normalizeInteger(settings.codeFontSize, defaultSettings.codeFontSize, 10, 22),
|
||||
markdownFontSize: normalizeInteger(settings.markdownFontSize, defaultSettings.markdownFontSize, 12, 24),
|
||||
noteMonospace: !!Number(settings.noteMonospace) || settings.noteMonospace === true || settings.noteMonospace === '1',
|
||||
noteOpenMode: settings.noteOpenMode === 'markdown' ? 'markdown' : defaultSettings.noteOpenMode,
|
||||
noteOpenMode: settings.noteOpenMode === 'preview' ? 'preview' : defaultSettings.noteOpenMode,
|
||||
resumeLastNote: !!Number(settings.resumeLastNote) || settings.resumeLastNote === true || settings.resumeLastNote === '1',
|
||||
lastNoteId: `${settings.lastNoteId || ''}`,
|
||||
lastNoteFolderId: `${settings.lastNoteFolderId || ''}`,
|
||||
|
|
|
|||
|
|
@ -325,8 +325,8 @@ const editorFragment = (note, folders, currentFolderId = '', viewerUserId = '',
|
|||
<span id="autosave-status"></span>
|
||||
<button type="button" id="undo-save-btn" class="btn btn-sm btn-secondary undo-save-btn" title="Undo last save (Ctrl+Shift+Z)" onclick="undoSnapshot()" hidden>Undo</button>
|
||||
<span id="autosave-indicator" class="htmx-indicator">Saving...</span>
|
||||
<button type="button" class="btn btn-sm" id="markdown-toggle" title="Markdown source" onclick="setEditorMode('markdown')">MD</button>
|
||||
<button type="button" class="btn btn-icon active" id="preview-toggle" title="Rendered view" onclick="setEditorMode('rich')">👁</button>
|
||||
<button type="button" class="btn btn-sm active" id="markdown-toggle" title="Markdown source" onclick="setEditorMode('markdown')">MD</button>
|
||||
<button type="button" class="btn btn-icon" id="preview-toggle" title="Rendered view" onclick="setEditorMode('rich')">👁</button>
|
||||
${note.deletedTime && isOwner ? `<button type="button" class="btn btn-sm" title="Restore from trash"
|
||||
hx-post="/fragments/notes/${encodeURIComponent(note.id)}/restore"
|
||||
hx-target="#nav-panel"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const {
|
|||
} = require('./shared');
|
||||
const { noteMetaFragment } = require('./fragments');
|
||||
|
||||
const ASSET_VERSION = '20260903flushbase1';
|
||||
const ASSET_VERSION = '20260903mddefault';
|
||||
|
||||
const noteFontFamilyCSS = (settings) => {
|
||||
const f = settings.noteFontFamily || 'sans';
|
||||
|
|
@ -111,6 +111,25 @@ const layoutPage = (options = {}) => {
|
|||
<script src="/hljs.min.js"></script>
|
||||
<script src="/html-docx.js"></script>
|
||||
<script>window.joplockStripNoteTitle=${stripMarkdownForTitle.toString()};</script>
|
||||
<script>
|
||||
window._joplockConfig={
|
||||
debug:${debug ? 'true' : 'false'},
|
||||
noteOpenMode:${JSON.stringify(settings.noteOpenMode || 'markdown')},
|
||||
mobileStartup:${JSON.stringify(mobileStartup || null)},
|
||||
theme:${JSON.stringify(settings.theme || 'earth')},
|
||||
dateFormat:${JSON.stringify(String(settings.dateFormat || 'MMM-DD-YY'))},
|
||||
datetimeFormat:${JSON.stringify(String(settings.datetimeFormat || 'YYYY-MM-DD HH:mm'))},
|
||||
liveSearch:${settings.liveSearch ? 'true' : 'false'},
|
||||
highlightActiveLine:${settings.highlightActiveLine !== false ? 'true' : 'false'},
|
||||
confirmTrash:${settings.confirmTrash !== false ? 'true' : 'false'},
|
||||
encryptionAutoLockMinutes:${JSON.stringify(settings.encryptionAutoLockMinutes || 5)},
|
||||
uiMode:${JSON.stringify(settings.uiMode || 'auto')},
|
||||
proseAutocompleteSentenceCount:${JSON.stringify(settings.proseAutocompleteSentenceCount || 1)},
|
||||
maxUploadMb:${JSON.stringify(maxUploadMb)},
|
||||
textExpanders:${JSON.stringify(Array.isArray(settings.textExpanders) ? settings.textExpanders : [])},
|
||||
openRouterEnabled:${(Array.isArray(settings.aiProfiles) ? settings.aiProfiles.some(p => p.apiKey) : !!settings.openRouterApiKey) ? 'true' : 'false'}
|
||||
};
|
||||
</script>
|
||||
<script src="/app.js?v=${ASSET_VERSION}" defer></script>
|
||||
<title>Joplock</title>
|
||||
</head>
|
||||
|
|
@ -318,25 +337,6 @@ const layoutPage = (options = {}) => {
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
window._joplockConfig={
|
||||
debug:${debug ? 'true' : 'false'},
|
||||
noteOpenMode:${JSON.stringify(settings.noteOpenMode || 'preview')},
|
||||
mobileStartup:${JSON.stringify(mobileStartup || null)},
|
||||
theme:${JSON.stringify(settings.theme || 'earth')},
|
||||
dateFormat:${JSON.stringify(String(settings.dateFormat || 'MMM-DD-YY'))},
|
||||
datetimeFormat:${JSON.stringify(String(settings.datetimeFormat || 'YYYY-MM-DD HH:mm'))},
|
||||
liveSearch:${settings.liveSearch ? 'true' : 'false'},
|
||||
highlightActiveLine:${settings.highlightActiveLine !== false ? 'true' : 'false'},
|
||||
confirmTrash:${settings.confirmTrash !== false ? 'true' : 'false'},
|
||||
encryptionAutoLockMinutes:${JSON.stringify(settings.encryptionAutoLockMinutes || 5)},
|
||||
uiMode:${JSON.stringify(settings.uiMode || 'auto')},
|
||||
proseAutocompleteSentenceCount:${JSON.stringify(settings.proseAutocompleteSentenceCount || 1)},
|
||||
maxUploadMb:${JSON.stringify(maxUploadMb)},
|
||||
textExpanders:${JSON.stringify(Array.isArray(settings.textExpanders) ? settings.textExpanders : [])},
|
||||
openRouterEnabled:${(Array.isArray(settings.aiProfiles) ? settings.aiProfiles.some(p => p.apiKey) : !!settings.openRouterApiKey) ? 'true' : 'false'}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -230,8 +230,8 @@ const settingsPage = (options = {}) => {
|
|||
<label class="settings-field">
|
||||
<span>Open notes in</span>
|
||||
<select id="settings-note-open-mode" class="login-input" onchange="saveSetting('noteOpenMode',this.value)">
|
||||
<option value="preview"${(settings.noteOpenMode || 'preview') === 'preview' ? ' selected' : ''}>Rendered mode</option>
|
||||
<option value="markdown"${settings.noteOpenMode === 'markdown' ? ' selected' : ''}>Markdown mode</option>
|
||||
<option value="markdown"${settings.noteOpenMode !== 'preview' ? ' selected' : ''}>Markdown mode</option>
|
||||
<option value="preview"${settings.noteOpenMode === 'preview' ? ' selected' : ''}>Rendered mode</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
|
|
|
|||
248
docs/joplock-db-schema.html
Normal file
248
docs/joplock-db-schema.html
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Joplock sidecar schema</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
.frame { max-width: 960px; width: 100%; }
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.lede {
|
||||
font-size: 0.92rem;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 40rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
svg { width: 100%; min-width: 720px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Database schema · Joplock sidecar</p>
|
||||
<h1>Four tables. Same Postgres. No Joplin FKs.</h1>
|
||||
<p class="lede">Joplock owns these four tables. Joplin users / items / sessions stay Joplin’s. IDs bind in application code, not in DDL.</p>
|
||||
|
||||
<svg viewBox="0 0 960 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="joplock-db-schema-title joplock-db-schema-desc">
|
||||
<title id="joplock-db-schema-title">Joplock sidecar schema</title>
|
||||
<desc id="joplock-db-schema-desc">Physical schema of the four Joplock sidecar tables in public Postgres: sessions, settings, vaults, and history. No SQL foreign keys; user_id, note_id, folder_id, and session_id are application-level pointers into Joplin.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Callout 1: __app__ sentinel on settings.user_id -->
|
||||
<text x="912" y="28" fill="#2d3142" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">user_id = '__app__' is admin settings</text>
|
||||
<path d="M 780 36 Q 760 56 720 80" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<circle cx="720" cy="80" r="2" fill="#2d3142"/>
|
||||
|
||||
<!-- ========== public.joplock_sessions x=48 y=40 w=400 h=76 ========== -->
|
||||
<rect x="48" y="40" width="400" height="76" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="48" y="40" width="400" height="76" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="48" y="40" width="400" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="48" y="60" width="400" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="48" y1="68" x2="448" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="60" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.joplock_sessions</text>
|
||||
<rect x="396" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="416" y="57" fill="#2d3142" opacity="0.8" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="48" y="68" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="60" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">session_id</text>
|
||||
<rect x="268" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<rect x="292" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="302" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(64)</text>
|
||||
|
||||
<text x="60" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">last_seen</text>
|
||||
<rect x="268" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">bigint</text>
|
||||
|
||||
<!-- ========== public.joplock_settings x=512 y=40 w=400 h=124 ========== -->
|
||||
<rect x="512" y="40" width="400" height="124" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="512" y="40" width="400" height="124" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="512" y="40" width="400" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="512" y="60" width="400" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="512" y1="68" x2="912" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="524" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.joplock_settings</text>
|
||||
<rect x="860" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="880" y="57" fill="#2d3142" opacity="0.8" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="512" y="68" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">user_id</text>
|
||||
<rect x="732" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="900" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(32)</text>
|
||||
|
||||
<text x="524" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">settings</text>
|
||||
<rect x="732" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">jsonb</text>
|
||||
|
||||
<rect x="512" y="116" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="132" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">updated_time</text>
|
||||
<rect x="732" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="133" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="132" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">bigint</text>
|
||||
|
||||
<text x="524" y="156" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">totp_seed</text>
|
||||
<text x="900" y="156" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(64)</text>
|
||||
|
||||
<!-- ========== public.joplock_vaults x=48 y=192 w=400 h=212 ========== -->
|
||||
<rect x="48" y="192" width="400" height="212" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="48" y="192" width="400" height="212" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="48" y="192" width="400" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="48" y="212" width="400" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="48" y1="220" x2="448" y2="220" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="60" y="210" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.joplock_vaults</text>
|
||||
<rect x="396" y="200" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="416" y="209" fill="#2d3142" opacity="0.8" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="48" y="220" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="60" y="236" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="268" y="228" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="237" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="436" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">serial</text>
|
||||
|
||||
<text x="60" y="260" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">user_id</text>
|
||||
<rect x="268" y="252" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="261" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(64)</text>
|
||||
|
||||
<rect x="48" y="268" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="60" y="284" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">folder_id</text>
|
||||
<rect x="268" y="276" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="285" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="284" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(64)</text>
|
||||
|
||||
<text x="60" y="308" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">salt</text>
|
||||
<rect x="268" y="300" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="309" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="308" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="48" y="316" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="60" y="332" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">verify</text>
|
||||
<rect x="268" y="324" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="278" y="333" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="436" y="332" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="60" y="356" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">created_at</text>
|
||||
<text x="436" y="356" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<line x1="48" y1="364" x2="448" y2="364" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="60" y="380" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="60" y="396" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">UNIQUE (user_id, folder_id)</text>
|
||||
|
||||
<!-- ========== public.joplock_history x=512 y=192 w=400 h=236 ========== -->
|
||||
<rect x="512" y="192" width="400" height="236" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="512" y="192" width="400" height="236" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="512" y="192" width="400" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="512" y="212" width="400" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="512" y1="220" x2="912" y2="220" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="524" y="210" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.joplock_history</text>
|
||||
<rect x="860" y="200" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="880" y="209" fill="#2d3142" opacity="0.8" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="512" y="220" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="236" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="732" y="228" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="237" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="900" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">bigserial</text>
|
||||
|
||||
<text x="524" y="260" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">note_id</text>
|
||||
<rect x="732" y="252" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="261" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(32)</text>
|
||||
|
||||
<rect x="512" y="268" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="284" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">user_id</text>
|
||||
<rect x="732" y="276" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="285" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="284" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(32)</text>
|
||||
|
||||
<text x="524" y="308" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">title</text>
|
||||
<rect x="732" y="300" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="309" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="308" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="512" y="316" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="332" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">body</text>
|
||||
<rect x="732" y="324" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="333" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="332" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="524" y="356" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">body_hash</text>
|
||||
<rect x="732" y="348" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="357" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="356" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">varchar(16)</text>
|
||||
|
||||
<rect x="512" y="364" width="400" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="524" y="380" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">saved_time</text>
|
||||
<rect x="732" y="372" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="742" y="381" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="900" y="380" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">bigint</text>
|
||||
|
||||
<line x1="512" y1="388" x2="912" y2="388" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="524" y="404" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="524" y="420" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">joplock_history_note_time (note_id, saved_time DESC)</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="48" y1="452" x2="912" y2="452" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="48" y="468" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LEGEND</text>
|
||||
<rect x="140" y="458" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="160" y="467" fill="#2d3142" opacity="0.8" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
<text x="188" y="468" fill="#4f5d75" font-size="8" font-family="'Geist', sans-serif">sidecar table</text>
|
||||
<rect x="300" y="458" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="310" y="467" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="328" y="468" fill="#4f5d75" font-size="8" font-family="'Geist', sans-serif">primary key</text>
|
||||
<rect x="428" y="458" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="438" y="467" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="456" y="468" fill="#4f5d75" font-size="8" font-family="'Geist', sans-serif">not null</text>
|
||||
<text x="560" y="468" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace">INDEXES</text>
|
||||
<text x="628" y="468" fill="#4f5d75" font-size="8" font-family="'Geist', sans-serif">unique / lookup</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -267,7 +267,13 @@ function startAutoLockTimer(){
|
|||
// They are session-scoped and are explicitly cleared on logout/login cleanup.
|
||||
;
|
||||
|
||||
var _defaultNoteOpenMode=_cfg.noteOpenMode||'preview';
|
||||
var _defaultNoteOpenMode=_cfg.noteOpenMode||'markdown';
|
||||
// Live read of the Settings preference. `_cfg` is snapshotted at parse time;
|
||||
// if `_joplockConfig` was not on the page yet (script order), `_defaultNoteOpenMode`
|
||||
// would stick at 'preview' and every note would open rendered. Always prefer the
|
||||
// live config object. Mobile read-only must NOT override this — a markdown
|
||||
// preference means open in CM6, on desktop and mobile.
|
||||
function preferredEditorMode(){var v='';try{v=(window._joplockConfig&&window._joplockConfig.noteOpenMode)||''}catch(_e){}if(!v)v=_defaultNoteOpenMode||'';return v==='markdown'?'markdown':'rich'}
|
||||
var _highlightActiveLine=_cfg.highlightActiveLine!==false;
|
||||
var _mobileStartup=_cfg.mobileStartup||null;
|
||||
var _uiMode=_cfg.uiMode||'auto';
|
||||
|
|
@ -465,7 +471,7 @@ function cmSetVal(v){if(!_cmView)return;var cur=_cmView.state.doc.toString();if(
|
|||
function getTinyMCE(){return _tinymceEditor}
|
||||
function tinyMCEContent(){return _tinymceEditor?_tinymceEditor.getContent():''}
|
||||
function tinyMCESetContent(html){if(_tinymceEditor)_tinymceEditor.setContent(html)}
|
||||
function tinyMCESyncToTA(){var ta=getTA();if(ta&&_tinymceEditor){var _synNoteId=_formNoteId(activeEditorForm());if(_tinymceContentNoteId&&_synNoteId&&_tinymceContentNoteId!==_synNoteId){_log('tinyMCESyncToTA skipped: TinyMCE content belongs to another note',_tinymceContentNoteId,_synNoteId);return false}var html=_tinymceEditor.getContent();var md=tinymceToMarkdown(html);if(ta.value!==md){ta.value=md;ta.dispatchEvent(new Event('input',{bubbles:true}));return true}}return false}
|
||||
function tinyMCESyncToTA(){var ta=getTA();if(ta&&_tinymceEditor){var _synNoteId=_formNoteId(activeEditorForm());if(_tinymceContentNoteId&&_synNoteId&&_tinymceContentNoteId!==_synNoteId){_log('tinyMCESyncToTA skipped: TinyMCE content belongs to another note',_tinymceContentNoteId,_synNoteId);return false}var host=document.getElementById('tinymce-host');if(!host||!host.classList.contains('tinymce-host-visible')){_log('tinyMCESyncToTA skipped: TinyMCE host not visible');return false}var html=_tinymceEditor.getContent();var md=tinymceToMarkdown(html);if(ta.value!==md){ta.value=md;ta.dispatchEvent(new Event('input',{bubbles:true}));return true}}return false}
|
||||
function _isMarkdownModeActive(){return _editorMode==='markdown'||_editorMode==='md'}
|
||||
|
||||
/* ---------------- Note export (rendered mode only): MD / HTML / DOCX / PDF ---------------- */
|
||||
|
|
@ -1200,7 +1206,7 @@ function initPersistentTinyMCE(){
|
|||
if(goEditable){
|
||||
// Entering edit mode: honor the saved note-open preference,
|
||||
// which may be markdown (read-only always shows rendered).
|
||||
if(_defaultNoteOpenMode==='markdown'&&_editorMode!=='markdown'){setEditorMode('markdown');}
|
||||
if(preferredEditorMode()==='markdown'&&_editorMode!=='markdown'){setEditorMode('markdown');}
|
||||
else{try{editor.focus()}catch(_e){}}
|
||||
}else{
|
||||
// Leaving edit mode: always drop back to rendered (read-only)
|
||||
|
|
@ -4441,7 +4447,7 @@ function snapshotHash(){var form=activeEditorForm();_savedHash=formHash(form);_d
|
|||
function _isLockedOverlayEventTarget(target){return !!(target&&target.closest&&target.closest('#editor-locked'))}
|
||||
function initEditorPanel(){var form=activeEditorForm();if(form)_displayedNoteId=_formNoteId(form);if(!form||form.dataset.editorInit)return;form.dataset.editorInit='1';if(form.dataset.shareReadonly==='1'){_applyFormReadonly(true);}_resetRingBuffer('note-switch');_dbgline('initEditorPanel begin',form.getAttribute('hx-put'));if(isMobileShellMode())closeNav();_previewDirty=false;setSaveState('','');snapshotHash();_snapshots=[];var undoBtn=queryActiveEditor('#undo-save-btn');if(undoBtn)undoBtn.hidden=true;pushSnapshot();form.addEventListener('input',function(e){if(_isLockedOverlayEventTarget(e.target))return;_dbgline('form input',{tag:e.target&&e.target.tagName,id:e.target&&e.target.id,name:e.target&&e.target.name});markEdited();scheduleSave()});form.addEventListener('change',function(e){if(_isLockedOverlayEventTarget(e.target))return;_dbgline('form change',{tag:e.target&&e.target.tagName,id:e.target&&e.target.id,name:e.target&&e.target.name});markEdited();scheduleSave()});initAutoTitle();applyMobileTitleMode();renderNoteMeta(); var ta=getTA();if(ta){ta.addEventListener('input',function(){autoTitle()});ta.addEventListener('keydown',function(e){if(_editorMode!=='markdown'&&_editorMode!=='md')return;if(e.key!=='Enter')return;var mac=navigator.platform&&navigator.platform.indexOf('Mac')!==-1;var mod=mac?e.metaKey:e.ctrlKey;if(mod){// Ctrl/Cmd+Enter = soft break (\n, same paragraph)
|
||||
e.preventDefault();var start=ta.selectionStart,end=ta.selectionEnd;ta.value=ta.value.slice(0,start)+'\n'+ta.value.slice(end);ta.selectionStart=ta.selectionEnd=start+1;ta.dispatchEvent(new Event('input',{bubbles:true}))}else{// Enter = new paragraph (\n\n)
|
||||
e.preventDefault();var start=ta.selectionStart,end=ta.selectionEnd;ta.value=ta.value.slice(0,start)+'\n\n'+ta.value.slice(end);ta.selectionStart=ta.selectionEnd=start+2;ta.dispatchEvent(new Event('input',{bubbles:true}))}})}var pendingSearch=(window._pendingNoteSearchTerm||'').trim();var mobileEditor=inMobileEditor();if(mobileEditor&&pendingSearch){var header=document.getElementById('mobile-editor-header');var searchHeader=document.getElementById('mobile-editor-search-header');if(header)header.style.display='none';if(searchHeader)searchHeader.style.display=''}var searchInput=activeSearchInput();if(searchInput&&pendingSearch&&!searchInput.value)searchInput.value=pendingSearch;window._pendingNoteSearchTerm='';/* Persistent TinyMCE: refresh content for this note (skip locked encrypted notes) */if(form.dataset.encrypted!=='1'){var _mobileRO=_tinymceReadonlyDefault();_editorMode=(_mobileRO?false:_defaultNoteOpenMode==='markdown')?'markdown':'rich';_tinymceReadonly=_mobileRO;syncEditorModeButtons();if(_editorMode==='markdown'){hideTinyMCEHost();applyEditorModeVisibility('markdown');var mdta=getTA();mountMarkdownEditor(mdta?mdta.value:'');initPersistentTinyMCE()}else{initPersistentTinyMCE();refreshTinyMCEForActiveNote()}if(pendingSearch){var _pendTerm=pendingSearch;if(_editorMode==='markdown'){setTimeout(function(){var si=activeSearchInput();if(si&&!si.value)si.value=_pendTerm;applySearchHighlight()},0)}else{/* rich: highlight is (re)applied by _setTinyMCEContent once the body is painted (covers sync + async render) */var si2=activeSearchInput();if(si2&&!si2.value)si2.value=_pendTerm;_log('initEditorPanel: setting pendingSearchHighlight, rich mode, term='+(_pendTerm||''));_pendingSearchHighlight=true;/* Fallback: if no setContent fires (e.g. same-note reopen with body already loaded), apply once the body has text. */var _tries=0;var _fb=setInterval(function(){_tries++;if(!_pendingSearchHighlight||_tries>20){clearInterval(_fb);return}var _b=_tinymceEditor&&_tinymceEditor.getBody&&_tinymceEditor.getBody();if(_b&&(_b.textContent||'').trim()&&activeSearchTerm()&&activeSearchTerm().trim()){_pendingSearchHighlight=false;clearInterval(_fb);applySearchHighlight()}},50)}}}else{hideTinyMCEHost()}}
|
||||
e.preventDefault();var start=ta.selectionStart,end=ta.selectionEnd;ta.value=ta.value.slice(0,start)+'\n\n'+ta.value.slice(end);ta.selectionStart=ta.selectionEnd=start+2;ta.dispatchEvent(new Event('input',{bubbles:true}))}})}var pendingSearch=(window._pendingNoteSearchTerm||'').trim();var mobileEditor=inMobileEditor();if(mobileEditor&&pendingSearch){var header=document.getElementById('mobile-editor-header');var searchHeader=document.getElementById('mobile-editor-search-header');if(header)header.style.display='none';if(searchHeader)searchHeader.style.display=''}var searchInput=activeSearchInput();if(searchInput&&pendingSearch&&!searchInput.value)searchInput.value=pendingSearch;window._pendingNoteSearchTerm='';/* Persistent TinyMCE: refresh content for this note (skip locked encrypted notes) */if(form.dataset.encrypted!=='1'){var _mobileRO=_tinymceReadonlyDefault();_editorMode=preferredEditorMode();_tinymceReadonly=_mobileRO&&_editorMode!=='markdown';syncEditorModeButtons();if(_editorMode==='markdown'){hideTinyMCEHost();applyEditorModeVisibility('markdown');var mdta=getTA();mountMarkdownEditor(mdta?mdta.value:'');initPersistentTinyMCE()}else{initPersistentTinyMCE();refreshTinyMCEForActiveNote()}if(pendingSearch){var _pendTerm=pendingSearch;if(_editorMode==='markdown'){setTimeout(function(){var si=activeSearchInput();if(si&&!si.value)si.value=_pendTerm;applySearchHighlight()},0)}else{/* rich: highlight is (re)applied by _setTinyMCEContent once the body is painted (covers sync + async render) */var si2=activeSearchInput();if(si2&&!si2.value)si2.value=_pendTerm;_log('initEditorPanel: setting pendingSearchHighlight, rich mode, term='+(_pendTerm||''));_pendingSearchHighlight=true;/* Fallback: if no setContent fires (e.g. same-note reopen with body already loaded), apply once the body has text. */var _tries=0;var _fb=setInterval(function(){_tries++;if(!_pendingSearchHighlight||_tries>20){clearInterval(_fb);return}var _b=_tinymceEditor&&_tinymceEditor.getBody&&_tinymceEditor.getBody();if(_b&&(_b.textContent||'').trim()&&activeSearchTerm()&&activeSearchTerm().trim()){_pendingSearchHighlight=false;clearInterval(_fb);applySearchHighlight()}},50)}}}else{hideTinyMCEHost()}}
|
||||
function applySearchHighlight(){var term=activeSearchTerm();_log('applySearchHighlight mode='+_editorMode+' term='+(term||''));var bar=document.getElementById('search-nav-bar');if(bar)bar.hidden=true;_searchMarks=[];_searchMarkIdx=0;var pv=queryActiveEditor('#note-preview');if(pv)clearPreviewSearchMarks(pv);clearTinyMCESearchMarks();if(!term||!term.trim()){clearCodeMirrorSearch();return}term=term.trim();if(_editorMode==='markdown'||_editorMode==='md'){_log('applySearchHighlight: markdown/CM6 branch');if(_cmView&&window.CM&&window.CM.SearchQuery&&window.CM.setSearchQuery){window.CM.openSearchPanel(_cmView);var _qParts=term.split(/\s+/).filter(Boolean);var q=_qParts.length>1?new window.CM.SearchQuery({search:_qParts.map(function(t){return escapeRegex(t)}).join('|'),caseSensitive:false,regexp:true}):new window.CM.SearchQuery({search:term,caseSensitive:false});_cmView.dispatch({effects:window.CM.setSearchQuery.of(q)});_cmSearchMatches=collectCodeMirrorSearchMatches(q);if(_cmSearchMatches.length)setCodeMirrorSearchActive(0);else searchNavShow(0,0)}}else if(_editorMode==='preview'&&pv){clearCodeMirrorSearch();var savedHandler=pv.oninput;pv.oninput=null;highlightInPreview(pv,term);pv.oninput=savedHandler}else{_log('applySearchHighlight: rich/TinyMCE branch');clearCodeMirrorSearch();highlightInTinyMCE(term)}}
|
||||
function escapeRegex(s){var specials=['.','+','*','?','^','$','(',')','{','}','[',']','|','\\'];return s.split('').map(function(c){return specials.indexOf(c)>=0?'\\'+c:c}).join('')}
|
||||
// Multi-term searches match each word independently, so highlight every term
|
||||
|
|
@ -5714,8 +5720,22 @@ function _completeUnlock(noteId,plaintext,vaultId){
|
|||
if(mdBtn)mdBtn.style.display='';
|
||||
if(pvBtn)pvBtn.style.display='';
|
||||
|
||||
// Open in the user's preferred mode — setEditorMode handles CM6 mount / TinyMCE setContent.
|
||||
setEditorMode(_defaultNoteOpenMode==='markdown'?'markdown':'rich');
|
||||
// Open in the user's preferred mode.
|
||||
// Locked notes skip editor init, so TinyMCE never received this note's
|
||||
// plaintext. setEditorMode('markdown') would tinyMCESyncToTA() leftover
|
||||
// (or empty) iframe content over the just-decrypted body — unlock then
|
||||
// shows a blank editor. Seed markdown from `plaintext` directly. Rich
|
||||
// mode is safe: cmSyncToTA is a no-op when CM isn't mounted, then the
|
||||
// preview fetch uses ta.value (plaintext).
|
||||
if(preferredEditorMode()==='markdown'){
|
||||
applyEditorModeVisibility('markdown');
|
||||
mountMarkdownEditor(plaintext);
|
||||
_editorMode='markdown';
|
||||
syncEditorModeButtons();
|
||||
_reconcileSaveStateAfterModeSwitch();
|
||||
}else{
|
||||
setEditorMode('rich');
|
||||
}
|
||||
snapshotHash();
|
||||
|
||||
_updateLockToggle(noteId,true);
|
||||
|
|
|
|||
77
tests/noteOpenMode.test.js
Normal file
77
tests/noteOpenMode.test.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Settings → "Open notes in: Markdown" must actually open notes in CM6.
|
||||
*
|
||||
* Two regressions this pins:
|
||||
* 1. `_joplockConfig` must be inlined before app.js so `_cfg.noteOpenMode`
|
||||
* is set when app.js parses (otherwise it sticks at 'preview').
|
||||
* 2. initEditorPanel must not let mobile read-only (`_mobileRO`) force
|
||||
* rendered mode — that made the setting a no-op on tablet/narrow windows.
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
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, i = start;
|
||||
while (i < appSrc.length) {
|
||||
if (appSrc[i] === '{') depth++;
|
||||
else if (appSrc[i] === '}') { depth--; if (depth === 0) return appSrc.slice(start, i + 1); }
|
||||
i++;
|
||||
}
|
||||
throw new Error(`Could not find closing brace for function ${name}`);
|
||||
}
|
||||
|
||||
test('preferredEditorMode reads live _joplockConfig, not only the parse-time snapshot', () => {
|
||||
const src = extractFn('preferredEditorMode');
|
||||
assert.ok(src.includes('window._joplockConfig'), 'must re-read live config');
|
||||
assert.ok(src.includes("==='markdown'?'markdown':'rich'"), 'markdown preference must map to markdown mode');
|
||||
});
|
||||
|
||||
test('initEditorPanel uses preferredEditorMode and does not let mobile RO force rich', () => {
|
||||
const src = extractFn('initEditorPanel');
|
||||
assert.ok(src.includes('preferredEditorMode()'), 'initEditorPanel must honor Settings noteOpenMode via preferredEditorMode()');
|
||||
assert.ok(!src.includes('_mobileRO?false:_defaultNoteOpenMode'), 'must not let mobile read-only override the open-mode preference');
|
||||
assert.ok(src.includes('_tinymceReadonly=_mobileRO&&_editorMode!==\'markdown\''), 'mobile read-only applies only when the note opens rendered');
|
||||
});
|
||||
|
||||
test('preferredEditorMode: live markdown config wins over a stale preview snapshot', () => {
|
||||
const dom = new JSDOM('<!DOCTYPE html><body></body>', { url: 'https://joplock.test' });
|
||||
const ctx = vm.createContext({
|
||||
window: dom.window,
|
||||
document: dom.window.document,
|
||||
_defaultNoteOpenMode: 'preview',
|
||||
});
|
||||
dom.window._joplockConfig = { noteOpenMode: 'markdown' };
|
||||
vm.runInContext(extractFn('preferredEditorMode'), ctx);
|
||||
assert.equal(vm.runInContext('preferredEditorMode()', ctx), 'markdown');
|
||||
});
|
||||
|
||||
test('preferredEditorMode: missing live config falls back to snapshot', () => {
|
||||
const dom = new JSDOM('<!DOCTYPE html><body></body>', { url: 'https://joplock.test' });
|
||||
const ctx = vm.createContext({
|
||||
window: dom.window,
|
||||
document: dom.window.document,
|
||||
_defaultNoteOpenMode: 'markdown',
|
||||
});
|
||||
vm.runInContext(extractFn('preferredEditorMode'), ctx);
|
||||
assert.equal(vm.runInContext('preferredEditorMode()', ctx), 'markdown');
|
||||
});
|
||||
|
||||
test('preferredEditorMode: preview / unknown → rich', () => {
|
||||
const dom = new JSDOM('<!DOCTYPE html><body></body>', { url: 'https://joplock.test' });
|
||||
const ctx = vm.createContext({
|
||||
window: dom.window,
|
||||
document: dom.window.document,
|
||||
_defaultNoteOpenMode: 'preview',
|
||||
});
|
||||
dom.window._joplockConfig = { noteOpenMode: 'preview' };
|
||||
vm.runInContext(extractFn('preferredEditorMode'), ctx);
|
||||
assert.equal(vm.runInContext('preferredEditorMode()', ctx), 'rich');
|
||||
});
|
||||
|
|
@ -116,6 +116,14 @@ test('settingsService returns defaults when table creation fails', async () => {
|
|||
assert.deepEqual(settings, defaultSettings);
|
||||
});
|
||||
|
||||
test('normalizeSettings defaults noteOpenMode to markdown', () => {
|
||||
assert.equal(defaultSettings.noteOpenMode, 'markdown');
|
||||
assert.equal(normalizeSettings({ noteOpenMode: 'markdown' }).noteOpenMode, 'markdown');
|
||||
assert.equal(normalizeSettings({ noteOpenMode: 'preview' }).noteOpenMode, 'preview', 'explicit rendered preference must be kept');
|
||||
assert.equal(normalizeSettings({ noteOpenMode: 'nope' }).noteOpenMode, 'markdown');
|
||||
assert.equal(normalizeSettings({}).noteOpenMode, 'markdown');
|
||||
});
|
||||
|
||||
test('normalizeSettings clamps autoLogoutMinutes', () => {
|
||||
assert.equal(normalizeSettings({ autoLogoutMinutes: 0 }).autoLogoutMinutes, 1);
|
||||
assert.equal(normalizeSettings({ autoLogoutMinutes: 999 }).autoLogoutMinutes, 480);
|
||||
|
|
|
|||
|
|
@ -723,16 +723,20 @@ test('editorFragment uses openUploadModal for upload toolbar action', () => {
|
|||
});
|
||||
|
||||
test('logged in layout emits inline config script that parses', () => {
|
||||
const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '<div></div>' });
|
||||
// The last script before </body> is now just the config object
|
||||
const match = html.match(/<script>\s*(window\._joplockConfig[\s\S]*?)<\/script>\s*<\/body>/);
|
||||
assert.ok(match, 'should have inline config script before </body>');
|
||||
const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '<div></div>', settings: { noteOpenMode: 'markdown' } });
|
||||
const match = html.match(/<script>\s*(window\._joplockConfig[\s\S]*?)<\/script>/);
|
||||
assert.ok(match, 'should have inline config script');
|
||||
assert.doesNotThrow(() => new vm.Script(match[1]));
|
||||
assert.ok(match[1].includes('window._joplockConfig'));
|
||||
assert.ok(match[1].includes('noteOpenMode'));
|
||||
assert.ok(match[1].includes('"markdown"'), 'markdown preference must be injected into _joplockConfig');
|
||||
assert.ok(match[1].includes('liveSearch'));
|
||||
assert.ok(!match[1].includes('proseAutocompleteManualTrigger'));
|
||||
assert.ok(!match[1].includes('proseAutocompleteManualTriggerOptions'));
|
||||
// Config MUST precede app.js so `_cfg.noteOpenMode` is set when app.js parses.
|
||||
const cfgIdx = html.indexOf('window._joplockConfig');
|
||||
const appIdx = html.indexOf('/app.js');
|
||||
assert.ok(cfgIdx !== -1 && appIdx !== -1 && cfgIdx < appIdx, '_joplockConfig must be defined before app.js loads');
|
||||
// Functions are in app.js, not inline
|
||||
assert.ok(!html.includes('function openFolderContextMenu(event,id,title)'));
|
||||
assert.ok(html.includes('/app.js'));
|
||||
|
|
|
|||
197
tests/vaultUnlockMarkdown.test.js
Normal file
197
tests/vaultUnlockMarkdown.test.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* Regression: unlocking an encrypted note with markdown as the preferred
|
||||
* open mode showed a blank editor.
|
||||
*
|
||||
* Root cause: locked notes skip editor init, so TinyMCE never received this
|
||||
* note's plaintext (and `#tinymce-host` stays hidden). `_completeUnlock`
|
||||
* used to call `setEditorMode('markdown')`, which `tinyMCESyncToTA()`s the
|
||||
* leftover/empty iframe over the just-decrypted textarea, then mounts CM6
|
||||
* from that wiped body.
|
||||
*
|
||||
* Pins:
|
||||
* * `_completeUnlock` seeds markdown from the `plaintext` argument.
|
||||
* * `tinyMCESyncToTA` refuses to write when the TinyMCE host is hidden
|
||||
* (empty `_tinymceContentNoteId` is otherwise permissive).
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
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, i = start;
|
||||
while (i < appSrc.length) {
|
||||
if (appSrc[i] === '{') depth++;
|
||||
else if (appSrc[i] === '}') { depth--; if (depth === 0) return appSrc.slice(start, i + 1); }
|
||||
i++;
|
||||
}
|
||||
throw new Error(`Could not find closing brace for function ${name}`);
|
||||
}
|
||||
|
||||
const NOTE_ID = 'a'.repeat(32);
|
||||
const VAULT_ID = 'b'.repeat(32);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static wiring
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('_completeUnlock markdown path seeds CM6 from plaintext, not setEditorMode', () => {
|
||||
const src = extractFn('_completeUnlock');
|
||||
assert.ok(src.includes("if(preferredEditorMode()==='markdown')"), '_completeUnlock must branch on markdown preference');
|
||||
assert.ok(src.includes('mountMarkdownEditor(plaintext)'), '_completeUnlock must mount CM6 from the decrypted plaintext argument');
|
||||
assert.ok(!src.includes("setEditorMode(_defaultNoteOpenMode==='markdown'?'markdown':'rich')"), '_completeUnlock must not route markdown unlock through setEditorMode (that tinyMCESyncToTA()s leftover iframe content over the decrypted body)');
|
||||
assert.ok(src.includes("setEditorMode('rich')"), 'rich-preference unlock may still use setEditorMode (cmSyncToTA is a no-op when CM is unmounted)');
|
||||
});
|
||||
|
||||
test('tinyMCESyncToTA skips when the TinyMCE host is not visible', () => {
|
||||
const src = extractFn('tinyMCESyncToTA');
|
||||
assert.ok(src.includes("tinyMCESyncToTA skipped: TinyMCE host not visible"), 'tinyMCESyncToTA must refuse to write when #tinymce-host is hidden');
|
||||
assert.ok(src.includes('tinymce-host-visible'), 'visibility check must use the tinymce-host-visible class');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Behavioral
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeUnlockDom({ hostVisible = false } = {}) {
|
||||
const hostClass = hostVisible ? 'tinymce-host tinymce-host-visible' : 'tinymce-host';
|
||||
return new JSDOM(`<!DOCTYPE html>
|
||||
<body>
|
||||
<form id="note-editor-form" class="editor-form"
|
||||
data-note-id="${NOTE_ID}" data-encrypted="1" data-vault-id="${VAULT_ID}"
|
||||
hx-put="/fragments/editor/${NOTE_ID}">
|
||||
<div class="editor-locked" id="editor-locked">locked</div>
|
||||
<div class="editor-toolbar" id="editor-toolbar" style="display:none"></div>
|
||||
<button type="button" id="markdown-toggle" style="display:none">MD</button>
|
||||
<button type="button" id="preview-toggle" style="display:none">PV</button>
|
||||
<textarea name="body" class="editor-body" id="note-body">CIPHERTEXT</textarea>
|
||||
<div id="cm-host" class="cm-host" style="display:none"></div>
|
||||
<div id="tinymce-slot" class="tinymce-slot"></div>
|
||||
</form>
|
||||
<div id="tinymce-host" class="${hostClass}"></div>
|
||||
</body>`, { url: 'https://joplock.test' });
|
||||
}
|
||||
|
||||
function makeSyncSandbox({ hostVisible, taValue, tinymceHtml, contentNoteId = '' }) {
|
||||
const dom = makeUnlockDom({ hostVisible });
|
||||
const ta = dom.window.document.getElementById('note-body');
|
||||
ta.value = taValue;
|
||||
const ctx = vm.createContext({
|
||||
document: dom.window.document,
|
||||
window: dom.window,
|
||||
Event: dom.window.Event,
|
||||
_tinymceEditor: { getContent() { return tinymceHtml; } },
|
||||
_tinymceContentNoteId: contentNoteId,
|
||||
_logs: [],
|
||||
});
|
||||
vm.runInContext(`
|
||||
function activeEditorForm(){return document.getElementById('note-editor-form')}
|
||||
function _formNoteId(form){return (form&&form.dataset&&form.dataset.noteId)||''}
|
||||
function getTA(){return document.getElementById('note-body')}
|
||||
function tinymceToMarkdown(html){return String(html||'').replace(/<[^>]+>/g,'').trim()}
|
||||
function _log(){ _logs.push([].slice.call(arguments).join(' ')); }
|
||||
`, ctx);
|
||||
vm.runInContext(extractFn('tinyMCESyncToTA'), ctx);
|
||||
return { ctx, ta };
|
||||
}
|
||||
|
||||
test('tinyMCESyncToTA does not clobber textarea when TinyMCE host is hidden', () => {
|
||||
const { ctx, ta } = makeSyncSandbox({
|
||||
hostVisible: false,
|
||||
taValue: 'just decrypted plaintext',
|
||||
tinymceHtml: '<p></p>',
|
||||
contentNoteId: '',
|
||||
});
|
||||
const changed = vm.runInContext('tinyMCESyncToTA()', ctx);
|
||||
assert.equal(changed, false, 'must report no change');
|
||||
assert.equal(ta.value, 'just decrypted plaintext', 'hidden TinyMCE leftover/empty content must not overwrite decrypted plaintext');
|
||||
});
|
||||
|
||||
test('tinyMCESyncToTA still syncs when TinyMCE host is visible (rich→markdown switch)', () => {
|
||||
const { ctx, ta } = makeSyncSandbox({
|
||||
hostVisible: true,
|
||||
taValue: 'stale textarea',
|
||||
tinymceHtml: '<p>live rich edits</p>',
|
||||
contentNoteId: NOTE_ID,
|
||||
});
|
||||
const changed = vm.runInContext('tinyMCESyncToTA()', ctx);
|
||||
assert.equal(changed, true, 'visible TinyMCE must sync into the textarea');
|
||||
assert.equal(ta.value, 'live rich edits', 'visible TinyMCE content is the source of truth for a real mode switch');
|
||||
});
|
||||
|
||||
function makeUnlockSandbox({ openMode = 'markdown' } = {}) {
|
||||
const dom = makeUnlockDom({ hostVisible: false });
|
||||
const mounted = [];
|
||||
const ctx = vm.createContext({
|
||||
document: dom.window.document,
|
||||
window: dom.window,
|
||||
Event: dom.window.Event,
|
||||
_defaultNoteOpenMode: openMode,
|
||||
_editorMode: 'rich',
|
||||
_tinymceEditor: { getContent() { return '<p>LEFTOVER FROM PREVIOUS NOTE</p>'; } },
|
||||
_tinymceContentNoteId: '',
|
||||
_mounted: mounted,
|
||||
_saveTimer: null,
|
||||
_savedHash: 0,
|
||||
SVG_LOCK_OPEN: 'open',
|
||||
SVG_LOCK_CLOSED: 'closed',
|
||||
});
|
||||
vm.runInContext(`
|
||||
function activeEditorForm(){return document.getElementById('note-editor-form')}
|
||||
function _formNoteId(form){return (form&&form.dataset&&form.dataset.noteId)||''}
|
||||
function _activeEditorNoteId(){return _formNoteId(activeEditorForm())}
|
||||
function getTA(){return document.getElementById('note-body')}
|
||||
function queryActiveEditor(sel){var f=activeEditorForm();return f&&f.querySelector?f.querySelector(sel):null}
|
||||
function touchVaultActivity(){}
|
||||
function _log(){}
|
||||
function applyEditorModeVisibility(mode){
|
||||
var host=queryActiveEditor('#cm-host');
|
||||
var ta=getTA();
|
||||
if(mode==='markdown'){if(host)host.style.display='';if(ta)ta.style.display='none'}
|
||||
}
|
||||
function mountMarkdownEditor(content){ _mounted.push(content); }
|
||||
function syncEditorModeButtons(){}
|
||||
function _reconcileSaveStateAfterModeSwitch(){}
|
||||
function snapshotHash(){ _savedHash = 1; }
|
||||
function _updateLockToggle(){}
|
||||
function _updateNoteLockIcon(){}
|
||||
function _refreshVaultIcon(){}
|
||||
function setEditorMode(mode){
|
||||
// Deliberately the OLD clobbering behaviour, so the test fails if
|
||||
// _completeUnlock still routes markdown unlock through this.
|
||||
var ta=getTA();
|
||||
if(mode==='markdown'&&ta) ta.value='CLOBBERED BY TINYMCE SYNC';
|
||||
_editorMode=mode;
|
||||
}
|
||||
function preferredEditorMode(){return _defaultNoteOpenMode==='markdown'?'markdown':'rich'}
|
||||
function tinymceToMarkdown(html){return String(html||'').replace(/<[^>]+>/g,'').trim()}
|
||||
`, ctx);
|
||||
vm.runInContext(extractFn('_completeUnlock'), ctx);
|
||||
return { ctx, dom, mounted };
|
||||
}
|
||||
|
||||
test('_completeUnlock with markdown preference keeps decrypted plaintext and mounts it', () => {
|
||||
const { ctx, dom, mounted } = makeUnlockSandbox({ openMode: 'markdown' });
|
||||
vm.runInContext(`_completeUnlock(${JSON.stringify(NOTE_ID)}, 'hello from the vault', ${JSON.stringify(VAULT_ID)})`, ctx);
|
||||
const ta = dom.window.document.getElementById('note-body');
|
||||
assert.equal(ta.value, 'hello from the vault', 'textarea must keep decrypted plaintext');
|
||||
assert.deepEqual(mounted, ['hello from the vault'], 'CM6 must be seeded from decrypted plaintext');
|
||||
assert.equal(ctx._editorMode, 'markdown');
|
||||
assert.equal(dom.window.document.getElementById('editor-locked').style.display, 'none', 'lock overlay must hide');
|
||||
assert.equal(dom.window.document.getElementById('editor-toolbar').style.display, '', 'toolbar must show');
|
||||
});
|
||||
|
||||
test('_completeUnlock with rendered preference still uses setEditorMode(rich)', () => {
|
||||
const { ctx, dom, mounted } = makeUnlockSandbox({ openMode: 'preview' });
|
||||
vm.runInContext(`_completeUnlock(${JSON.stringify(NOTE_ID)}, 'hello from the vault', ${JSON.stringify(VAULT_ID)})`, ctx);
|
||||
const ta = dom.window.document.getElementById('note-body');
|
||||
assert.equal(ta.value, 'hello from the vault', 'rich unlock must not clobber plaintext either');
|
||||
assert.equal(mounted.length, 0, 'markdown editor must not mount when preference is rendered');
|
||||
assert.equal(ctx._editorMode, 'rich');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue