Skip to content

Commit 71f4abe

Browse files
feat: programmatic text selection example
1 parent 07796cd commit 71f4abe

10 files changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"dirname": "programmatic-text-selection",
3+
"tags": [
4+
"editing",
5+
"viewing",
6+
"react"
7+
],
8+
"title": "Programmatic Text Selection"
9+
}
104 KB
Loading
3.86 MB
Binary file not shown.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>SuperDoc React Example</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.jsx"></script>
11+
</body>
12+
</html>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "react-superdoc-example",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite"
8+
},
9+
"dependencies": {
10+
"@harbour-enterprises/superdoc": "^0.11.13",
11+
"react": "^19.0.0",
12+
"react-dom": "^19.0.0"
13+
},
14+
"devDependencies": {
15+
"@vitejs/plugin-react": "^4.0.4",
16+
"vite": "^7.0.5",
17+
"prosemirror-state": "^1.4.3"
18+
}
19+
}
67.3 KB
Binary file not shown.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { useRef, useState } from 'react';
2+
import { TextSelection } from 'prosemirror-state';
3+
import DocumentEditor from './components/DocumentEditor';
4+
5+
function App() {
6+
const [documentFile, setDocumentFile] = useState(null);
7+
const [showLengthInput, setShowLengthInput] = useState(true);
8+
const selectionMethodRef = useRef('length');
9+
const selectionLengthRef = useRef(10);
10+
const fileInputRef = useRef(null);
11+
const editorRef = useRef(null);
12+
13+
const handleFileChange = (event) => {
14+
const file = event.target.files?.[0];
15+
if (file) {
16+
setDocumentFile(file);
17+
}
18+
};
19+
20+
const handleEditorReady = (editor) => {
21+
console.log('SuperDoc editor is ready', editor);
22+
editorRef.current = editor;
23+
};
24+
25+
const getCurrentSelection = () => {
26+
const { view } = editorRef.current.activeEditor;
27+
return view.state.selection;
28+
};
29+
30+
const getLengthBasedPositions = () => {
31+
const selection = getCurrentSelection();
32+
const { view } = editorRef.current.activeEditor;
33+
const currentPos = selection.from;
34+
const selectionLength = selectionLengthRef.current;
35+
const docLength = view.state.doc.content.size;
36+
37+
return {
38+
from: currentPos,
39+
to: Math.min(currentPos + selectionLength, docLength)
40+
};
41+
};
42+
43+
const getLineBasedPositions = () => {
44+
const selection = getCurrentSelection();
45+
const { $from } = selection;
46+
return {
47+
from: $from.start(),
48+
to: $from.end()
49+
};
50+
};
51+
52+
const applySelection = (from, to) => {
53+
const activeEditor = editorRef.current.activeEditor;
54+
const { view } = activeEditor;
55+
56+
const newSelection = TextSelection.create(view.state.doc, from, to);
57+
const tr = view.state.tr.setSelection(newSelection);
58+
const state = view.state.apply(tr);
59+
view.updateState(state);
60+
61+
activeEditor.commands.setUnderline();
62+
};
63+
64+
const handleSelection = (getPositions) => {
65+
const { from, to } = getPositions();
66+
applySelection(from, to);
67+
};
68+
69+
const handleLengthSelection = () => handleSelection(getLengthBasedPositions);
70+
const handleLineSelection = () => handleSelection(getLineBasedPositions);
71+
72+
const handleSelectionClick = () => {
73+
if (selectionMethodRef.current === 'length') {
74+
handleLengthSelection();
75+
} else {
76+
handleLineSelection();
77+
}
78+
};
79+
80+
return (
81+
<div className="app">
82+
<header>
83+
<h1>SuperDoc Example</h1>
84+
<button onClick={() => fileInputRef.current?.click()}>
85+
Load Document
86+
</button>
87+
<div className="selection-controls">
88+
<div className="selection-group">
89+
<label htmlFor="selectionMethod">Selection method:</label>
90+
<select
91+
id="selectionMethod"
92+
defaultValue={selectionMethodRef.current}
93+
onChange={(e) => {
94+
selectionMethodRef.current = e.target.value;
95+
setShowLengthInput(e.target.value === 'length');
96+
}}
97+
>
98+
<option value="length">By Length</option>
99+
<option value="line">By Line</option>
100+
</select>
101+
102+
{/* hide input when not needed */}
103+
{showLengthInput && (
104+
<>
105+
<label htmlFor="selectionLength">Characters:</label>
106+
<input
107+
id="selectionLength"
108+
type="number"
109+
defaultValue={selectionLengthRef.current}
110+
onChange={(e) => selectionLengthRef.current = Number(e.target.value)}
111+
min="1"
112+
max="1000"
113+
/>
114+
</>
115+
)}
116+
117+
<button onClick={handleSelectionClick}>
118+
Select and underline
119+
</button>
120+
</div>
121+
</div>
122+
<input
123+
type="file"
124+
ref={fileInputRef}
125+
accept=".docx, application/vnd.openxmlformats-officedocument.wordprocessingml.document"
126+
onChange={handleFileChange}
127+
style={{ display: 'none' }}
128+
/>
129+
</header>
130+
131+
<main>
132+
<DocumentEditor
133+
initialData={documentFile}
134+
onEditorReady={handleEditorReady}
135+
/>
136+
</main>
137+
138+
<style jsx>{`
139+
.app {
140+
height: 100vh;
141+
display: flex;
142+
flex-direction: column;
143+
}
144+
header {
145+
padding: 1rem;
146+
background: #f5f5f5;
147+
display: flex;
148+
align-items: center;
149+
gap: 1rem;
150+
}
151+
header button {
152+
padding: 0.5rem 1rem;
153+
background: #1355ff;
154+
color: white;
155+
border: none;
156+
border-radius: 4px;
157+
cursor: pointer;
158+
}
159+
header button:hover {
160+
background: #0044ff;
161+
}
162+
.selection-controls {
163+
display: flex;
164+
align-items: center;
165+
gap: 1rem;
166+
}
167+
.selection-group {
168+
display: flex;
169+
align-items: center;
170+
gap: 0.5rem;
171+
padding: 0.5rem;
172+
border: 1px solid #ddd;
173+
border-radius: 4px;
174+
background: #fafafa;
175+
}
176+
.selection-group > label:first-child {
177+
font-weight: bold;
178+
font-size: 0.9rem;
179+
color: #333;
180+
}
181+
.selection-controls label {
182+
font-size: 0.9rem;
183+
}
184+
.selection-controls input[type="number"] {
185+
width: 80px;
186+
padding: 0.3rem;
187+
border: 1px solid #ccc;
188+
border-radius: 4px;
189+
}
190+
.selection-controls select {
191+
padding: 0.3rem;
192+
border: 1px solid #ccc;
193+
border-radius: 4px;
194+
background: white;
195+
}
196+
main {
197+
flex: 1;
198+
min-height: 0;
199+
}
200+
`}</style>
201+
</div>
202+
);
203+
}
204+
205+
export default App;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { SuperDoc } from '@harbour-enterprises/superdoc';
2+
import '@harbour-enterprises/superdoc/style.css';
3+
import { useEffect, useRef } from 'react';
4+
5+
const DocumentEditor = ({
6+
initialData = null,
7+
readOnly = false,
8+
onEditorReady
9+
}) => {
10+
const editorRef = useRef(null);
11+
12+
useEffect(() => {
13+
const config = {
14+
selector: '#superdoc',
15+
toolbar: '#superdoc-toolbar',
16+
documentMode: readOnly ? 'viewing' : 'editing',
17+
pagination: true,
18+
rulers: true,
19+
onReady: () => {
20+
if (onEditorReady) {
21+
onEditorReady(editor);
22+
}
23+
},
24+
onEditorCreate: (event) => {
25+
console.log('Editor is created', event);
26+
},
27+
onEditorDestroy: () => {
28+
console.log('Editor is destroyed');
29+
}
30+
}
31+
32+
if (initialData) config.document = initialData;
33+
// config.document = './sample.docx'; // or use path to file
34+
35+
const editor = new SuperDoc(config);
36+
37+
editorRef.current = editor;
38+
39+
// Cleanup on unmount
40+
return () => {
41+
if (editorRef.current) {
42+
editorRef.current = null;
43+
}
44+
};
45+
}, [initialData, readOnly, onEditorReady]);
46+
47+
return (
48+
<div className="document-editor">
49+
<div id="superdoc-toolbar" className="toolbar" />
50+
<div id="superdoc" className="superdoc-container" />
51+
<style jsx>{`
52+
.document-editor {
53+
display: flex;
54+
flex-direction: column;
55+
height: 100%;
56+
width: 100%;
57+
}
58+
.toolbar {
59+
flex: 0 0 auto;
60+
border-bottom: 1px solid #eee;
61+
}
62+
.superdoc-container {
63+
display: flex;
64+
justify-content: center;
65+
flex: 1 1 auto;
66+
overflow: auto;
67+
}
68+
`}</style>
69+
</div>
70+
);
71+
};
72+
73+
export default DocumentEditor;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import App from './App';
4+
5+
ReactDOM.createRoot(document.getElementById('root')).render(
6+
<React.StrictMode>
7+
<App />
8+
</React.StrictMode>
9+
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'vite';
2+
import react from '@vitejs/plugin-react';
3+
4+
export default defineConfig({
5+
plugins: [react()],
6+
optimizeDeps: {
7+
include: ['@harbour-enterprises/superdoc']
8+
}
9+
});

0 commit comments

Comments
 (0)