Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/super-editor/src/assets/styles/elements/prosemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@
caret-color: transparent;
}

/* Allow selection visibility in viewing mode when allowSelectionInViewMode is enabled */
.presentation-editor--allow-selection .ProseMirror-hideselection *::selection {
background: Highlight;
background: -moz-Highlight;
}

.presentation-editor--allow-selection .ProseMirror-hideselection *::-moz-selection {
background: Highlight;
}

.presentation-editor--allow-selection .ProseMirror-hideselection * {
caret-color: auto;
}

/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
.ProseMirror [draggable][contenteditable='false'] {
user-select: text;
Expand Down
38 changes: 29 additions & 9 deletions packages/super-editor/src/core/extensions/editable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import { Extension } from '../Extension.js';
*
* When editable is false, all user interactions are blocked:
* - Text input via beforeinput events
* - Mouse interactions via mousedown
* - Focus via automatic blur
* - Click, double-click, and triple-click events
* - Mouse interactions via mousedown (unless allowSelectionInViewMode is true)
* - Focus via automatic blur (unless allowSelectionInViewMode is true)
* - Click, double-click, and triple-click events (unless allowSelectionInViewMode is true)
* - Keyboard shortcuts via handleKeyDown
* - Paste and drop events
*
* When allowSelectionInViewMode is true and editable is false:
* - Mouse interactions are allowed for text selection
* - Focus is allowed
* - Click events are allowed for selection
* - But text input, keyboard shortcuts, paste, and drop remain blocked
*/
export const Editable = Extension.create({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no tests for the new flag (editable.js didn't have tests previously) - which makes it even more dangerous not knowing if this change could impact something

test add at least a unit test for the Cmd+C allowlist to prevent regressions

name: 'editable',
Expand All @@ -30,25 +36,39 @@ export const Editable = Extension.create({
return false;
},
mousedown: (_view, event) => {
if (!editor.options.editable) {
// Allow mousedown for selection when allowSelectionInViewMode is enabled
if (!editor.options.editable && !editor.options.allowSelectionInViewMode) {
event.preventDefault();
return true;
}
return false;
},
focus: (view, event) => {
if (!editor.options.editable) {
// Allow focus when allowSelectionInViewMode is enabled
if (!editor.options.editable && !editor.options.allowSelectionInViewMode) {
event.preventDefault();
view.dom.blur();
return true;
}
return false;
},
},
handleClick: () => !editor.options.editable,
handleDoubleClick: () => !editor.options.editable,
handleTripleClick: () => !editor.options.editable,
handleKeyDown: () => !editor.options.editable,
// Allow click events for selection when allowSelectionInViewMode is enabled
handleClick: () => !editor.options.editable && !editor.options.allowSelectionInViewMode,
handleDoubleClick: () => !editor.options.editable && !editor.options.allowSelectionInViewMode,
handleTripleClick: () => !editor.options.editable && !editor.options.allowSelectionInViewMode,
// Always block keyboard input, paste, and drop when not editable
handleKeyDown: (_view, event) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only allows Cmd+C

keyboard users or accessibility can't select (Shift+Arrow to extend, Cmd+A to select all) and/or navigate (arrows, Home/End).

Suggested change
handleKeyDown: (_view, event) => {
handleKeyDown: (_view, event) => {
if (!editor.options.editable) {
if (editor.options.allowSelectionInViewMode) {
// Allow navigation keys for selection
const isNavigationKey = [
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'Home', 'End', 'PageUp', 'PageDown'
].includes(event.key);
// Allow copy and select all
const isCopyOrSelectAll = (event.ctrlKey || event.metaKey) &&
['c', 'a'].includes(event.key.toLowerCase());
if (isNavigationKey || isCopyOrSelectAll) return false;
}
return true;
}
return false;
},

if (!editor.options.editable) {
// Allow Ctrl+C / Cmd+C for copy when allowSelectionInViewMode is enabled
if (editor.options.allowSelectionInViewMode) {
const isCopy = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c';
if (isCopy) return false;
}
return true;
}
return false;
},
handlePaste: () => !editor.options.editable,
handleDrop: () => !editor.options.editable,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,10 @@ export class PresentationEditor extends EventEmitter {
#syncDocumentModeClass() {
if (!this.#visibleHost) return;
this.#visibleHost.classList.toggle('presentation-editor--viewing', this.#documentMode === 'viewing');
this.#visibleHost.classList.toggle(
'presentation-editor--allow-selection',
this.#documentMode === 'viewing' && !!this.#options.allowSelectionInViewMode,
);
}

/**
Expand Down Expand Up @@ -4611,12 +4615,16 @@ export class PresentationEditor extends EventEmitter {
* Determines whether the current viewing mode should block edits.
* When documentMode is viewing but the active editor has been toggled
* back to editable (e.g. permission ranges), we treat the view as editable.
* Also returns false when allowSelectionInViewMode is enabled, allowing
* text selection while still blocking actual edits.
*/
#isViewLocked(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: #isViewLocked() returning false while in viewing mode is a bit confusing - the name implies "is the view locked?" but it now means "should we hide selection?".

not worth changing now, but if this method grows more conditions might be worth splitting into #isInViewingMode() and #shouldBlockSelectionVisuals()

if (this.#documentMode !== 'viewing') return false;
const hasPermissionOverride = !!(this.#editor as Editor & { storage?: Record<string, any> })?.storage
?.permissionRanges?.hasAllowedRanges;
if (hasPermissionOverride) return false;
// Allow selection visuals when allowSelectionInViewMode is enabled
if (this.#options.allowSelectionInViewMode) return false;
Comment on lines 4625 to +4627

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep view locked for input bridge in selection-only mode

Returning false here when allowSelectionInViewMode is set makes the rest of the presentation layer treat viewing mode as editable. In particular, #setupInputBridge() uses !this.#isViewLocked() to decide whether to forward key/composition/beforeinput events to the hidden editor, so selection-only mode now forwards IME/composition input and keyboard shortcuts even though Editor.setEditable(false) still marks the document read-only. The Editable extension only blocks beforeinput/keydown, not composition events, so IME input can still mutate the document in viewing mode. Consider keeping #isViewLocked() true for the input bridge (or adding a separate “selection-only” check) so selection visuals are allowed without re-enabling input forwarding.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#isViewLocked() returning false now also affects HeaderFooterSessionManager's #validateEditPermission() this could allow entering header/footer edit mode even though typing is blocked.

worth verifying that doesn't cause weird UI states, or adding a separate documentMode check there?

return this.#documentMode === 'viewing';
}

Expand Down
7 changes: 7 additions & 0 deletions packages/super-editor/src/core/presentation-editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export type PresentationEditorOptions = ConstructorParameters<typeof Editor>[0]
* @default false
*/
disableContextMenu?: boolean;
/**
* Allow text selection in viewing mode.
* When true, users can select and copy text while in viewing mode,
* but editing (typing, paste, delete) remains blocked.
* @default false
*/
allowSelectionInViewMode?: boolean;
};

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/super-editor/src/core/types/EditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ export interface EditorOptions {
/** Whether the editor is editable */
editable?: boolean;

/**
* Allow text selection in viewing mode.
* When true, users can select and copy text while in viewing mode,
* but editing (typing, paste, delete) remains blocked.
* @default false
*/
allowSelectionInViewMode?: boolean;

/** Editor properties */
editorProps?: Record<string, unknown>;

Expand Down
8 changes: 8 additions & 0 deletions packages/super-editor/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ export interface OpenOptions {
isCommentsEnabled?: boolean;
suppressDefaultDocxStyles?: boolean;
documentMode?: 'editing' | 'viewing' | 'suggesting';
/**
* Allow text selection in viewing mode.
* When true, users can select and copy text while in viewing mode,
* but editing (typing, paste, delete) remains blocked.
* @default false
*/
allowSelectionInViewMode?: boolean;
content?: unknown;
mediaFiles?: Record<string, unknown>;
fonts?: Record<string, unknown>;
Expand All @@ -166,6 +173,7 @@ export declare class Editor {
content?: string | object;
extensions?: any[];
editable?: boolean;
allowSelectionInViewMode?: boolean;
autofocus?: boolean | 'start' | 'end' | 'all' | number;
[key: string]: any;
});
Expand Down
8 changes: 5 additions & 3 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const { isHighContrastMode } = useHighContrastMode();
const { uiFontFamily } = useUiFontFamily();

const isViewingMode = () => proxy?.$superdoc?.config?.documentMode === 'viewing';
const allowSelectionInViewMode = () => !!proxy?.$superdoc?.config?.allowSelectionInViewMode;
const isViewingCommentsVisible = computed(
() => isViewingMode() && proxy?.$superdoc?.config?.comments?.visible === true,
);
Expand Down Expand Up @@ -283,13 +284,13 @@ const onEditorSelectionChange = ({ editor, transaction }) => {
// When comment is added selection will be equal to comment text
// Should skip calculations to keep text selection for comments correct
skipSelectionUpdate.value = false;
if (isViewingMode()) {
if (isViewingMode() && !allowSelectionInViewMode()) {
resetSelection();
}
return;
}

if (isViewingMode()) {
if (isViewingMode() && !allowSelectionInViewMode()) {
resetSelection();
return;
}
Expand Down Expand Up @@ -463,6 +464,7 @@ const editorOptions = (doc) => {
html: doc.html,
markdown: doc.markdown,
documentMode: proxy.$superdoc.config.documentMode,
allowSelectionInViewMode: proxy.$superdoc.config.allowSelectionInViewMode,
rulers: doc.rulers,
rulerContainer: proxy.$superdoc.config.rulerContainer,
isInternal: proxy.$superdoc.config.isInternal,
Expand Down Expand Up @@ -688,7 +690,7 @@ const getSelectionPosition = computed(() => {
});

const handleSelectionChange = (selection) => {
if (isViewingMode()) {
if (isViewingMode() && !allowSelectionInViewMode()) {
resetSelection();
return;
}
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/core/SuperDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class SuperDoc extends EventEmitter {
superdocId: null,
selector: '#superdoc',
documentMode: 'editing',
allowSelectionInViewMode: false,
role: 'editor',
document: {},
documents: [],
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/dev/components/SuperdocDev.vue
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const init = async () => {
// format: 'docx',
// html: '<p>Hello world</p>',
// isDev: true,
// allowSelectionInViewMode: true,
user,
title: 'Test document',
users: [
Expand Down
5 changes: 1 addition & 4 deletions packages/superdoc/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"customConditions": []
}
"extends": "./tsconfig.json"
}
Loading