Skip to content

Commit 8ef357c

Browse files
committed
feat: enforce read-only mode UI lockdown for shared documents
- Add body.editor-readonly CSS class to disable all editing buttons - Toggle class in showSharedBanner/hideSharedBanner (cloud-share.js) - Add capturing click interceptor with toast notification - Add JS safety-net guards to core text-mutation functions - Guard file import, drag-drop, image paste, keyboard shortcuts
1 parent 390d559 commit 8ef357c

File tree

5 files changed

+94
-0
lines changed

5 files changed

+94
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Read-Only Mode UI Lockdown
2+
3+
**Date:** 2026-03-18
4+
5+
## Summary
6+
7+
Enforces true read-only mode when viewing shared documents. Previously only the textarea was made read-only via the HTML `readOnly` attribute, but all toolbar buttons (formatting, tags, templates, etc.) remained fully functional and could modify content programmatically. Now all editing UI is visually disabled and functionally blocked.
8+
9+
## Changes
10+
11+
### styles.css
12+
- Added `body.editor-readonly` CSS rules targeting all editing buttons (`.fmt-btn`, tag buttons, QAB write controls, find/replace, dropzone)
13+
- Disabled buttons show `opacity: 0.35`, `cursor: not-allowed`, and `user-select: none` with `!important`
14+
15+
### js/cloud-share.js
16+
- `showSharedBanner()`: adds `editor-readonly` class to `<body>` alongside `readOnly = true`
17+
- `hideSharedBanner()`: removes `editor-readonly` class alongside `readOnly = false`
18+
- Added capturing click interceptor for disabled buttons — shows "🔒 Read-only mode — editing is disabled" toast
19+
20+
### js/editor-features.js
21+
- Added `if (M.markdownEditor.readOnly) return;` guard to 7 core functions:
22+
- `wrapSelection()`, `insertAtCursor()`, `insertLinePrefix()` (formatting)
23+
- `replaceOne()`, `replaceAll()` (find & replace)
24+
- Image paste handler
25+
- Keyboard shortcut handler (Ctrl+B, Ctrl+I, Ctrl+K)
26+
27+
### js/app-init.js
28+
- Added read-only guard to `handleDrop()` (drag & drop file import)
29+
- Added read-only guard to file input change handler
30+
31+
## What remains enabled in read-only mode
32+
- View mode switching, theme toggle, zen/focus mode
33+
- Export (MD, HTML, PDF, LLM Memory)
34+
- Copy markdown, scrolling, AI panel browsing

js/app-init.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
});
138138

