gridjs-spreadsheet 26.6.1 → 26.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/{a08275ef8caf83cc1c2eae91f17663e8.svg → 1f345d060b26fa53bc9d78c36441afd6.svg} +106 -26
  2. package/angular.d.ts +67 -0
  3. package/angular.js +174 -0
  4. package/example/README.md +242 -41
  5. package/example/angular-gridjs/.nvmrc +1 -0
  6. package/example/angular-gridjs/angular.json +66 -0
  7. package/example/angular-gridjs/package.json +31 -0
  8. package/example/angular-gridjs/proxy.conf.json +12 -0
  9. package/example/angular-gridjs/src/app/api.ts +7 -0
  10. package/example/angular-gridjs/src/app/app.component.html +57 -0
  11. package/example/angular-gridjs/src/app/app.component.ts +153 -0
  12. package/example/angular-gridjs/src/app/routing.ts +52 -0
  13. package/example/angular-gridjs/src/favicon.ico +0 -0
  14. package/example/angular-gridjs/src/index.html +12 -0
  15. package/example/angular-gridjs/src/main.ts +7 -0
  16. package/example/angular-gridjs/src/styles.css +37 -0
  17. package/example/angular-gridjs/tsconfig.app.json +9 -0
  18. package/example/angular-gridjs/tsconfig.json +29 -0
  19. package/example/pom.xml +3 -3
  20. package/example/react-gridjs/index.html +12 -0
  21. package/example/react-gridjs/package.json +20 -0
  22. package/example/react-gridjs/src/App.jsx +188 -0
  23. package/example/react-gridjs/src/api.js +7 -0
  24. package/example/react-gridjs/src/main.jsx +9 -0
  25. package/example/react-gridjs/src/routing.js +51 -0
  26. package/example/react-gridjs/src/styles.css +37 -0
  27. package/example/react-gridjs/vite.config.js +16 -0
  28. package/example/src/main/java/com/aspose/gridjs/demo/controller/DataController.java +80 -2
  29. package/example/src/main/java/com/aspose/gridjs/demo/controller/GridJs2Controller.java +12 -0
  30. package/example/vanilla-gridjs/npm/index.html +29 -0
  31. package/example/vanilla-gridjs/npm/main.js +8 -0
  32. package/example/vanilla-gridjs/npm/package.json +16 -0
  33. package/example/vanilla-gridjs/npm/vite.config.js +11 -0
  34. package/example/vanilla-gridjs/script/index.html +32 -0
  35. package/example/vanilla-gridjs/script/main.js +9 -0
  36. package/example/vanilla-gridjs/script/package.json +12 -0
  37. package/example/vanilla-gridjs/script/vite.config.js +11 -0
  38. package/example/vanilla-gridjs/shared/demo-app.js +205 -0
  39. package/example/vanilla-gridjs/shared/styles.css +36 -0
  40. package/example/vue-gridjs/index.html +12 -0
  41. package/example/vue-gridjs/package.json +19 -0
  42. package/example/vue-gridjs/src/App.vue +199 -0
  43. package/example/vue-gridjs/src/api.js +7 -0
  44. package/example/vue-gridjs/src/main.js +9 -0
  45. package/example/vue-gridjs/src/routing.js +51 -0
  46. package/example/vue-gridjs/src/styles.css +37 -0
  47. package/example/vue-gridjs/vite.config.js +21 -0
  48. package/index.d.ts +528 -472
  49. package/index.js +1 -1
  50. package/package.json +143 -50
  51. package/react.d.ts +27 -0
  52. package/react.js +111 -0
  53. package/readme.md +183 -380
  54. package/shared.d.ts +66 -0
  55. package/shared.js +204 -0
  56. package/vue.d.ts +20 -0
  57. package/vue.js +125 -0
  58. package/xspreadsheet.css +365 -1
  59. package/xspreadsheet.js +5 -5
  60. package/example/.mvn/wrapper/maven-wrapper.properties +0 -18
  61. package/example/src/test/java/com/aspose/gridjs/demo/GridjsdemoApplicationTests.java +0 -1441
