diff --git a/.github/workflows/run-tests-example-apps.yml b/.github/workflows/run-tests-example-apps.yml new file mode 100644 index 0000000000..7c324f2cca --- /dev/null +++ b/.github/workflows/run-tests-example-apps.yml @@ -0,0 +1,30 @@ +name: Run tests for example apps + +on: + workflow_call: + # TODO: Enable this when we test this workflow manually + # pull_request: + # branches: + # - develop + +jobs: + run-tests-example-apps: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: cd examples/tests && npm ci + + - name: Install Playwright browsers + run: cd examples/tests && npx playwright install --with-deps + + - name: Run tests + run: cd examples/tests && npm test \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 08ec9b4ec3..277caee046 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -91,7 +91,7 @@ export default [ } }, rules: { - 'no-unused-vars': 'warn', // See warnings but don't block + 'no-unused-vars': ['warn', { "varsIgnorePattern": "^_" }], // See warnings but don't block // Relax these rules - they're more style than bugs 'no-empty': ['warn', { allowEmptyCatch: true }], // Allow empty catch blocks diff --git a/examples/loading-from-json/demo-config.json b/examples/loading-from-json/demo-config.json new file mode 100644 index 0000000000..485e31e29c --- /dev/null +++ b/examples/loading-from-json/demo-config.json @@ -0,0 +1,9 @@ +{ + "dirname": "loading-from-json", + "tags": [ + "editing", + "viewing", + "vanilla-js" + ], + "title": "Loading from JSON" +} \ No newline at end of file diff --git a/examples/loading-from-json/demo-thumbnail.png b/examples/loading-from-json/demo-thumbnail.png new file mode 100644 index 0000000000..6a8b0fe52a Binary files /dev/null and b/examples/loading-from-json/demo-thumbnail.png differ diff --git a/examples/loading-from-json/demo-video.mp4 b/examples/loading-from-json/demo-video.mp4 new file mode 100644 index 0000000000..9886a67205 Binary files /dev/null and b/examples/loading-from-json/demo-video.mp4 differ diff --git a/examples/loading-from-json/index.html b/examples/loading-from-json/index.html new file mode 100644 index 0000000000..bd0ef18e17 --- /dev/null +++ b/examples/loading-from-json/index.html @@ -0,0 +1,28 @@ + + + + + + SuperDoc Vanilla Example + + +
+
+

SuperDoc Example