139139
function handleDrop(e) {
140+
if (M.markdownEditor.readOnly) return;
140141
var dt = e.dataTransfer;
141142
var files = dt.files;
142143
if (files.length) {
@@ -492,6 +493,7 @@
492493
// FILE INPUT & EXPORT
493494
// ========================================
494495
M.fileInput.addEventListener('change', function (e) {
496+
if (M.markdownEditor.readOnly) return;
495497
if (e.target.files.length) M.importFile(e.target.files[0]);
496498
});
497499
M.importButton.addEventListener('click', function () {

js/cloud-share.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
// --- View Lock for shared links (ppt / preview) ---
1313
M.sharedViewLock = null; // null = no lock, 'ppt' | 'preview' = locked
14+
var readonlyClickHandlerInstalled = false;
1415

1516
// --- Firebase Config ---
1617
var firebaseConfig = {
@@ -433,6 +434,7 @@
433434
document.body.classList.add('shared-view-active');
434435
document.body.classList.remove('banner-collapsed');
435436
M.markdownEditor.readOnly = true;
437+
document.body.classList.add('editor-readonly');
436438

437439
// If view-locked, disable all view mode buttons that don't match
438440
if (M.sharedViewLock) {
@@ -444,6 +446,26 @@
444446
bannerAutoHideTimer = setTimeout(function () {
445447
collapseBannerToPill();
446448
}, 4000);
449+
450+
// Intercept clicks on disabled editing buttons and show a toast
451+
if (!readonlyClickHandlerInstalled) {
452+
readonlyClickHandlerInstalled = true;
453+
document.addEventListener('click', function (e) {
454+
if (!M.markdownEditor.readOnly) return;
455+
var target = e.target.closest(
456+
'.fmt-btn, #new-document-btn, #template-btn, #share-button, ' +
457+
'#mobile-share-button, #speech-to-text-btn, #run-all-btn, ' +
458+
'#qab-new, #qab-import, #qab-template, #qab-share, #qab-voice, ' +
459+
'#qab-copy, .ai-action-chip, .ai-ctx-btn, ' +
460+
'#replace-one, #replace-all, #qab-replace-one, #qab-replace-all'
461+
);
462+
if (target) {
463+
e.preventDefault();
464+
e.stopImmediatePropagation();
465+
if (M.showToast) M.showToast('🔒 Read-only mode — editing is disabled', 'warning');
466+
}
467+
}, true); // capturing phase
468+
}
447469
}
448470

449471
/**
@@ -514,6 +536,7 @@
514536
pill.style.display = 'none';
515537
document.body.classList.remove('shared-view-active', 'banner-collapsed');
516538
M.markdownEditor.readOnly = false;
539+
document.body.classList.remove('editor-readonly');
517540
clearTimeout(bannerAutoHideTimer);
518541
clearTimeout(bannerReShowTimer);
519542
}

js/editor-features.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
// IMAGE PASTE FROM CLIPBOARD
162162
// ========================================
163163
M.markdownEditor.addEventListener('paste', function (e) {
164+
if (M.markdownEditor.readOnly) return;
164165
var items = e.clipboardData && e.clipboardData.items;
165166
if (!items) return;
166167
for (var i = 0; i < items.length; i++) {
@@ -182,6 +183,7 @@
182183
// FORMATTING TOOLBAR HELPERS
183184
// ========================================
184185
function wrapSelection(before, after, placeholder) {
186+
if (M.markdownEditor.readOnly) return;
185187
var start = M.markdownEditor.selectionStart;
186188
var end = M.markdownEditor.selectionEnd;
187189
var text = M.markdownEditor.value;
@@ -200,6 +202,7 @@
200202
}
201203

202204
function insertAtCursor(text) {
205+
if (M.markdownEditor.readOnly) return;
203206
var start = M.markdownEditor.selectionStart;
204207
var end = M.markdownEditor.selectionEnd;
205208
var value = M.markdownEditor.value;
@@ -211,6 +214,7 @@
211214
M.insertAtCursor = insertAtCursor;
212215

213216
function insertLinePrefix(prefix) {
217+
if (M.markdownEditor.readOnly) return;
214218
var start = M.markdownEditor.selectionStart;
215219
var end = M.markdownEditor.selectionEnd;
216220
var text = M.markdownEditor.value;
@@ -395,6 +399,7 @@
395399
// --- Keyboard Shortcuts for Formatting ---
396400
M.markdownEditor.addEventListener('keydown', function (e) {
397401
if (!(e.ctrlKey || e.metaKey)) return;
402+
if (M.markdownEditor.readOnly) return;
398403
if (e.key === 'z' || e.key === 'Z') {
399404
e.preventDefault();
400405
if (e.shiftKey) performRedo(); else performUndo();
@@ -526,6 +531,7 @@
526531
function findPrev() { if (findMatches.length === 0) return; selectMatch((findCurrentIndex - 1 + findMatches.length) % findMatches.length); }
527532

528533
function replaceOne() {
534+
if (M.markdownEditor.readOnly) return;
529535
if (findCurrentIndex < 0 || findCurrentIndex >= findMatches.length) return;
530536
var els = getActiveFindEls();
531537
var match = findMatches[findCurrentIndex];
@@ -538,6 +544,7 @@
538544
function escapeRegExpChars(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
539545

540546
function replaceAll() {
547+
if (M.markdownEditor.readOnly) return;
541548
var els = getActiveFindEls();
542549
var query = els.findInput ? els.findInput.value : '';
543550
var replacement = els.replaceInput ? els.replaceInput.value : '';

styles.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6406,4 +6406,32 @@ body.focus-mode #markdown-editor {
64066406
width: auto;
64076407
max-height: 60vh;
64086408
}
6409+
}
6410+
6411+
/* ========================================
6412+
READ-ONLY MODE — disable editing UI
6413+
Applied when viewing a shared document.
6414+
Only scrolling and read-safe actions remain.
6415+
======================================== */
6416+
body.editor-readonly .fmt-btn,
6417+
body.editor-readonly #new-document-btn,
6418+
body.editor-readonly #template-btn,
6419+
body.editor-readonly #share-button,
6420+
body.editor-readonly #mobile-share-button,
6421+
body.editor-readonly #speech-to-text-btn,
6422+
body.editor-readonly #qab-new,
6423+
body.editor-readonly #qab-import,
6424+
body.editor-readonly #qab-template,
6425+
body.editor-readonly #qab-share,
6426+
body.editor-readonly #qab-voice,
6427+
body.editor-readonly .ai-action-chip,
6428+
body.editor-readonly .ai-ctx-btn,
6429+
body.editor-readonly #replace-one,
6430+
body.editor-readonly #replace-all,
6431+
body.editor-readonly #qab-replace-one,
6432+
body.editor-readonly #qab-replace-all,
6433+
body.editor-readonly .dropzone-wrapper {
6434+
opacity: 0.35 !important;
6435+
cursor: not-allowed !important;
6436+
user-select: none !important;
64096437
}

0 commit comments

Comments
 (0)