@@ -0,0 +1,52 @@
1
+ // Route (URL query string) helpers shared by the demo.
2
+ //
3
+ // The demo encodes which workbook is open directly in the URL so a page
4
+ // refresh or a shared link reopens the same file, e.g.:
5
+ // /?file=Sample.xlsx&demo=permanent&uid=uid-Sample-xlsx
6
+ // /?file=Sample.xlsx&demo=highlight&fromUpload=1
7
+ //
8
+ // demo:
9
+ // 'permanent' - loads the workbook through the uid-based streaming endpoint;
10
+ // the backend caches the parsed result under this uid.
11
+ // 'highlight' - loads the workbook through the plain streaming endpoint,
12
+ // used by the "highlight and custom context menu" demo.
13
+ export type DemoType = 'permanent' | 'highlight';
14
+ export type WorkbookRef = { file: string; storedFile: string; uid: string; fromUpload: string; demo: DemoType };
15
+
16
+ export const DEMO_PERMANENT: DemoType = 'permanent';
17
+ export const DEMO_HIGHLIGHT: DemoType = 'highlight';
18
+ const CLIENT_PREFIX = 'angular';
19
+
20
+ // Turns a file name into a stable cache key,
21
+ // e.g. "Sales Report.xlsx" -> "uid-Sales-Report-xlsx".
22
+ export function makeUid(file: string): string {
23
+ return CLIENT_PREFIX + '-uid-' + file.replace(/[^a-zA-Z0-9]/g, '-');
24
+ }
25
+
26
+ // Reads the currently selected workbook (if any) from the URL query string.
27
+ // Returns null when no `file` param is present, i.e. the start page.
28
+ export function readRoute(): WorkbookRef | null {
29
+ const params = new URLSearchParams(window.location.search);
30
+ const file = params.get('file');
31
+ if (!file) return null;
32
+ const demo: DemoType = params.get('demo') === DEMO_HIGHLIGHT ? DEMO_HIGHLIGHT : DEMO_PERMANENT;
33
+ return {
34
+ file,
35
+ storedFile: params.get('storedFile') || file,
36
+ demo,
37
+ uid: params.get('uid') || makeUid(file),
38
+ fromUpload: params.get('fromUpload') || '',
39
+ };
40
+ }
41
+
42
+ // Pushes the given workbook selection into the URL (without a full page
43
+ // reload) so the browser's back/forward buttons and page refresh keep working.
44
+ export function pushWorkbookRoute(workbook: WorkbookRef): void {
45
+ const params = new URLSearchParams({ file: workbook.file, demo: workbook.demo || DEMO_PERMANENT });
46
+ if (workbook.storedFile !== workbook.file) params.set('storedFile', workbook.storedFile);
47
+ if ((workbook.demo || DEMO_PERMANENT) === DEMO_PERMANENT || workbook.fromUpload) {
48
+ params.set('uid', workbook.uid || makeUid(workbook.file));
49
+ }
50
+ if (workbook.fromUpload) params.set('fromUpload', workbook.fromUpload);
51
+ window.history.pushState(null, '', '/?' + params.toString());
52
+ }
File without changes
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>GridJS Angular Demo</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <app-root></app-root>
11
+ </body>
12
+ </html>
@@ -0,0 +1,7 @@
1
+ // Entry point: just bootstraps the standalone AppComponent (see ./app/app.component.ts).
2
+ // Shared route/API helpers live in ./app/routing.ts and ./app/api.ts so
3
+ // app.component.ts stays focused on component state and view logic.
4
+ import { bootstrapApplication } from '@angular/platform-browser';
5
+ import { AppComponent } from './app/app.component';
6
+
7
+ bootstrapApplication(AppComponent).catch((error) => console.error(error));
@@ -0,0 +1,37 @@
1
+ :root { font-family: Arial, Helvetica, sans-serif; color: #212529; background: #f8f9fa; }
2
+ * { box-sizing: border-box; }
3
+ html, body, #root, #app { min-height: 100%; }
4
+ body { margin: 0; overflow-y: hidden; background: #f8f9fa; font-size: 14px; }
5
+ button, input { font: inherit; }
6
+ .demo-shell { min-height: 100dvh; display: flex; flex-direction: column; background: #f8f9fa; }
7
+ .demo-navbar { min-height: 44px; display: flex; align-items: center; justify-content: center; background: #fff; border-bottom: 1px solid #dee2e6; box-shadow: 0 1px 3px rgba(0,0,0,.08); padding: 8px 12px; font-size: 19px; }
8
+ .demo-navbar a { color: #0000ee; text-decoration: underline; }
9
+ .demo-container { flex: 1; width: 100%; max-width: 960px; margin: 0 auto; padding: 16px 12px; }
10
+ .demo-selector { border: 1px solid #adb5bd; border-radius: 4px; margin: 0 0 16px; padding: 18px 14px 14px; display: flex; align-items: center; }
11
+ .demo-selector legend { width: auto; font-size: 14px; padding: 0 6px; }
12
+ .radio-card { min-height: 32px; display: inline-flex; align-items: center; gap: 6px; padding: 0 14px; margin-left: -1px; border: 1px solid #adb5bd; background: #f0f0f0; color: #444; font-size: 14px; cursor: pointer; position: relative; }
13
+ .radio-card:first-of-type { margin-left: 0; border-radius: 4px 0 0 4px; }
14
+ .radio-card:last-of-type { border-radius: 0 4px 4px 0; }
15
+ .radio-card input { width: 14px; height: 14px; margin: 0; accent-color: #0d6efd; }
16
+ .radio-card.active { background: #0d6efd; border-color: #0d6efd; color: #fff; z-index: 1; }
17
+ .upload-block { margin-top: 8px; }
18
+ .upload-block h4, .path-block h4 { margin: 0 0 10px; font-size: 14px; font-weight: bold; line-height: 1.3; }
19
+ .upload-block input[type=file] { margin-bottom: 12px; font-size: 13px; }
20
+ .path-block { margin-top: 4px; }
21
+ .file-list { min-height: 140px; max-height: 220px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 4px; background: #f6f7ef; padding: 10px 12px; }
22
+ .file-item { display: block; margin-bottom: 10px; color: #0000ee; text-decoration: underline; font-size: 14px; font-style: italic; }
23
+ .file-item:hover { text-decoration: underline; }
24
+ .radio-card:focus-within, .file-item:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid rgba(13, 110, 253, .35); outline-offset: 2px; }
25
+ .status-message { min-height: 20px; margin: 8px 0; color: #495057; }
26
+ .empty-message { margin: 12px 4px; color: #64748b; }
27
+ .demo-footer { min-height: 40px; border-top: 1px solid #dee2e6; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 12px; font-size: 13px; color: #495057; background: #f8f9fa; }
28
+ .demo-footer a { color: #0000ee; text-decoration: underline; }
29
+ .editor-page { height: 100dvh; width: 100vw; overflow: hidden; background: #fff; }
30
+ .editor-loading { height: 100vh; display: grid; place-items: center; color: #64748b; background: #fff; }
31
+ .gridjs-react-host, .gridjs-vue-host, gridjs-spreadsheet { display: block; width: 100%; height: 100vh; }
32
+ @media (max-width: 700px) {
33
+ body { overflow-y: auto; }
34
+ .demo-selector { flex-direction: column; align-items: stretch; }
35
+ .radio-card { margin-left: 0; width: 100%; }
36
+ .radio-card:first-of-type, .radio-card:last-of-type { border-radius: 4px; }
37
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/app",
5
+ "types": []
6
+ },
7
+ "files": ["src/main.ts"],
8
+ "include": ["src/**/*.d.ts"]
9
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "outDir": "./dist/out-tsc",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": false,
8
+ "noImplicitOverride": false,
9
+ "noPropertyAccessFromIndexSignature": false,
10
+ "noImplicitReturns": false,
11
+ "noFallthroughCasesInSwitch": false,
12
+ "sourceMap": true,
13
+ "declaration": false,
14
+ "downlevelIteration": true,
15
+ "experimentalDecorators": true,
16
+ "moduleResolution": "bundler",
17
+ "importHelpers": true,
18
+ "target": "ES2022",
19
+ "module": "ES2022",
20
+ "useDefineForClassFields": false,
21
+ "lib": ["ES2022", "dom"]
22
+ },
23
+ "angularCompilerOptions": {
24
+ "enableI18nLegacyMessageIdFormat": false,
25
+ "strictInjectionParameters": true,
26
+ "strictInputAccessModifiers": false,
27
+ "strictTemplates": false
28
+ }
29
+ }
package/example/pom.xml CHANGED
@@ -17,7 +17,7 @@
17
17
  </repositories>
18
18
  <groupId>com.aspose.gridjs</groupId>
19
19
  <artifactId>gridjsdemo</artifactId>
20
- <version>26.6</version>
20
+ <version>26.7</version>
21
21
  <name>gridjsdemo</name>
22
22
  <description>GridJs Demo project for Spring Boot</description>
23
23
  <properties>
@@ -51,12 +51,12 @@
51
51
  <dependency>
52
52
  <groupId>com.aspose</groupId>
53
53
  <artifactId>aspose-cells</artifactId>
54
- <version>26.5</version>
54
+ <version>26.7</version>
55
55
  </dependency>
56
56
  <dependency>
57
57
  <groupId>com.aspose</groupId>
58
58
  <artifactId>aspose-cells</artifactId>
59
- <version>26.5</version>
59
+ <version>26.7</version>
60
60
  <classifier>gridjs</classifier>
61
61
  </dependency>
62
62
  <!-- <dependency>
@@ -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>GridJS React Demo</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "gridjs-react-java-demo",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite --host 127.0.0.1 --port 5173 --strictPort",
7
+ "build": "vite build",
8
+ "preview": "vite preview --host 127.0.0.1 --port 6173 --strictPort"
9
+ },
10
+ "dependencies": {
11
+ "gridjs-spreadsheet": "latest",
12
+ "jszip": "^3.10.1",
13
+ "react": "latest",
14
+ "react-dom": "latest"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "latest",
18
+ "vite": "latest"
19
+ }
20
+ }
@@ -0,0 +1,188 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { GridJsSpreadsheet } from 'gridjs-spreadsheet/react';
3
+ import { DEMO_PERMANENT, DEMO_HIGHLIGHT, makeUid, readRoute, pushWorkbookRoute } from './routing';
4
+ import { fetchJson } from './api';
5
+
6
+ // Root demo component.
7
+ // Renders either:
8
+ // - the start page (demo picker + sample file list + upload box), or
9
+ // - the full-page spreadsheet editor once a workbook has been selected.
10
+ // Which one is shown is driven entirely by the URL (see ./routing.js).
11
+ export function App() {
12
+ // Sample workbooks listed on the start page.
13
+ const [files, setFiles] = useState([]);
14
+ const [directory, setDirectory] = useState('');
15
+ // The workbook selected via the URL; null means "show the start page".
16
+ const [current, setCurrent] = useState(() => readRoute());
17
+ // Which demo mode is active (only relevant while on the start page).
18
+ const [demoType, setDemoType] = useState(() => readRoute()?.demo || DEMO_PERMANENT);
19
+ const [status, setStatus] = useState('Loading workbook list...');
20
+ const adapterRef = useRef(null);
21
+
22
+ const logEvent = useCallback((message) => {
23
+ console.log('[GridJS Event]', message);
24
+ }, []);
25
+
26
+ // Loads the list of sample workbooks available on the server.
27
+ const loadFiles = useCallback(async () => {
28
+ try {
29
+ const result = await fetchJson('/gridjsdemo/api/files');
30
+ setFiles(result.files || []);
31
+ setDirectory(result.directory || '');
32
+ setStatus('Ready');
33
+ } catch (error) {
34
+ setStatus('Failed to load files: ' + error.message);
35
+ }
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ loadFiles();
40
+ // Keep state in sync when the user navigates with the browser's back/forward buttons.
41
+ const onPopState = () => {
42
+ const next = readRoute();
43
+ setCurrent(next);
44
+ if (next?.demo) setDemoType(next.demo);
45
+ };
46
+ window.addEventListener('popstate', onPopState);
47
+ return () => window.removeEventListener('popstate', onPopState);
48
+ }, [loadFiles]);
49
+
50
+ // Navigates to a workbook: updates the URL and local state together.
51
+ const openWorkbook = (workbook) => {
52
+ const normalized = { ...workbook, demo: workbook.demo || demoType };
53
+ pushWorkbookRoute(normalized);
54
+ setCurrent(normalized);
55
+ setDemoType(normalized.demo);
56
+ };
57
+
58
+ const openFile = (file) => openWorkbook({ file, storedFile: file, demo: demoType, uid: makeUid(file), fromUpload: '' });
59
+
60
+ // Fetches workbook data for the GridJsSpreadsheet component. Which endpoint
61
+ // is used depends on the demo mode:
62
+ // - permanent: uid-based endpoint, backend caches parsed data per uid.
63
+ // - highlight: plain endpoint, re-parses the file on every load.
64
+ const loader = useCallback(async () => {
65
+ if (!current?.file) return null;
66
+ const demo = current.demo || DEMO_PERMANENT;
67
+ const params = new URLSearchParams({
68
+ filename: current.storedFile || current.file,
69
+ fromUpload: current.fromUpload || '',
70
+ });
71
+ let endpoint = '/GridJs2/DetailStreamJson';
72
+ if (current.fromUpload) {
73
+ params.set('uid', current.uid || makeUid(current.file));
74
+ endpoint = '/GridJs2/DetailStreamJsonWithUidFromUpload';
75
+ } else if (demo === DEMO_PERMANENT) {
76
+ params.set('uid', current.uid || makeUid(current.file));
77
+ endpoint = '/GridJs2/DetailStreamJsonWithUid';
78
+ }
79
+ const payload = await fetchJson(endpoint + '?' + params);
80
+ return { ...payload, filename: current.file };
81
+ }, [current]);
82
+
83
+ const upload = async (event) => {
84
+ const file = event.target.files?.[0];
85
+ if (!file) return;
86
+ const form = new FormData();
87
+ form.append('file', file);
88
+ form.append('client', 'react');
89
+ setStatus('Uploading ' + file.name + '...');
90
+ try {
91
+ const result = await fetchJson('/gridjsdemo/api/upload', { method: 'POST', body: form });
92
+ logEvent('uploaded ' + result.file);
93
+ // Newly uploaded files live under the server's uploads dir, flagged with fromUpload=1.
94
+ openWorkbook({
95
+ file: result.displayName || file.name,
96
+ storedFile: result.file,
97
+ demo: demoType,
98
+ uid: result.uid || makeUid(result.file),
99
+ fromUpload: '1',
100
+ });
101
+ } catch (error) {
102
+ setStatus('Upload failed: ' + error.message);
103
+ } finally {
104
+ event.target.value = '';
105
+ }
106
+ };
107
+
108
+ // Restores the previously active sheet/cell (from the workbook data) once
109
+ // the grid has mounted, and points the "open file" link back at the start page.
110
+ const handleReady = (instance, adapter) => {
111
+ adapterRef.current = adapter;
112
+ instance.setActiveSheetByName?.(adapter.payload?.actname)?.setActiveCell?.(adapter.payload?.actrow, adapter.payload?.actcol);
113
+ instance.setOpenFileUrl?.('/');
114
+ logEvent('ready ' + (adapter.payload?.filename || current.file));
115
+ };
116
+
117
+ const handleError = (payload) => {
118
+ console.error(payload);
119
+ logEvent('error ' + (payload?.message || payload?.type || 'unknown'));
120
+ };
121
+
122
+ // A workbook is selected -> render the full-page spreadsheet editor.
123
+ if (current) {
124
+ return (
125
+ <main className="editor-page">
126
+ <GridJsSpreadsheet
127
+ key={current.file + '-' + current.uid + '-' + current.demo}
128
+ loader={loader}
129
+ height="100vh"
130
+ showToolbar
131
+ showContextmenu
132
+ onReady={handleReady}
133
+ onChange={() => logEvent('changed')}
134
+ onError={handleError}
135
+ onCellSelected={(_, ri, ci) => logEvent('cell selected ' + ri + ',' + ci)}
136
+ onCellEdited={(text, ri, ci) => logEvent('cell edited ' + ri + ',' + ci)}
137
+ onSheetSelected={(_, name) => logEvent('sheet ' + name)}
138
+ />
139
+ </main>
140
+ );
141
+ }
142
+
143
+ // No workbook selected -> render the start page (demo picker + file list + upload).
144
+ return (
145
+ <div className="demo-shell">
146
+ <header className="demo-navbar">
147
+ <a href="https://docs.aspose.com/cells/java/aspose-cells-gridjs/">Aspose.Cells.GridJs demo for Java</a>
148
+ </header>
149
+
150
+ <main className="demo-container">
151
+ <fieldset className="demo-selector">
152
+ <legend>Select a demo:</legend>
153
+ <label className={demoType === DEMO_PERMANENT ? 'radio-card active' : 'radio-card'}>
154
+ <input type="radio" name="demoType" checked={demoType === DEMO_PERMANENT} onChange={() => setDemoType(DEMO_PERMANENT)} />
155
+ <span>permanent url load demo</span>
156
+ </label>
157
+ <label className={demoType === DEMO_HIGHLIGHT ? 'radio-card active' : 'radio-card'}>
158
+ <input type="radio" name="demoType" checked={demoType === DEMO_HIGHLIGHT} onChange={() => setDemoType(DEMO_HIGHLIGHT)} />
159
+ <span>highlight and custom context menu demo</span>
160
+ </label>
161
+ </fieldset>
162
+
163
+ <section className="upload-block">
164
+ <h4>Upload a local file to start show workbook</h4>
165
+ <input type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" onChange={upload} />
166
+ </section>
167
+
168
+ <section className="path-block">
169
+ <h4>The specific file path : {directory}</h4>
170
+ <h4>Navigate one of the below file to start show workbook</h4>
171
+ </section>
172
+
173
+ <p className="status-message" role="status">{status}</p>
174
+
175
+ <section className="file-list" aria-label="Workbook files">
176
+ {status === 'Ready' && files.length === 0 && <p className="empty-message">No workbooks found in the configured directory.</p>}
177
+ {files.map((file) => (
178
+ <a href={'/?file=' + encodeURIComponent(file)} key={file} className="file-item" onClick={(event) => { event.preventDefault(); openFile(file); }}>
179
+ <em>{file}</em>
180
+ </a>
181
+ ))}
182
+ </section>
183
+ </main>
184
+
185
+ <footer className="demo-footer">© 2026 - <a href="https://products.aspose.com/cells/java">Aspose.Cells for Java</a></footer>
186
+ </div>
187
+ );
188
+ }
@@ -0,0 +1,7 @@
1
+ // Small fetch() wrapper that turns non-2xx responses into thrown Errors,
2
+ // so callers can just `await fetchJson(url)` and catch failures in one place.
3
+ export async function fetchJson(url, options) {
4
+ const response = await fetch(url, options);
5
+ if (!response.ok) throw new Error(response.status + ' ' + response.statusText);
6
+ return response.json();
7
+ }
@@ -0,0 +1,9 @@
1
+ // Entry point: just mounts the <App /> root component (see ./App.jsx).
2
+ // Shared route/API helpers live in ./routing.js and ./api.js so this file
3
+ // and App.jsx stay focused on their own concerns.
4
+ import { createRoot } from 'react-dom/client';
5
+ import 'gridjs-spreadsheet/xspreadsheet.css';
6
+ import './styles.css';
7
+ import { App } from './App';
8
+
9
+ createRoot(document.getElementById('root')).render(<App />);
@@ -0,0 +1,51 @@
1
+ // Route (URL query string) helpers shared by the demo.
2
+ //
3
+ // The demo encodes which workbook is open directly in the URL so a page
4
+ // refresh or a shared link reopens the same file, e.g.:
5
+ // /?file=Sample.xlsx&demo=permanent&uid=uid-Sample-xlsx
6
+ // /?file=Sample.xlsx&demo=highlight&fromUpload=1
7
+ //
8
+ // demo:
9
+ // 'permanent' - loads the workbook through the uid-based streaming endpoint;
10
+ // the backend caches the parsed result under this uid.
11
+ // 'highlight' - loads the workbook through the plain streaming endpoint,
12
+ // used by the "highlight and custom context menu" demo.
13
+ export const DEMO_PERMANENT = 'permanent';
14
+ export const DEMO_HIGHLIGHT = 'highlight';
15
+ const CLIENT_PREFIX = 'react';
16
+
17
+ // Turns a file name into a stable cache key,
18
+ // e.g. "Sales Report.xlsx" -> "uid-Sales-Report-xlsx".
19
+ export function makeUid(file) {
20
+ return CLIENT_PREFIX + '-uid-' + file.replace(/[^a-zA-Z0-9]/g, '-');
21
+ }
22
+
23
+ // Reads the currently selected workbook (if any) from the URL query string.
24
+ // Returns null when no `file` param is present, i.e. the start page.
25
+ export function readRoute() {
26
+ const params = new URLSearchParams(window.location.search);
27
+ const file = params.get('file');
28
+ if (!file) return null;
29
+ const demo = params.get('demo') === DEMO_HIGHLIGHT ? DEMO_HIGHLIGHT : DEMO_PERMANENT;
30
+ return {
31
+ file,
32
+ storedFile: params.get('storedFile') || file,
33
+ demo,
34
+ uid: params.get('uid') || makeUid(file),
35
+ fromUpload: params.get('fromUpload') || '',
36
+ };
37
+ }
38
+
39
+ // Pushes the given workbook selection into the URL (without a full page
40
+ // reload) so the browser's back/forward buttons and page refresh keep working.
41
+ export function pushWorkbookRoute(workbook) {
42
+ const params = new URLSearchParams({ file: workbook.file, demo: workbook.demo || DEMO_PERMANENT });
43
+ if (workbook.storedFile && workbook.storedFile !== workbook.file) {
44
+ params.set('storedFile', workbook.storedFile);
45
+ }
46
+ if ((workbook.demo || DEMO_PERMANENT) === DEMO_PERMANENT || workbook.fromUpload) {
47
+ params.set('uid', workbook.uid || makeUid(workbook.file));
48
+ }
49
+ if (workbook.fromUpload) params.set('fromUpload', workbook.fromUpload);
50
+ window.history.pushState(null, '', '/?' + params.toString());
51
+ }
@@ -0,0 +1,37 @@
1
+ :root { font-family: Arial, Helvetica, sans-serif; color: #212529; background: #f8f9fa; }
2
+ * { box-sizing: border-box; }
3
+ html, body, #root, #app { min-height: 100%; }
4
+ body { margin: 0; overflow-y: hidden; background: #f8f9fa; font-size: 14px; }
5
+ button, input { font: inherit; }
6
+ .demo-shell { min-height: 100dvh; display: flex; flex-direction: column; background: #f8f9fa; }
7
+ .demo-navbar { min-height: 44px; display: flex; align-items: center; justify-content: center; background: #fff; border-bottom: 1px solid #dee2e6; box-shadow: 0 1px 3px rgba(0,0,0,.08); padding: 8px 12px; font-size: 19px; }
8
+ .demo-navbar a { color: #0000ee; text-decoration: underline; }
9
+ .demo-container { flex: 1; width: 100%; max-width: 960px; margin: 0 auto; padding: 16px 12px; }
10
+ .demo-selector { border: 1px solid #adb5bd; border-radius: 4px; margin: 0 0 16px; padding: 18px 14px 14px; display: flex; align-items: center; }
11
+ .demo-selector legend { width: auto; font-size: 14px; padding: 0 6px; }
12
+ .radio-card { min-height: 32px; display: inline-flex; align-items: center; gap: 6px; padding: 0 14px; margin-left: -1px; border: 1px solid #adb5bd; background: #f0f0f0; color: #444; font-size: 14px; cursor: pointer; position: relative; }
13
+ .radio-card:first-of-type { margin-left: 0; border-radius: 4px 0 0 4px; }
14
+ .radio-card:last-of-type { border-radius: 0 4px 4px 0; }
15
+ .radio-card input { width: 14px; height: 14px; margin: 0; accent-color: #0d6efd; }
16
+ .radio-card.active { background: #0d6efd; border-color: #0d6efd; color: #fff; z-index: 1; }
17
+ .upload-block { margin-top: 8px; }
18
+ .upload-block h4, .path-block h4 { margin: 0 0 10px; font-size: 14px; font-weight: bold; line-height: 1.3; }
19
+ .upload-block input[type=file] { margin-bottom: 12px; font-size: 13px; }
20
+ .path-block { margin-top: 4px; }
21
+ .file-list { min-height: 140px; max-height: 220px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 4px; background: #f6f7ef; padding: 10px 12px; }
22
+ .file-item { display: block; margin-bottom: 10px; color: #0000ee; text-decoration: underline; font-size: 14px; font-style: italic; }
23
+ .file-item:hover { text-decoration: underline; }
24
+ .radio-card:focus-within, .file-item:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid rgba(13, 110, 253, .35); outline-offset: 2px; }
25
+ .status-message { min-height: 20px; margin: 8px 0; color: #495057; }
26
+ .empty-message { margin: 12px 4px; color: #64748b; }
27
+ .demo-footer { min-height: 40px; border-top: 1px solid #dee2e6; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 12px; font-size: 13px; color: #495057; background: #f8f9fa; }
28
+ .demo-footer a { color: #0000ee; text-decoration: underline; }
29
+ .editor-page { height: 100dvh; width: 100vw; overflow: hidden; background: #fff; }
30
+ .editor-loading { height: 100vh; display: grid; place-items: center; color: #64748b; background: #fff; }
31
+ .gridjs-react-host, .gridjs-vue-host, gridjs-spreadsheet { display: block; width: 100%; height: 100vh; }
32
+ @media (max-width: 700px) {
33
+ body { overflow-y: auto; }
34
+ .demo-selector { flex-direction: column; align-items: stretch; }
35
+ .radio-card { margin-left: 0; width: 100%; }
36
+ .radio-card:first-of-type, .radio-card:last-of-type { border-radius: 4px; }
37
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ proxy: {
8
+ '/GridJs2': 'http://127.0.0.1:8080',
9
+ '/gridjsdemo': 'http://127.0.0.1:8080'
10
+ }
11
+ },
12
+ build: {
13
+ outDir: 'dist',
14
+ emptyOutDir: true
15
+ }
16
+ });
@@ -7,7 +7,12 @@ import java.nio.file.Files;
7
7
  import java.nio.file.Path;
8
8
  import java.nio.file.Paths;
9
9
  import java.util.ArrayList;
10
+ import java.util.Arrays;
11
+ import java.util.Comparator;
12
+ import java.util.LinkedHashMap;
10
13
  import java.util.List;
14
+ import java.util.Map;
15
+ import java.util.UUID;
11
16
 
12
17
  import org.springframework.beans.factory.annotation.Value;
13
18
  import org.springframework.http.ResponseEntity;
@@ -28,8 +33,34 @@ public class DataController {
28
33
  @Value("${testconfig.FileName}")
29
34
  private String testFileName;//="chart.xlsx";
30
35
 
31
- @Value("${testconfig.ListDir}")
32
- private String listDir;
36
+ @Value("${testconfig.ListDir}")
37
+ private String listDir;
38
+
39
+ @GetMapping("/api/health")
40
+ public Map<String, String> health() {
41
+ Map<String, String> response = new LinkedHashMap<>();
42
+ response.put("status", "ok");
43
+ response.put("service", "gridjsdemo-java");
44
+ return response;
45
+ }
46
+
47
+ @GetMapping("/api/files")
48
+ public Map<String, Object> filesJson() {
49
+ List<String> files = new ArrayList<>();
50
+ File directory = new File(listDir);
51
+ File[] entries = directory.listFiles(File::isFile);
52
+ if (entries != null) {
53
+ Arrays.sort(entries, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
54
+ for (File entry : entries) {
55
+ files.add(entry.getName());
56
+ }
57
+ }
58
+
59
+ Map<String, Object> response = new LinkedHashMap<>();
60
+ response.put("directory", directory.getAbsolutePath());
61
+ response.put("files", files);
62
+ return response;
63
+ }
33
64
 
34
65
  @GetMapping({"/index"})
35
66
  public ModelAndView getIndexPage()
@@ -88,6 +119,53 @@ public class DataController {
88
119
 
89
120
  @Value("${testconfig.UploadPath}")
90
121
  private String uploadDir;
122
+
123
+ @PostMapping("/api/upload")
124
+ public ResponseEntity<Map<String, Object>> uploadApi(
125
+ @RequestParam("file") MultipartFile file,
126
+ @RequestParam(value = "client", defaultValue = "vanilla") String client) {
127
+ Map<String, Object> response = new LinkedHashMap<>();
128
+ if (file.isEmpty()) {
129
+ response.put("error", "Choose a workbook before uploading.");
130
+ return ResponseEntity.badRequest().body(response);
131
+ }
132
+
133
+ try {
134
+ Path root = Paths.get(uploadDir).toAbsolutePath().normalize();
135
+ Files.createDirectories(root);
136
+
137
+ String originalName = safeFileName(file.getOriginalFilename());
138
+ String namespace = safeNamespace(client);
139
+ String storedName = namespace + "-" + UUID.randomUUID().toString() + "-" + originalName;
140
+ Path target = root.resolve(storedName).normalize();
141
+ if (!target.getParent().equals(root)) {
142
+ response.put("error", "Invalid upload path.");
143
+ return ResponseEntity.badRequest().body(response);
144
+ }
145
+
146
+ file.transferTo(target.toFile());
147
+ String uid = namespace + "-" + GridJsWorkbook.getUidForFile(storedName);
148
+ response.put("file", storedName);
149
+ response.put("displayName", originalName);
150
+ response.put("uid", uid);
151
+ response.put("fromUpload", "1");
152
+ return ResponseEntity.ok(response);
153
+ } catch (Exception exception) {
154
+ response.put("error", "Upload failed: " + exception.getMessage());
155
+ return ResponseEntity.status(500).body(response);
156
+ }
157
+ }
158
+
159
+ private static String safeFileName(String name) {
160
+ String candidate = name == null ? "workbook.xlsx" : Paths.get(name).getFileName().toString();
161
+ String sanitized = candidate.replaceAll("[^a-zA-Z0-9._() -]", "_");
162
+ return sanitized.isEmpty() ? "workbook.xlsx" : sanitized;
163
+ }
164
+
165
+ private static String safeNamespace(String client) {
166
+ String sanitized = client == null ? "vanilla" : client.replaceAll("[^a-zA-Z0-9-]", "-");
167
+ return sanitized.isEmpty() ? "vanilla" : sanitized;
168
+ }
91
169
 
92
170
  @PostMapping("/upload")
93
171
  public ModelAndView uploadFile(@RequestParam("file") MultipartFile file) {
@@ -95,6 +95,18 @@ public class GridJs2Controller extends GridJsControllerBase{
95
95
  e.printStackTrace();
96
96
  }
97
97
  }
98
+
99
+ @GetMapping("/DetailStreamJson")
100
+ public void detailStreamJson(@RequestParam String filename, HttpServletResponse response) {
101
+ Path filePath = Paths.get(listDir, filename);
102
+ response.setContentType("application/json");
103
+ response.setHeader("Content-Encoding", "gzip");
104
+ try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(response.getOutputStream())) {
105
+ _gridJsService.detailStreamJson(gzipOutputStream, filePath.toString());
106
+ } catch (Exception exception) {
107
+ exception.printStackTrace();
108
+ }
109
+ }
98
110
 
99
111
 
100
112
  @Value("${testconfig.UploadPath}")