+ + + +
+
+
+
+
+
+ + + diff --git a/examples/loading-from-json/package.json b/examples/loading-from-json/package.json new file mode 100644 index 0000000000..e1de37387f --- /dev/null +++ b/examples/loading-from-json/package.json @@ -0,0 +1,15 @@ +{ + "name": "vanilla-superdoc-example", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@harbour-enterprises/superdoc": "^0.15.16" + }, + "devDependencies": { + "vite": "^4.4.6" + } +} diff --git a/examples/loading-from-json/src/main.js b/examples/loading-from-json/src/main.js new file mode 100644 index 0000000000..3112cad3df --- /dev/null +++ b/examples/loading-from-json/src/main.js @@ -0,0 +1,100 @@ +// Imports +import { SuperDoc } from '@harbour-enterprises/superdoc'; +import '@harbour-enterprises/superdoc/style.css'; +import './style.css'; + +// Init +let editor = null; + +//Init SuperDoc from DOCX file +function initSuperDocFromFile(file = null) { + if (editor) { + editor = null; + } + + editor = new SuperDoc({ + selector: '#superdoc', + toolbar: '#superdoc-toolbar', + document: file, // DOCX URL, File object, or document config + documentMode: 'editing', + mode: 'docx', + pagination: true, + rulers: true, + onReady: (event) => { + const docJSON = event.superdoc.activeEditor.getJSON(); + console.log('SuperDoc ready - JSON', docJSON); + }, + onEditorUpdate: (event) => { + const docJSON = event.editor.getJSON(); + console.log('SuperDoc updated - JSON', docJSON); + }, + }); +} + +//Init SuperDoc from JSON +function initSuperDocFromJSON() { + if (editor) { + editor = null; + } + + //Hardcoded demo JSON + const demoJSON = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello, SuperDoc~!!', + }, + ], + }, + ], + }; + + editor = new SuperDoc({ + selector: '#superdoc', + toolbar: '#superdoc-toolbar', + documentMode: 'editing', + + /* LOADING JSON */ + //https://docs.superdoc.dev/core/supereditor/configuration#param-options-content + mode: 'docx', + jsonOverride: demoJSON, + + pagination: true, + rulers: true, + onReady: (event) => { + const docJSON = event.superdoc.activeEditor.getJSON(); + console.log('SuperDoc ready - JSON', docJSON); + }, + onEditorUpdate: (event) => { + const docJSON = event.editor.getJSON(); + console.log('SuperDoc updated - JSON', docJSON); + }, + }); +} + +// Setup file input handling +const fileInput = document.getElementById('fileInput'); +const loadButton = document.getElementById('loadButton'); +const loadJSON = document.getElementById('loadJSON'); + +loadButton.addEventListener('click', () => { + fileInput.click(); +}); + +loadJSON.addEventListener('click', () => { + initSuperDocFromJSON(); +}); + +fileInput.addEventListener('change', (event) => { + const file = event.target.files?.[0]; + if (file) { + initSuperDocFromFile(file); + } +}); + +// Initialize empty editor on page load +initSuperDocFromFile(null); diff --git a/examples/loading-from-json/src/style.css b/examples/loading-from-json/src/style.css new file mode 100644 index 0000000000..09052dcc1d --- /dev/null +++ b/examples/loading-from-json/src/style.css @@ -0,0 +1,45 @@ +.container { + height: 100vh; + display: flex; + flex-direction: column; + } + + header { + padding: 1rem; + background: #f5f5f5; + display: flex; + align-items: center; + gap: 1rem; + } + + button { + padding: 0.5rem 1rem; + background: #1355ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + button:hover { + background: #0044ff; + } + + main { + flex: 1; + padding: 1rem; + display: flex; + flex-direction: column; + } + + #superdoc-toolbar { + border-bottom: 1px solid #eee; + } + + #superdoc { + display: flex; + justify-content: center; + flex: 1; + overflow: auto; + } + \ No newline at end of file diff --git a/examples/loading-from-json/vite.config.js b/examples/loading-from-json/vite.config.js new file mode 100644 index 0000000000..e1edef2913 --- /dev/null +++ b/examples/loading-from-json/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + optimizeDeps: { + include: ['@harbour-enterprises/superdoc'] + } +}); \ No newline at end of file diff --git a/examples/react-example/package-lock.json b/examples/react-example/package-lock.json index 66d621e35b..e7686cbeb0 100644 --- a/examples/react-example/package-lock.json +++ b/examples/react-example/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.0.4", - "vite": "^7.0.5" + "vite": "^6.2.0" } }, "node_modules/@ampproject/remapping": { @@ -3899,24 +3899,24 @@ } }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.6", + "fdir": "^6.4.4", "picomatch": "^4.0.2", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3925,14 +3925,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" diff --git a/examples/react-example/package.json b/examples/react-example/package.json index 18365e9c66..a09eb9343d 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -13,6 +13,6 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.0.4", - "vite": "^7.0.5" + "vite": "^6.2.0" } } diff --git a/examples/replace-content-example/README.md b/examples/replace-content-example/README.md new file mode 100644 index 0000000000..363c66360c --- /dev/null +++ b/examples/replace-content-example/README.md @@ -0,0 +1,26 @@ +# Replace Content Example + +A React example demonstrating how to replace document content with HTML or JSON using SuperDoc. + +## Features + +- Load DOCX documents +- Replace entire document or selection with custom content +- Switch between HTML and JSON input formats +- Side panel with content replacement controls + +## Usage + +1. Load a document using "Load Document" button +2. Open the side panel using the tab on the right +3. Choose replacement scope (Document or Selection) +4. Select content type (HTML or JSON) +5. Enter your content in the textarea +6. Click "Replace content" to apply changes + +## Running + +```bash +npm install +npm run dev +``` \ No newline at end of file diff --git a/examples/replace-content-example/demo-config.json b/examples/replace-content-example/demo-config.json new file mode 100644 index 0000000000..b2193840f3 --- /dev/null +++ b/examples/replace-content-example/demo-config.json @@ -0,0 +1,9 @@ +{ + "dirname": "replace-content-example", + "tags": [ + "editing", + "viewing", + "react" + ], + "title": "Replacing content with HTML/JSON" +} \ No newline at end of file diff --git a/examples/replace-content-example/demo-thumbnail.png b/examples/replace-content-example/demo-thumbnail.png new file mode 100644 index 0000000000..8425d2ff17 Binary files /dev/null and b/examples/replace-content-example/demo-thumbnail.png differ diff --git a/examples/replace-content-example/demo-video.mp4 b/examples/replace-content-example/demo-video.mp4 new file mode 100644 index 0000000000..4d7b4c9819 Binary files /dev/null and b/examples/replace-content-example/demo-video.mp4 differ diff --git a/examples/replace-content-example/index.html b/examples/replace-content-example/index.html new file mode 100644 index 0000000000..5a4e3e6da2 --- /dev/null +++ b/examples/replace-content-example/index.html @@ -0,0 +1,12 @@ + + + + + + SuperDoc React Example + + +
+ + + \ No newline at end of file diff --git a/examples/replace-content-example/package.json b/examples/replace-content-example/package.json new file mode 100644 index 0000000000..96123482ba --- /dev/null +++ b/examples/replace-content-example/package.json @@ -0,0 +1,19 @@ +{ + "name": "react-superdoc-example", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@harbour-enterprises/superdoc": "^0.14.15", + "highlight.js": "^11.11.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.4", + "vite": "^7.0.5" + } +} diff --git a/examples/replace-content-example/public/sample.docx b/examples/replace-content-example/public/sample.docx new file mode 100644 index 0000000000..c89890fb5d Binary files /dev/null and b/examples/replace-content-example/public/sample.docx differ diff --git a/examples/replace-content-example/src/App.jsx b/examples/replace-content-example/src/App.jsx new file mode 100644 index 0000000000..fcf4e8c6a8 --- /dev/null +++ b/examples/replace-content-example/src/App.jsx @@ -0,0 +1,306 @@ +import { useRef, useReducer } from 'react'; +import DocumentEditor from './components/DocumentEditor'; + +function App() { + const [, _forceUpdate] = useReducer(x => x + 1, 0); + + const forceUpdate = async (uploadingFile = false) => { + // Save editor content to documentFileRef before forcing update + if (editorRef.current && editorRef.current.activeEditor && !uploadingFile) { + try { + const result = await editorRef.current.activeEditor.exportDocx(); + const DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + const blob = new Blob([result], { type: DOCX }); + const file = new File([blob], `document-${Date.now()}.docx`, { type: DOCX }); + documentFileRef.current = file; + console.log('Saved editor content as DOCX file to documentFileRef:', file); + } catch (error) { + console.warn('Could not save editor content:', error); + } + } + _forceUpdate(); + }; + +const exampleJSON = { + type: 'text', + marks: [ + { + type: 'aiAnimationMark', + attrs: { + class: 'sd-ai-text-appear', + dataMarkId: `ai-animation-${Date.now()}`, + }, + }, + ], + text: 'Hello, SuperDoc~!!', +}; + + const exampleHTML = '

Hello, SuperDoc~!!

'; + + const documentFileRef = useRef(null); + const drawerOpenRef = useRef(true); + const replacementScopeRef = useRef('document'); + const replacementContentTypeRef = useRef('html'); + const textareaRef = useRef(null); + + const editorRef = useRef(null); + const fileInputRef = useRef(null); + + const handleFileChange = (event) => { + const file = event.target.files?.[0]; + if (file) { + documentFileRef.current = file; + forceUpdate(true); + } + }; + + const handleEditorReady = (editorInstance) => { + console.log('SuperDoc editor is ready', editorInstance); + editorRef.current = editorInstance; + }; + + const handleReplacementScopeChange = (event) => { + replacementScopeRef.current = event.target.value; + forceUpdate(); + }; + + const handleReplacementContentTypeChange = (event) => { + replacementContentTypeRef.current = event.target.value; + // Update textarea content without triggering re-render + if (textareaRef.current) { + textareaRef.current.value = replacementContentTypeRef.current === 'json' ? JSON.stringify(exampleJSON, null, 2) : exampleHTML; + } + forceUpdate(); + }; + + const toggleDrawer = () => { + drawerOpenRef.current = !drawerOpenRef.current; + forceUpdate(); + }; + + const handleReplaceContent = () => { + // Get textarea value directly from DOM + const textareaContent = textareaRef.current ? textareaRef.current.value : ''; + + if (!editorRef.current) { + console.error('Editor not available'); + return; + } + + if (replacementScopeRef.current === 'document') { + // Select all content in the document + editorRef.current.activeEditor.commands.selectAll(); + } + + const replacementContent = replacementContentTypeRef.current === "json" ? JSON.parse(textareaContent) : textareaContent; + + // Insert the raw content with animation mark + editorRef.current.activeEditor.commands.insertContent(replacementContent); + }; + + return ( +
+
+

SuperDoc Example

+ + +
+ +
+ + +
+ {drawerOpenRef.current ? '◀' : '▶'} +
+ +
+
+

Content Replacement

+ +
+ + +
+ +
+ + +
+ +
+ +