socialcalc-ai 1.0.0 → 1.0.2

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.
package/README.md CHANGED
@@ -1,55 +1,243 @@
1
- # SocialCalc AI (`socialcalc-ai`)
1
+ # socialcalc-ai
2
+
3
+ [![npm version](https://img.shields.io/npm/v/socialcalc-ai.svg?color=cb3837&style=flat-square)](https://www.npmjs.com/package/socialcalc-ai)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
6
+ [![Module: ESM](https://img.shields.io/badge/Module-ESM-success.svg?style=flat-square)](https://nodejs.org/api/esm.html)
2
7
 
3
8
  > **A modernized, modularized, high-performance spreadsheet engine with ES6 module exports, React/Ionic UI components, AI template capabilities, and an extensible plugin system.**
4
9
 
10
+ Derived from Dan Bricklin's proven, industrial-strength **SocialCalc** engine, `socialcalc-ai` has been re-architected for modern web, mobile, and AI-driven applications. It replaces legacy global variables with clean ES modules, provides full TypeScript definitions, adds smooth mobile touch physics, and includes a full suite of modern React/Ionic UI controls.
11
+
12
+ ---
13
+
14
+ ## 📑 Table of Contents
15
+
16
+ - [Features](#-features)
17
+ - [Installation](#-installation)
18
+ - [Quick Start in React / Ionic](#-quick-start-in-react--ionic)
19
+ - [Quick Start in Vanilla JavaScript](#-quick-start-in-vanilla-javascript)
20
+ - [Architecture Overview](#-architecture-overview)
21
+ - [UI Components](#-ui-components)
22
+ - [CellEditModal](#1-celleditmodal)
23
+ - [HorizontalScrollBar](#2-horizontalscrollbar)
24
+ - [RowActionPopover](#3-rowactionpopover)
25
+ - [EditableCellsModal](#4-editablecellsmodal)
26
+ - [DemoVideosModal](#5-demovideosmodal)
27
+ - [Plugins & Modules API](#-plugins--modules-api)
28
+ - [Row & Column Headers](#1-row--column-headers-modulesrow-col-headersjs)
29
+ - [Grid Lines](#2-grid-lines-modulesgrid-linesjs)
30
+ - [Smooth Touch Scroll](#3-smooth-touch-scroll-modulestouch-scrolljs)
31
+ - [Horizontal Scroll](#4-horizontal-scroll-moduleshorizontal-scrolljs)
32
+ - [Editable Cells & Template Locks](#5-editable-cells--template-locks-moduleseditable-cellsjs)
33
+ - [Plugin Manager](#6-plugin-manager-modulesplugin-managerjs)
34
+ - [History & Undo/Redo](#7-history-undo--redo-moduleshistoryjs)
35
+ - [Exporters & Sheets](#8-exporters--sheets-modulesexportersjs-modulessheetsjs)
36
+ - [Invoice Utilities](#9-invoice-utilities-modulesinvoicejs)
37
+ - [Event-Driven Architecture](#-event-driven-architecture)
38
+ - [AI & MultiSheet Calc (MSC) Integration](#-ai--multisheet-calc-msc-integration)
39
+ - [TypeScript Support](#-typescript-support)
40
+ - [Browser & Framework Compatibility](#-browser--framework-compatibility)
41
+ - [License & Acknowledgments](#-license--acknowledgments)
42
+
43
+ ---
44
+
45
+ ## ✨ Features
46
+
47
+ - **⚡ Modern ES6 Architecture**: Pure ES module exports replacing legacy global namespace pollution. Works with Vite, Next.js, Webpack, Rollup, and ES2020+ runtimes.
48
+ - **📱 Touch & Mobile Optimized**: Smooth momentum physics and inertia swipe scrolling without screen jumping or layout stutter.
49
+ - **🎨 Modern React & Ionic UI Suite**:
50
+ - Full-featured **CellEditModal** (Formulas, rich formatting, custom color palettes, borders, and compressed image inserts).
51
+ - Smooth **HorizontalScrollBar** with live column indicators and drag navigation.
52
+ - Interactive **RowActionPopover** for dynamic row insertion and deletion.
53
+ - **EditableCellsModal** to inspect and manage cell locks.
54
+ - **DemoVideosModal** with an interactive formula cheat sheet.
55
+ - **🔌 Pluggable Architecture**: Dynamically enable, disable, or configure spreadsheet subsystems (headers, gridlines, momentum scroll, locks).
56
+ - **📐 Universal Calculation Engine**: Industrial-grade formula AST evaluator supporting standard math, statistics, string, logic, and financial functions.
57
+ - **🤖 AI-Ready MultiSheet Calc (MSC)**: Serialize and deserialize full spreadsheet workbooks as structured JSON or MSC strings, ideal for LLM spreadsheet generation pipelines.
58
+ - **🛡 Full TypeScript Definitions**: First-class `index.d.ts` definitions included out of the box.
59
+
5
60
  ---
6
61
 
7
62
  ## 📦 Installation
8
63
 
64
+ Install `socialcalc-ai` via npm:
65
+
9
66
  ```bash
10
67
  npm install socialcalc-ai
11
68
  ```
12
69
 
13
- Or with yarn / pnpm:
70
+ Or using Yarn / pnpm:
71
+
14
72
  ```bash
73
+ # Using Yarn
15
74
  yarn add socialcalc-ai
75
+
76
+ # Using pnpm
16
77
  pnpm add socialcalc-ai
17
78
  ```
18
79
 
80
+ ### Peer Dependencies
81
+
82
+ If you plan to use the pre-built React / Ionic components (`CellEditModal`, `HorizontalScrollBar`, etc.), ensure the peer dependencies are installed:
83
+
84
+ ```bash
85
+ npm install react react-dom @ionic/react ionicons
86
+ ```
87
+
88
+ *(Note: Peer dependencies are optional if you only consume the core calculation engine in headless Node.js or Vanilla JS environments).*
89
+
19
90
  ---
20
91
 
21
- ## 📌 Overview
92
+ ## 🚀 Quick Start in React / Ionic
93
+
94
+ Below is a complete, minimal working example showing how to mount the spreadsheet engine and hook up the interactive React UI suite:
95
+
96
+ ```tsx
97
+ import React, { useEffect, useState } from "react";
98
+ import * as AppGeneral from "socialcalc-ai";
99
+ import {
100
+ CellEditModal,
101
+ HorizontalScrollBar,
102
+ RowActionPopover,
103
+ EditableCellsModal,
104
+ } from "socialcalc-ai";
105
+
106
+ // Optional: Ionic base styles if using Ionic framework
107
+ import "@ionic/react/css/core.css";
22
108
 
23
- This package (`socialcalc-ai`) is a **standalone, framework-agnostic core** with first-class React & Ionic bindings. It takes Dan Bricklin's proven, industrial-strength SocialCalc engine and modernizes it for modern web, mobile, and AI applications:
24
- - **Clean ES6 module structure** replacing legacy globals.
25
- - **Pluggable Architecture**: Dynamically toggle features like Grid Lines, 123/ABCD Row-Col Headers, Touch Scroll with momentum, and Editable Cell Locking.
26
- - **Modern Touch & Mobile Support**: Smooth inertia swipe scrolling without jumping.
27
- - **Modern Ionic / React Components**: High-fidelity modal editors, responsive horizontal scroll controls, context popovers for row operations, and cell mapping inspectors.
28
- - **Universal Spreadsheet Commands**: Full support for SocialCalc formulas, cell formatting, font styling, borders, multi-sheet workbooks, and CSV/MSC serialization.
109
+ export const SpreadsheetEditor: React.FC = () => {
110
+ const [cellEditData, setCellEditData] = useState<any>(null);
111
+ const [showEditModal, setShowEditModal] = useState(false);
112
+ const [rowPopoverEvent, setRowPopoverEvent] = useState<{ event: any; rowNum: number } | null>(null);
113
+
114
+ useEffect(() => {
115
+ // 1. Initialize SocialCalc DOM container with initial data (or empty string)
116
+ const initialMSC = "";
117
+ AppGeneral.initializeApp(initialMSC);
29
118
 
30
- > ⚠️ **CRITICAL FOR DEVELOPERS & AGENTS**:
31
- > This package is consumed as a shared standalone dependency across multiple downstream apps. Any breaking changes to public APIs, event signatures, or file paths will impact all applications using it. Always preserve backward compatibility.
119
+ // 2. Activate desired plugins
120
+ AppGeneral.enableRowColHeaders(); // 1, 2, 3... & A, B, C... headers with resize handles
121
+ AppGeneral.enableGridLines(); // Modern cell border grid
122
+ AppGeneral.enableTouchScroll(); // Smooth mobile inertia scroll
123
+
124
+ // 3. Listen for Cell Edit Modal requests
125
+ const handleCellEdit = (e: any) => {
126
+ setCellEditData(e.detail);
127
+ setShowEditModal(true);
128
+ };
129
+
130
+ // 4. Listen for Row Header clicks (context actions)
131
+ const handleRowClick = (e: any) => {
132
+ setRowPopoverEvent({
133
+ event: e.detail.originalEvent || e.detail,
134
+ rowNum: e.detail.rowNum,
135
+ });
136
+ };
137
+
138
+ window.addEventListener("socialcalc:cell-edit-request", handleCellEdit);
139
+ window.addEventListener("socialcalc:row-header-click", handleRowClick);
140
+
141
+ return () => {
142
+ window.removeEventListener("socialcalc:cell-edit-request", handleCellEdit);
143
+ window.removeEventListener("socialcalc:row-header-click", handleRowClick);
144
+ };
145
+ }, []);
146
+
147
+ return (
148
+ <div style={{ position: "relative", width: "100%", height: "100vh", overflow: "hidden" }}>
149
+ {/* SocialCalc DOM Mounting Target Elements */}
150
+ <div id="container" style={{ width: "100%", height: "calc(100% - 44px)" }}>
151
+ <div id="workbookControl" style={{ display: "none" }}></div>
152
+ <div id="tableeditor" style={{ width: "100%", height: "100%" }}></div>
153
+ <div id="msg"></div>
154
+ </div>
155
+
156
+ {/* Horizontal Viewport Scrollbar */}
157
+ <HorizontalScrollBar />
158
+
159
+ {/* Rich Cell Editor Bottom Sheet */}
160
+ <CellEditModal
161
+ isOpen={showEditModal}
162
+ cellData={cellEditData}
163
+ onClose={() => setShowEditModal(false)}
164
+ />
165
+
166
+ {/* Row Insert / Delete Popover */}
167
+ <RowActionPopover
168
+ isOpen={!!rowPopoverEvent}
169
+ event={rowPopoverEvent?.event}
170
+ selectedRow={rowPopoverEvent?.rowNum}
171
+ onDismiss={() => setRowPopoverEvent(null)}
172
+ />
173
+ </div>
174
+ );
175
+ };
176
+
177
+ export default SpreadsheetEditor;
178
+ ```
32
179
 
33
180
  ---
34
181
 
35
- ## 📁 Package Architecture
182
+ ## 🌐 Quick Start in Vanilla JavaScript
183
+
184
+ `socialcalc-ai` can be used in plain HTML/JavaScript or non-React web apps without any framework overhead:
185
+
186
+ ```html
187
+ <!DOCTYPE html>
188
+ <html lang="en">
189
+ <head>
190
+ <meta charset="UTF-8" />
191
+ <title>SocialCalc AI Standalone</title>
192
+ </head>
193
+ <body>
194
+ <div id="container">
195
+ <div id="workbookControl" style="display: none;"></div>
196
+ <div id="tableeditor"></div>
197
+ <div id="msg"></div>
198
+ </div>
199
+
200
+ <script type="module">
201
+ import * as SocialCalcApp from "socialcalc-ai";
202
+
203
+ // Initialize spreadsheet
204
+ SocialCalcApp.initializeApp("");
205
+
206
+ // Enable modern features
207
+ SocialCalcApp.enableGridLines();
208
+ SocialCalcApp.enableRowColHeaders();
209
+ SocialCalcApp.enableTouchScroll();
210
+
211
+ // Listen for cell change events
212
+ SocialCalcApp.setupCellChangeListener((coord, value) => {
213
+ console.log(`Cell ${coord} updated:`, value);
214
+ });
215
+ </script>
216
+ </body>
217
+ </html>
218
+ ```
219
+
220
+ ---
221
+
222
+ ## 📁 Architecture Overview
36
223
 
37
224
  ```text
38
- socialcalc/
39
- ├── index.js # Main entry point: re-exports core, all modules & React components
225
+ socialcalc-ai/
226
+ ├── index.js # Main entry point: aggregates core, modules & UI
227
+ ├── index.d.ts # Complete TypeScript typings
40
228
  ├── package.json # Standalone package definition
41
- ├── core/ # Core calculation engine & table rendering
42
- │ ├── constants.js # SocialCalc constant definitions & default styles
229
+ ├── core/ # Core calculation engine & DOM table rendering
230
+ │ ├── constants.js # Formatting codes, default styles & limits
43
231
  │ ├── core.js # Spreadsheet sheet model, cell evaluation, formula AST
44
- │ ├── formula.js # Built-in math, logical, string, financial formulas
232
+ │ ├── formula.js # Built-in math, logical, string, financial functions
45
233
  │ ├── format-number.js # Numeric and date formatting routines
46
234
  │ ├── spreadsheet-control.js # Spreadsheet control lifecycle & pane management
47
235
  │ ├── table-editor.js # DOM table editor layout & cursor rendering
48
236
  │ └── index.js # Assembled core SocialCalc singleton export
49
- ├── modules/ # Modularized functional features & plugins
237
+ ├── modules/ # Modular functional features & plugins
50
238
  │ ├── plugin-manager.js # Central registry for dynamic SocialCalc plugins
51
- │ ├── grid-lines.js # Dynamic cell border grid-lines plugin
52
- │ ├── row-col-headers.js # 123 Row & ABCD Col headers with resize handles & row selection
239
+ │ ├── grid-lines.js # Cell border grid-lines plugin
240
+ │ ├── row-col-headers.js # 123 / ABCD headers with resize handles & row selection
53
241
  │ ├── touch-scroll.js # Momentum/inertia mobile touch scroll
54
242
  │ ├── horizontal-scroll.js # Viewport tracking & horizontal scrolling subscriptions
55
243
  │ ├── editable-cells.js # Cell lock & template permission plugin
@@ -60,12 +248,13 @@ socialcalc/
60
248
  │ ├── init.js # Workbook & table editor DOM initialization
61
249
  │ ├── prompts.js # Enhanced input dialogs & modal dispatchers
62
250
  │ ├── exporters.js # HTML, CSV, MSC export helpers
251
+ │ ├── invoice.js # Coordinate mapping & invoice utilities
63
252
  │ ├── device.js # Device detection and responsive helpers
64
253
  │ ├── logos.js # Brand/logo helper utilities
65
254
  │ ├── utils.js # Coordinate conversions, DOM utilities
66
255
  │ └── weight.js # Cell weight/scoring utilities
67
256
  ├── components/ # React & Ionic UI Components
68
- │ ├── CellEditModal/ # Rich bottom sheet editor (Text, Formula, Colors, Borders, Images)
257
+ │ ├── CellEditModal/ # Rich editor sheet (Formulas, Text, Colors, Borders, Images)
69
258
  │ ├── HorizontalScrollBar.tsx # Smooth horizontal navigation bar & track slider
70
259
  │ ├── RowActionPopover/ # Context menu for row insertion & deletion
71
260
  │ ├── EditableCellsModal/ # Template mapping manager & cell lock editor
@@ -77,192 +266,284 @@ socialcalc/
77
266
 
78
267
  ---
79
268
 
80
- ## 🚀 Quick Start in React / Ionic
269
+ ## 🎨 UI Components
81
270
 
82
- ### 1. Installation
271
+ ### 1. `CellEditModal`
272
+ A modern, responsive bottom-sheet modal that replaces legacy browser `prompt()` dialogs with a rich spreadsheet formatting bar:
273
+ - **Formula & Text Editor**: Live input bar with syntax validation (e.g. `=SUM(A1:B10)`).
274
+ - **Styling**: Bold, Italic, Alignment (Left, Center, Right).
275
+ - **Curated Palette**: 8 font colors (`FONT_COLORS`) and 7 background colors (`BG_COLORS`).
276
+ - **Cell Borders**: Top, Bottom, Left, Right, and All borders with Solid, Dashed, or Dotted styles.
277
+ - **Image Insertion**: Embed images into cells with automatic client-side compression via `imageCompressor.ts`.
278
+ - **Fallback**: Automatically falls back to native inline input if modal mode is disabled.
83
279
 
84
- ```bash
85
- npm install socialcalc-ai
280
+ ```tsx
281
+ <CellEditModal
282
+ isOpen={isOpen}
283
+ cellData={cellData}
284
+ onClose={() => setIsOpen(false)}
285
+ />
86
286
  ```
87
287
 
88
- ### 2. Initialization in React Component
288
+ ### 2. `HorizontalScrollBar`
289
+ A smooth horizontal track controller designed specifically for wide spreadsheets on mobile and desktop:
290
+ - Step-by-step column advancement buttons (◀ / ▶).
291
+ - Responsive drag track with active column indicator (e.g., `Cols: A - J`).
292
+ - Synchronizes bi-directionally with keyboard arrow navigation and touch scrolling.
89
293
 
90
294
  ```tsx
91
- import React, { useEffect, useState } from "react";
92
- import * as AppGeneral from "socialcalc-ai";
93
- import {
94
- CellEditModal,
95
- HorizontalScrollBar,
96
- RowActionPopover,
97
- } from "socialcalc-ai";
98
-
99
- const MySpreadsheet: React.FC = () => {
100
- const [cellEditData, setCellEditData] = useState<any>(null);
101
- const [showModal, setShowModal] = useState(false);
102
-
103
- useEffect(() => {
104
- // 1. Initialize DOM workbook container
105
- const initialMSC = { /* SocialCalc MSC json or save string */ };
106
- AppGeneral.initializeApp(JSON.stringify(initialMSC));
295
+ <HorizontalScrollBar />
296
+ ```
107
297
 
108
- // 2. Enable desired plugins
109
- AppGeneral.enableRowColHeaders(); // 123 / ABCD headers
110
- AppGeneral.enableGridLines(); // Cell grid borders
111
- AppGeneral.enableTouchScroll(); // Smooth touch scrolling
298
+ ### 3. `RowActionPopover`
299
+ A context popup that appears when clicking any row header index:
300
+ - **Insert Row Above**: Inserts an empty row above the selected index.
301
+ - **Insert Row Below**: Inserts an empty row below the selected index.
302
+ - **Delete Row**: Removes the selected row and cascades formula references.
112
303
 
113
- // 3. Listen for Cell Edit Modal request events
114
- const handleCellEdit = (e: any) => {
115
- setCellEditData(e.detail);
116
- setShowModal(true);
117
- };
118
- window.addEventListener("socialcalc:cell-edit-request", handleCellEdit);
304
+ ```tsx
305
+ <RowActionPopover
306
+ isOpen={showPopover}
307
+ event={clickEvent}
308
+ selectedRow={rowNumber}
309
+ onDismiss={() => setShowPopover(false)}
310
+ />
311
+ ```
119
312
 
120
- return () => {
121
- window.removeEventListener("socialcalc:cell-edit-request", handleCellEdit);
122
- };
123
- }, []);
313
+ ### 4. `EditableCellsModal`
314
+ An administrative inspector for template creators:
315
+ - View all locked vs editable cells in the active sheet.
316
+ - Bind application field mappings (e.g., `vendor_name -> B4`) to spreadsheet coordinates.
317
+ - Test template locking permissions.
124
318
 
125
- return (
126
- <div style={{ position: "relative", width: "100%", height: "100vh" }}>
127
- {/* SocialCalc mounting targets */}
128
- <div id="container">
129
- <div id="workbookControl" style={{ display: "none" }}></div>
130
- <div id="tableeditor"></div>
131
- <div id="msg"></div>
132
- </div>
319
+ ```tsx
320
+ <EditableCellsModal
321
+ isOpen={showModal}
322
+ onClose={() => setShowModal(false)}
323
+ />
324
+ ```
133
325
 
134
- {/* Horizontal Scroll Bar */}
135
- <HorizontalScrollBar />
326
+ ### 5. `DemoVideosModal`
327
+ An interactive help and reference modal containing:
328
+ - Built-in formula cheat sheets (`SUM`, `AVERAGE`, `IF`, `VLOOKUP`, `COUNT`, `CONCATENATE`, etc.).
329
+ - Embedded tutorial guides for spreadsheet operators.
136
330
 
137
- {/* Modern Cell Edit Modal */}
138
- <CellEditModal
139
- isOpen={showModal}
140
- cellData={cellEditData}
141
- onClose={() => setShowModal(false)}
142
- />
143
- </div>
144
- );
145
- };
331
+ ```tsx
332
+ <DemoVideosModal
333
+ isOpen={showHelp}
334
+ onClose={() => setShowHelp(false)}
335
+ />
146
336
  ```
147
337
 
148
338
  ---
149
339
 
150
- ## 🛠 Features & Plugins Deep Dive
151
-
152
- ### 1. Row and Column Headers (`modules/row-col-headers.js`)
153
- Displays standard spreadsheet row numbers (`1, 2, 3...`) and column letters (`A, B, C...`).
154
- - **Smooth Column Resizing**: Hover or drag right edge of any column header to resize. Live floating tooltip and guideline indicate new column width.
155
- - **Corner Resize Handle**: Tap/click a column header to display a mobile-friendly corner drag handle (`#sc-col-resize-corner-handle`).
156
- - **Row Selection**: Clicking a row header selects the full row and fires `socialcalc:row-header-click`.
157
- - **API**:
158
- ```ts
159
- AppGeneral.enableRowColHeaders();
160
- AppGeneral.disableRowColHeaders();
161
- AppGeneral.toggleRowColHeaders(); // returns boolean
162
- AppGeneral.isRowColHeadersEnabled(); // returns boolean
163
- AppGeneral.executeSheetCommand(cmd); // runs SocialCalc sheet command synchronously
164
- ```
340
+ ## 🔌 Plugins & Modules API
341
+
342
+ ### 1. Row & Column Headers (`modules/row-col-headers.js`)
343
+ Enables 123 row numbers and ABCD column headers with interactive column resizing:
344
+ ```ts
345
+ // Enable row/col headers
346
+ AppGeneral.enableRowColHeaders();
347
+
348
+ // Disable row/col headers
349
+ AppGeneral.disableRowColHeaders();
350
+
351
+ // Toggle headers dynamically (returns boolean)
352
+ const isEnabled = AppGeneral.toggleRowColHeaders();
353
+
354
+ // Execute raw SocialCalc commands
355
+ AppGeneral.executeSheetCommand("set A1 text hello");
356
+ ```
165
357
 
166
358
  ### 2. Grid Lines (`modules/grid-lines.js`)
167
- Provides clean, modern cell grid lines across all table cells.
168
- - **API**:
169
- ```ts
170
- AppGeneral.enableGridLines(customCss?: string);
171
- AppGeneral.disableGridLines();
172
- AppGeneral.toggleGridLines(); // returns boolean
173
- AppGeneral.isGridLinesEnabled(); // returns boolean
174
- ```
359
+ Adds subtle, high-contrast borders between cells:
360
+ ```ts
361
+ AppGeneral.enableGridLines();
362
+ AppGeneral.disableGridLines();
363
+ AppGeneral.toggleGridLines();
364
+ ```
175
365
 
176
366
  ### 3. Smooth Touch Scroll (`modules/touch-scroll.js`)
177
- Replaces clunky full-page jumping on mobile swipes with smooth continuous panning and physics-based inertia momentum.
178
- - **API**:
179
- ```ts
180
- AppGeneral.enableTouchScroll();
181
- AppGeneral.disableTouchScroll();
182
- AppGeneral.toggleTouchScroll(); // returns boolean
183
- AppGeneral.isTouchScrollEnabled(); // returns boolean
184
- AppGeneral.configureTouchScroll({ momentumFriction: 0.94 });
185
- ```
186
-
187
- ### 4. Cell Edit Modal (`components/CellEditModal/`)
188
- A responsive, high-feature editing sheet replacing legacy browser prompt popups:
189
- - **Text & Numeric Inputs**: Edit formulas (e.g. `=SUM(B4:C4)`), raw values, or labels.
190
- - **Formatting Presets**: Currency, Percent, Date, Time, Integer.
191
- - **Styling**: Bold, Italic, Alignment (Left, Center, Right).
192
- - **Colors**: 8 curated font colors and 7 cell background colors.
193
- - **Borders**: All, Top, Bottom, Left, Right with Solid/Dashed/Dotted styles and custom widths.
194
- - **Image Insertion**: Embed images into cells with automatic client-side compression (`imageCompressor.ts`).
195
- - **Fallback**: Calling `toggleCellEditModal(false)` automatically falls back to native inline input.
196
-
197
- ### 5. Horizontal Scroll Bar (`components/HorizontalScrollBar.tsx`)
198
- A dedicated, smooth horizontal control bar for navigating wide sheets:
199
- - Step buttons for next/prev columns.
200
- - Draggable slider track with live position indicator.
201
- - Automatically syncs with spreadsheet viewport when user scrolls or uses keyboard navigation.
202
-
203
- ### 6. Row Action Popover (`components/RowActionPopover/`)
204
- Context popup that triggers when user taps a row header:
205
- - **Insert Above**: Calls `executeSheetCommand("insertrow A" + rowNum)`.
206
- - **Insert Below**: Calls `executeSheetCommand("insertrow A" + (rowNum + 1))`.
207
- - **Delete Row**: Calls `executeSheetCommand("deleterow A" + rowNum)`.
208
-
209
- ### 7. Editable Cells Mode & Template Mapping (`modules/editable-cells.js`)
210
- Allows template authors to lock the spreadsheet so end-users can only edit designated cells:
211
- - **API**:
212
- ```ts
213
- AppGeneral.enableEditableCellsOnly();
214
- AppGeneral.disableEditableCellsOnly();
215
- AppGeneral.toggleEditableCellsOnly();
216
- AppGeneral.setAppMapping(mappingObj);
217
- ```
218
-
219
- ### 8. Plugin Manager (`modules/plugin-manager.js`)
220
- Allows developers to register custom plugins with lifecycle hooks:
221
- - **API**:
222
- ```ts
223
- AppGeneral.registerPlugin("myPlugin", {
224
- metadata: { displayName: "My Plugin", description: "Custom feature" },
225
- enable: () => { ... },
226
- disable: () => { ... },
227
- isEnabled: () => true,
228
- toggle: () => { ... },
229
- });
230
- ```
231
-
232
- ### 9. Cell Change & Status Listeners (`modules/listeners.js`)
233
- Safely intercepts cell updates and spreadsheet status cycles:
234
- - **API**:
235
- ```ts
236
- const unsubscribe = AppGeneral.setupCellChangeListener((coord: string) => {
237
- console.log("Cell modified:", coord);
238
- });
239
- ```
240
- - **Forwarding Safety**: Intercepts `SocialCalc.EditorSheetStatusCallback` while preserving all 4 internal parameters (`recalcdata, status, arg, editor`), avoiding undefined context dereference during render routines.
367
+ Adds mobile touch inertia scrolling:
368
+ ```ts
369
+ AppGeneral.enableTouchScroll();
370
+ AppGeneral.configureTouchScroll({
371
+ momentumFriction: 0.94, // adjust friction
372
+ velocityMultiplier: 1.2
373
+ });
374
+ AppGeneral.disableTouchScroll();
375
+ ```
376
+
377
+ ### 4. Horizontal Scroll (`modules/horizontal-scroll.js`)
378
+ Programmatic viewport scrolling:
379
+ ```ts
380
+ // Scroll horizontally by a relative number of columns
381
+ AppGeneral.scrollHorizontalBy(3);
382
+
383
+ // Scroll to a specific target column index (1-based)
384
+ AppGeneral.scrollToColumn(5); // Scrolls to column E
385
+
386
+ // Subscribe to horizontal scroll events
387
+ const unsubscribe = AppGeneral.subscribeHorizontalScroll((info) => {
388
+ console.log("Current first col:", info.currentFirstCol);
389
+ });
390
+ ```
391
+
392
+ ### 5. Editable Cells & Template Locks (`modules/editable-cells.js`)
393
+ Restricts user editing to designated cells while protecting formula and header cells:
394
+ ```ts
395
+ // Enable template protection mode
396
+ AppGeneral.enableEditableCellsOnly();
397
+
398
+ // Define allowed mapping
399
+ AppGeneral.setAppMapping({
400
+ sheet1: {
401
+ customerName: "B3",
402
+ itemsTotal: "D15"
403
+ }
404
+ });
405
+
406
+ // Check if a specific cell coordinate is editable
407
+ const canEdit = AppGeneral.isCellEditable(editor, "B3"); // true
408
+ const isLocked = AppGeneral.isCellEditable(editor, "A1"); // false
409
+ ```
410
+
411
+ ### 6. Plugin Manager (`modules/plugin-manager.js`)
412
+ Allows registration of custom lifecycle plugins:
413
+ ```ts
414
+ AppGeneral.registerPlugin("myCustomPlugin", {
415
+ metadata: {
416
+ displayName: "Audit Logger",
417
+ description: "Logs cell updates to an external service"
418
+ },
419
+ enable: () => console.log("Plugin activated"),
420
+ disable: () => console.log("Plugin deactivated"),
421
+ isEnabled: () => true
422
+ });
423
+ ```
424
+
425
+ ### 7. History (Undo / Redo) (`modules/history.js`)
426
+ ```ts
427
+ AppGeneral.undo();
428
+ AppGeneral.redo();
429
+ ```
430
+
431
+ ### 8. Exporters & Sheets (`modules/exporters.js`, `modules/sheets.js`)
432
+ ```ts
433
+ // Export sheet as CSV string
434
+ const csvData = AppGeneral.getCSVContent();
435
+
436
+ // Export sheet as HTML string
437
+ const htmlContent = AppGeneral.getCurrentHTMLContent();
438
+
439
+ // Get full spreadsheet data (save format / MSC)
440
+ const mscData = AppGeneral.getSpreadsheetContent();
441
+
442
+ // Multi-sheet operations
443
+ AppGeneral.switchSheet("Sheet2");
444
+ AppGeneral.addNewSheet("Expenses");
445
+ AppGeneral.deleteSheet("Sheet3");
446
+ AppGeneral.renameSheet("Sheet1", "Overview");
447
+ ```
448
+
449
+ ### 9. Invoice Utilities (`modules/invoice.js`)
450
+ ```ts
451
+ import { getInvoiceCoordinates } from "socialcalc-ai";
452
+
453
+ const coords = getInvoiceCoordinates();
454
+ // Returns standard coordinates for billTo, from, invoiceDetails, items, totals
455
+ ```
241
456
 
242
457
  ---
243
458
 
244
- ## 📡 Events Dispatched by SocialCalc
459
+ ## 📡 Event-Driven Architecture
460
+
461
+ `socialcalc-ai` broadcasts standard DOM CustomEvents on the `window` object. This allows any framework (React, Vue, Svelte, Angular, Vanilla JS) to react to spreadsheet changes:
245
462
 
246
- | Event Name | `detail` Payload | Description |
463
+ | Event Name | `event.detail` Structure | Purpose |
247
464
  | :--- | :--- | :--- |
248
- | `socialcalc:cell-edit-request` | `{ coord, text, okfn, cleanup }` | Dispatched when a cell is clicked/tapped (consumed by `CellEditModal`). |
249
- | `socialcalc:row-header-click` | `{ rowNum, clientX, clientY }` | Dispatched when a row number is clicked (consumed by `RowActionPopover`). |
250
- | `socialcalc:cell-change` | `{ coord, value }` | Dispatched when any cell value is successfully modified. |
251
- | `socialcalc:horizontal-scroll`| `{ currentFirstCol, currentColName, totalCols }` | Dispatched when sheet column scrolls. |
252
- | `socialcalc:plugin-change` | `{ pluginName, state, allStates }` | Dispatched when any plugin is enabled/disabled. |
465
+ | `socialcalc:cell-edit-request` | `{ coord: string, text: string, okfn: Function, cleanup: Function }` | Fired when a cell is clicked/tapped. Consumed by `CellEditModal`. |
466
+ | `socialcalc:row-header-click` | `{ rowNum: number, originalEvent: MouseEvent }` | Fired when a row number index is clicked. Consumed by `RowActionPopover`. |
467
+ | `socialcalc:cell-change` | `{ coord: string, value: string }` | Fired after a cell's value or formula is committed and recalculated. |
468
+ | `socialcalc:horizontal-scroll`| `{ currentFirstCol: number, currentColName: string, totalCols: number }` | Fired whenever the horizontal viewport column changes. |
469
+ | `socialcalc:plugin-change` | `{ pluginName: string, state: boolean, allStates: Record<string, boolean> }` | Fired whenever a plugin is registered, enabled, or disabled. |
470
+
471
+ ### Event Listener Example:
472
+
473
+ ```ts
474
+ window.addEventListener("socialcalc:cell-change", (e: CustomEvent) => {
475
+ const { coord, value } = e.detail;
476
+ console.log(`Cell ${coord} changed to: ${value}`);
477
+ });
478
+ ```
253
479
 
254
480
  ---
255
481
 
256
- ## 🧪 Testing & Verification
482
+ ## 🤖 AI & MultiSheet Calc (MSC) Integration
257
483
 
258
- Run tests anytime you modify the package:
259
- ```bash
260
- # Run Vitest test suite
261
- npm test -- --run
484
+ `socialcalc-ai` is built for automated and LLM-driven spreadsheet workflows:
485
+
486
+ ### Generating Spreadsheets via LLMs
487
+ AI agents can generate spreadsheet structures in SocialCalc MSC format (MultiSheet Calc). An MSC document defines multiple named sheets with cell formulas, formats, and values:
488
+
489
+ ```json
490
+ {
491
+ "sheets": {
492
+ "Summary": {
493
+ "content": "cell:A1:t:Quarterly Revenue\ncell:A3:t:Q1\ncell:B3:v:12500\ncell:A4:t:Q2\ncell:B4:v:14200\ncell:A5:t:Total\ncell:B5:v#:=SUM(B3:B4)"
494
+ }
495
+ }
496
+ }
497
+ ```
262
498
 
263
- # Run TypeScript compilation & Vite build
264
- npm run build
499
+ Load this directly into `socialcalc-ai`:
265
500
 
266
- # Verify SocialCalc core modularization in Node
267
- node test-socialcalc.js
501
+ ```ts
502
+ import * as AppGeneral from "socialcalc-ai";
503
+
504
+ AppGeneral.loadWorkbookData(aiGeneratedMscJson);
268
505
  ```
506
+
507
+ ---
508
+
509
+ ## 🛡 TypeScript Support
510
+
511
+ `socialcalc-ai` ships with full type definitions in `index.d.ts`:
512
+
513
+ ```ts
514
+ import * as AppGeneral from "socialcalc-ai";
515
+ import {
516
+ CellEditModal,
517
+ CellEditModalProps,
518
+ HorizontalScrollBarProps,
519
+ RowActionPopoverProps
520
+ } from "socialcalc-ai";
521
+ ```
522
+
523
+ ---
524
+
525
+ ## 💻 Browser & Framework Compatibility
526
+
527
+ - **Browsers**: Chrome, Edge, Safari, Firefox (Desktop & Mobile).
528
+ - **Frameworks**: React 18+, Ionic React 7+, Next.js (Client Component), Vite, Plain JavaScript.
529
+ - **Runtimes**: Node.js 18+ (for headless calculation & CSV/MSC transformation).
530
+
531
+ ---
532
+
533
+ ## 📦 Release Notes
534
+
535
+ ### v1.0.2
536
+ - **Cell Edit Modal Fix**: Fixed cell click interceptor and mouse delegates so clicking any cell reliably triggers the `CellEditModal` bottom sheet.
537
+ - **Scroll Distortion Fix**: Resolved table border collapse and column misalignment when scrolling past multi-row (`rowspan`) and multi-column (`colspan`) blocks.
538
+ - **Formula Support**: Enhanced modal editing to support formula values (`=SUM(...)`) seamlessly alongside raw numbers, text, and HTML formatting.
539
+ - **Strict Column Sizing**: Enforced CSS pixel widths and `box-sizing: border-box` containment across `<colgroup>` and sizing rows to preserve layout integrity.
540
+ - **Safety Guards**: Added null guards and try/catch protection across SocialCalc workbook control callbacks.
541
+
542
+ ---
543
+
544
+ ## 📜 License & Acknowledgments
545
+
546
+ - **Original SocialCalc Engine**: Created by Dan Bricklin (co-creator of VisiCalc) and Socialtext.
547
+ - **Modernized `socialcalc-ai` Package**: Maintained and modernized for ES6 modules, React/Ionic UI components, touch physics, and AI workflows by **Anirudh Sharma** under the **MIT License**.
548
+
549
+ Feel free to open issues and pull requests to help enhance the modern spreadsheet engine!
package/core/core.js CHANGED
@@ -5186,6 +5186,7 @@
5186
5186
  if (context.showRCHeaders) {
5187
5187
  newcol = document.createElement("col");
5188
5188
  newcol.width = context.rownamewidth;
5189
+ newcol.style.width = context.rownamewidth + "px";
5189
5190
  result.appendChild(newcol);
5190
5191
  }
5191
5192
 
@@ -5197,12 +5198,16 @@
5197
5198
  ) {
5198
5199
  newcol = document.createElement("col");
5199
5200
  t = context.colwidth[colnum];
5200
- if (t) newcol.width = t;
5201
+ if (t) {
5202
+ newcol.width = t;
5203
+ newcol.style.width = t + "px";
5204
+ }
5201
5205
  result.appendChild(newcol);
5202
5206
  }
5203
5207
  if (colpane < context.colpanes.length - 1) {
5204
5208
  newcol = document.createElement("col");
5205
5209
  newcol.width = context.defaultpanedividerwidth;
5210
+ newcol.style.width = context.defaultpanedividerwidth + "px";
5206
5211
  result.appendChild(newcol);
5207
5212
  }
5208
5213
  }
@@ -5218,6 +5223,8 @@
5218
5223
  if (context.showRCHeaders) {
5219
5224
  newcell = document.createElement("td");
5220
5225
  newcell.style.width = context.rownamewidth + "px";
5226
+ newcell.style.minWidth = context.rownamewidth + "px";
5227
+ newcell.style.maxWidth = context.rownamewidth + "px";
5221
5228
  newcell.height = "1";
5222
5229
  result.appendChild(newcell);
5223
5230
  }
@@ -5230,13 +5237,21 @@
5230
5237
  ) {
5231
5238
  newcell = document.createElement("td");
5232
5239
  t = context.colwidth[colnum];
5233
- if (t) newcell.width = t;
5240
+ if (t) {
5241
+ newcell.width = t;
5242
+ newcell.style.width = t + "px";
5243
+ newcell.style.minWidth = t + "px";
5244
+ newcell.style.maxWidth = t + "px";
5245
+ }
5234
5246
  newcell.height = "1";
5235
5247
  result.appendChild(newcell);
5236
5248
  }
5237
5249
  if (colpane < context.colpanes.length - 1) {
5238
5250
  newcell = document.createElement("td");
5239
5251
  newcell.width = context.defaultpanedividerwidth;
5252
+ newcell.style.width = context.defaultpanedividerwidth + "px";
5253
+ newcell.style.minWidth = context.defaultpanedividerwidth + "px";
5254
+ newcell.style.maxWidth = context.defaultpanedividerwidth + "px";
5240
5255
  newcell.height = "1";
5241
5256
  result.appendChild(newcell);
5242
5257
  }
@@ -5289,8 +5304,8 @@
5289
5304
  if (rownum === firstVisibleRow && colnum === firstVisibleCol) {
5290
5305
  // This is the visible start of the scrolled spanned cell in this pane!
5291
5306
  isSpanContinuation = true;
5292
- spanColSpan = Math.min(spanEndCol, panecollimits ? panecollimits.last : spanEndCol) - firstVisibleCol + 1;
5293
- spanRowSpan = Math.min(spanEndRow, panerowlimits ? panerowlimits.last : spanEndRow) - firstVisibleRow + 1;
5307
+ spanColSpan = Math.max(1, Math.min(spanEndCol, panecollimits ? panecollimits.last : spanEndCol) - firstVisibleCol + 1);
5308
+ spanRowSpan = Math.max(1, Math.min(spanEndRow, panerowlimits ? panerowlimits.last : spanEndRow) - firstVisibleRow + 1);
5294
5309
  cell = originCell;
5295
5310
  } else {
5296
5311
  // Covered by the visible span in this pane -- skip
@@ -5416,6 +5431,9 @@
5416
5431
  } else stylestr += "text-align:left;";
5417
5432
  }
5418
5433
 
5434
+ var activeColSpan = isSpanContinuation ? spanColSpan : (result.colSpan || cell.colspan || 1);
5435
+ var activeRowSpan = isSpanContinuation ? spanRowSpan : (result.rowSpan || cell.rowspan || 1);
5436
+
5419
5437
  num = cell.bt;
5420
5438
  if (num && (!isSpanContinuation || (originCR && rownum === originCR.row))) {
5421
5439
  stylestr += "border-top:" + sheetobj.borderstyles[num] + ";";
@@ -5428,12 +5446,12 @@
5428
5446
  if (
5429
5447
  context.CellInPane(
5430
5448
  rownum,
5431
- colnum + (isSpanContinuation ? spanColSpan : (cell.colspan || 1)),
5449
+ colnum + activeColSpan,
5432
5450
  rowpane,
5433
5451
  colpane
5434
5452
  )
5435
5453
  )
5436
- t = SocialCalc.crToCoord(colnum + (isSpanContinuation ? spanColSpan : (cell.colspan || 1)), rownum);
5454
+ t = SocialCalc.crToCoord(colnum + activeColSpan, rownum);
5437
5455
  else t = "nomatch";
5438
5456
  if (context.cellskip[t]) t = context.cellskip[t];
5439
5457
  if (!sheetobj.cells[t] || !sheetobj.cells[t].bl)
@@ -5446,13 +5464,13 @@
5446
5464
  } else if (context.showGrid) {
5447
5465
  if (
5448
5466
  context.CellInPane(
5449
- rownum + (isSpanContinuation ? spanRowSpan : (cell.rowspan || 1)),
5467
+ rownum + activeRowSpan,
5450
5468
  colnum,
5451
5469
  rowpane,
5452
5470
  colpane
5453
5471
  )
5454
5472
  )
5455
- t = SocialCalc.crToCoord(colnum, rownum + (isSpanContinuation ? spanRowSpan : (cell.rowspan || 1)));
5473
+ t = SocialCalc.crToCoord(colnum, rownum + activeRowSpan);
5456
5474
  else t = "nomatch";
5457
5475
  if (context.cellskip[t]) t = context.cellskip[t];
5458
5476
  if (!sheetobj.cells[t] || !sheetobj.cells[t].bt)
@@ -5483,6 +5501,8 @@
5483
5501
  }
5484
5502
 
5485
5503
  result.style.cssText = stylestr;
5504
+ result.style.boxSizing = "border-box";
5505
+ result.style.overflow = "hidden";
5486
5506
 
5487
5507
  //!!!!!!!!!
5488
5508
  // NOTE: csss and cssc are not supported yet.
@@ -7014,8 +7014,13 @@ SocialCalc.SpreadsheetCmdTable = {
7014
7014
  // this is for checkmark toggling
7015
7015
  SocialCalc.Callbacks.ToggleCell = function (cellname) {
7016
7016
  var control = SocialCalc.GetCurrentWorkBookControl();
7017
+ if (!control || !control.currentSheetButton || !control.workbook || !control.workbook.sheetArr) {
7018
+ return;
7019
+ }
7017
7020
  var sheetid = control.currentSheetButton.id;
7021
+ if (!control.workbook.sheetArr[sheetid]) return;
7018
7022
  var sheetobj = control.workbook.sheetArr[sheetid].sheet;
7023
+ if (!sheetobj || !sheetobj.cells) return;
7019
7024
  var cell = sheetobj.cells[cellname];
7020
7025
  var sheetname = sheetobj.sheetname;
7021
7026
 
@@ -1582,22 +1582,33 @@
1582
1582
  mouseinfo.registeredElements.push({ element: element, editor: editor });
1583
1583
  }
1584
1584
 
1585
+ if (!SocialCalc.ProcessEditorMouseDownHandler) {
1586
+ SocialCalc.ProcessEditorMouseDownHandler = function (e) {
1587
+ return SocialCalc.ProcessEditorMouseDown(e);
1588
+ };
1589
+ }
1590
+ if (!SocialCalc.ProcessEditorDblClickHandler) {
1591
+ SocialCalc.ProcessEditorDblClickHandler = function (e) {
1592
+ return SocialCalc.ProcessEditorDblClick(e);
1593
+ };
1594
+ }
1595
+
1585
1596
  if (element.addEventListener) {
1586
1597
  // DOM Level 2 -- Firefox, et al
1587
1598
  element.addEventListener(
1588
1599
  "mousedown",
1589
- SocialCalc.ProcessEditorMouseDown,
1600
+ SocialCalc.ProcessEditorMouseDownHandler,
1590
1601
  false
1591
1602
  );
1592
1603
  element.addEventListener(
1593
1604
  "dblclick",
1594
- SocialCalc.ProcessEditorDblClick,
1605
+ SocialCalc.ProcessEditorDblClickHandler,
1595
1606
  false
1596
1607
  );
1597
1608
  } else if (element.attachEvent) {
1598
1609
  // IE 5+
1599
- element.attachEvent("onmousedown", SocialCalc.ProcessEditorMouseDown);
1600
- element.attachEvent("ondblclick", SocialCalc.ProcessEditorDblClick);
1610
+ element.attachEvent("onmousedown", SocialCalc.ProcessEditorMouseDownHandler);
1611
+ element.attachEvent("ondblclick", SocialCalc.ProcessEditorDblClickHandler);
1601
1612
  } else {
1602
1613
  // don't handle this
1603
1614
  throw "Browser not supported";
@@ -1629,21 +1640,21 @@
1629
1640
  // DOM Level 2
1630
1641
  oldelement.removeEventListener(
1631
1642
  "mousedown",
1632
- SocialCalc.ProcessEditorMouseDown,
1643
+ SocialCalc.ProcessEditorMouseDownHandler || SocialCalc.ProcessEditorMouseDown,
1633
1644
  false
1634
1645
  );
1635
1646
  oldelement.removeEventListener(
1636
1647
  "dblclick",
1637
- SocialCalc.ProcessEditorDblClick,
1648
+ SocialCalc.ProcessEditorDblClickHandler || SocialCalc.ProcessEditorDblClick,
1638
1649
  false
1639
1650
  );
1640
1651
  } else if (oldelement.detachEvent) {
1641
1652
  // IE
1642
1653
  oldelement.detachEvent(
1643
1654
  "onmousedown",
1644
- SocialCalc.ProcessEditorMouseDown
1655
+ SocialCalc.ProcessEditorMouseDownHandler || SocialCalc.ProcessEditorMouseDown
1645
1656
  );
1646
- oldelement.detachEvent("ondblclick", SocialCalc.ProcessEditorDblClick);
1657
+ oldelement.detachEvent("ondblclick", SocialCalc.ProcessEditorDblClickHandler || SocialCalc.ProcessEditorDblClick);
1647
1658
  }
1648
1659
  mouseinfo.registeredElements.splice(i, 1);
1649
1660
  }
@@ -3772,18 +3783,6 @@
3772
3783
  return;
3773
3784
  }
3774
3785
 
3775
- if ((vamount == 1 || vamount == -1) && hamount == 0) {
3776
- // special case quick scrolls
3777
- if (vamount == 1) {
3778
- editor.ScrollTableUpOneRow();
3779
- } else {
3780
- editor.ScrollTableDownOneRow();
3781
- }
3782
- if (editor.ecell) editor.SetECellHeaders("selected");
3783
- editor.SchedulePositionCalculations();
3784
- return;
3785
- }
3786
-
3787
3786
  // Do a gross move and render
3788
3787
 
3789
3788
  if (vamount != 0 || hamount != 0) {
@@ -3874,215 +3873,13 @@
3874
3873
  };
3875
3874
 
3876
3875
  SocialCalc.ScrollTableUpOneRow = function (editor) {
3877
- var toprow,
3878
- rowpane,
3879
- rownum,
3880
- colnum,
3881
- colpane,
3882
- cell,
3883
- oldrownum,
3884
- maxspan,
3885
- newbottomrow,
3886
- newrow,
3887
- oldchild,
3888
- bottomrownum;
3889
- var rowneedsrefresh = {};
3890
-
3891
- var context = editor.context;
3892
- var sheetobj = context.sheetobj;
3893
- var tableobj = editor.fullgrid;
3894
-
3895
- var tbodyobj;
3896
-
3897
- tbodyobj = tableobj.lastChild;
3898
-
3899
- toprow = context.showRCHeaders ? 2 : 1;
3900
- for (rowpane = 0; rowpane < context.rowpanes.length - 1; rowpane++) {
3901
- toprow +=
3902
- context.rowpanes[rowpane].last - context.rowpanes[rowpane].first + 2; // skip pane and spacing row
3903
- }
3904
-
3905
- tbodyobj.removeChild(tbodyobj.childNodes[toprow]);
3906
-
3907
- context.rowpanes[rowpane].first++;
3908
- context.rowpanes[rowpane].last++;
3909
- editor.FitToEditTable();
3910
- context.CalculateColWidthData(); // Just in case, since normally done in RenderSheet
3911
-
3912
- newbottomrow = context.RenderRow(context.rowpanes[rowpane].last, rowpane);
3913
- tbodyobj.appendChild(newbottomrow);
3914
-
3915
- // if scrolled off a row with starting rowspans, replace rows for the largest rowspan
3916
-
3917
- var maxrowspan = 1;
3918
- oldrownum = context.rowpanes[rowpane].first - 1;
3919
-
3920
- for (colpane = 0; colpane < context.colpanes.length; colpane++) {
3921
- for (
3922
- colnum = context.colpanes[colpane].first;
3923
- colnum <= context.colpanes[colpane].last;
3924
- colnum++
3925
- ) {
3926
- var coord = SocialCalc.crToCoord(colnum, oldrownum);
3927
- if (context.cellskip[coord]) continue;
3928
- cell = sheetobj.cells[coord];
3929
- if (cell && cell.rowspan > maxrowspan) maxrowspan = cell.rowspan;
3930
- }
3931
- }
3932
-
3933
- if (maxrowspan > 1) {
3934
- for (rownum = 1; rownum < maxrowspan; rownum++) {
3935
- if (rownum + oldrownum >= context.rowpanes[rowpane].last) break;
3936
- newrow = context.RenderRow(rownum + oldrownum, rowpane);
3937
- oldchild = tbodyobj.childNodes[toprow + rownum - 1];
3938
- tbodyobj.replaceChild(newrow, oldchild);
3939
- }
3940
- }
3941
-
3942
- // if added a row that includes rowspans from above, update the size of those to include new row
3943
-
3944
- bottomrownum = context.rowpanes[rowpane].last;
3945
-
3946
- for (colpane = 0; colpane < context.colpanes.length; colpane++) {
3947
- for (
3948
- colnum = context.colpanes[colpane].first;
3949
- colnum <= context.colpanes[colpane].last;
3950
- colnum++
3951
- ) {
3952
- coord = context.cellskip[SocialCalc.crToCoord(colnum, bottomrownum)];
3953
- if (!coord) continue; // only look at spanned cells
3954
- rownum = context.coordToCR[coord].row - 0;
3955
- if (
3956
- rownum == context.rowpanes[rowpane].last ||
3957
- rownum < context.rowpanes[rowpane].first
3958
- )
3959
- continue; // this row (colspan) or starts above pane
3960
- cell = sheetobj.cells[coord];
3961
- if (cell && cell.rowspan > 1) rowneedsrefresh[rownum] = true; // remember row num to update
3962
- }
3963
- }
3964
-
3965
- for (rownum in rowneedsrefresh) {
3966
- newrow = context.RenderRow(rownum, rowpane);
3967
- oldchild =
3968
- tbodyobj.childNodes[
3969
- toprow + (rownum - context.rowpanes[rowpane].first)
3970
- ];
3971
- tbodyobj.replaceChild(newrow, oldchild);
3972
- }
3973
-
3974
- return tableobj;
3876
+ SocialCalc.ScrollRelativeBoth(editor, 1, 0);
3877
+ return editor.fullgrid;
3975
3878
  };
3976
3879
 
3977
3880
  SocialCalc.ScrollTableDownOneRow = function (editor) {
3978
- var toprow,
3979
- rowpane,
3980
- rownum,
3981
- colnum,
3982
- colpane,
3983
- cell,
3984
- newrownum,
3985
- maxspan,
3986
- newbottomrow,
3987
- newrow,
3988
- oldchild,
3989
- bottomrownum,
3990
- maxrowspan,
3991
- coord;
3992
- var rowneedsrefresh = {};
3993
-
3994
- var context = editor.context;
3995
- var sheetobj = context.sheetobj;
3996
- var tableobj = editor.fullgrid;
3997
-
3998
- var tbodyobj;
3999
-
4000
- tbodyobj = tableobj.lastChild;
4001
-
4002
- toprow = context.showRCHeaders ? 2 : 1;
4003
- for (rowpane = 0; rowpane < context.rowpanes.length - 1; rowpane++) {
4004
- toprow +=
4005
- context.rowpanes[rowpane].last - context.rowpanes[rowpane].first + 2; // skip pane and spacing row
4006
- }
4007
-
4008
- tbodyobj.removeChild(
4009
- tbodyobj.childNodes[
4010
- toprow +
4011
- (context.rowpanes[rowpane].last - context.rowpanes[rowpane].first)
4012
- ]
4013
- );
4014
-
4015
- context.rowpanes[rowpane].first--;
4016
- context.rowpanes[rowpane].last--;
4017
- editor.FitToEditTable();
4018
- context.CalculateColWidthData(); // Just in case, since normally done in RenderSheet
4019
-
4020
- newrow = context.RenderRow(context.rowpanes[rowpane].first, rowpane);
4021
- tbodyobj.insertBefore(newrow, tbodyobj.childNodes[toprow]);
4022
-
4023
- // if inserted a row with starting rowspans, replace rows for the largest rowspan
4024
-
4025
- maxrowspan = 1;
4026
- newrownum = context.rowpanes[rowpane].first;
4027
-
4028
- for (colpane = 0; colpane < context.colpanes.length; colpane++) {
4029
- for (
4030
- colnum = context.colpanes[colpane].first;
4031
- colnum <= context.colpanes[colpane].last;
4032
- colnum++
4033
- ) {
4034
- coord = SocialCalc.crToCoord(colnum, newrownum);
4035
- if (context.cellskip[coord]) continue;
4036
- cell = sheetobj.cells[coord];
4037
- if (cell && cell.rowspan > maxrowspan) maxrowspan = cell.rowspan;
4038
- }
4039
- }
4040
-
4041
- if (maxrowspan > 1) {
4042
- for (rownum = 1; rownum < maxrowspan; rownum++) {
4043
- if (rownum + newrownum > context.rowpanes[rowpane].last) break;
4044
- newrow = context.RenderRow(rownum + newrownum, rowpane);
4045
- oldchild = tbodyobj.childNodes[toprow + rownum];
4046
- tbodyobj.replaceChild(newrow, oldchild);
4047
- }
4048
- }
4049
-
4050
- // if last row now includes rowspans or rowspans from above, update the size of those to remove deleted row
4051
-
4052
- bottomrownum = context.rowpanes[rowpane].last;
4053
-
4054
- for (colpane = 0; colpane < context.colpanes.length; colpane++) {
4055
- for (
4056
- colnum = context.colpanes[colpane].first;
4057
- colnum <= context.colpanes[colpane].last;
4058
- colnum++
4059
- ) {
4060
- coord = SocialCalc.crToCoord(colnum, bottomrownum);
4061
- cell = sheetobj.cells[coord];
4062
- if (cell && cell.rowspan > 1) {
4063
- rowneedsrefresh[bottomrownum] = true; // need to update this row
4064
- continue;
4065
- }
4066
- coord = context.cellskip[SocialCalc.crToCoord(colnum, bottomrownum)];
4067
- if (!coord) continue; // only look at spanned cells
4068
- rownum = context.coordToCR[coord].row - 0;
4069
- if (rownum == bottomrownum || rownum < context.rowpanes[rowpane].first)
4070
- continue; // this row (colspan) or starts above pane
4071
- cell = sheetobj.cells[coord];
4072
- if (cell && cell.rowspan > 1) rowneedsrefresh[rownum] = true; // remember row num to update
4073
- }
4074
- }
4075
-
4076
- for (rownum in rowneedsrefresh) {
4077
- newrow = context.RenderRow(rownum, rowpane);
4078
- oldchild =
4079
- tbodyobj.childNodes[
4080
- toprow + (rownum - context.rowpanes[rowpane].first)
4081
- ];
4082
- tbodyobj.replaceChild(newrow, oldchild);
4083
- }
4084
-
4085
- return tableobj;
3881
+ SocialCalc.ScrollRelativeBoth(editor, -1, 0);
3882
+ return editor.fullgrid;
4086
3883
  };
4087
3884
 
4088
3885
  // *************************************
package/modules/init.js CHANGED
@@ -8,9 +8,10 @@ if (typeof window !== "undefined" && window.SocialCalc) {
8
8
  SocialCalc = global.SocialCalc;
9
9
  } else {
10
10
  console.error("SocialCalc not found in global scope");
11
- SocialCalc = {}; // Fallback to prevent errors
12
11
  }
13
12
 
13
+ import { setupMouseListener } from "./listeners.js";
14
+
14
15
  export function initializeApp(data) {
15
16
  /* Initializes the spreadsheet */
16
17
  // Try multiple candidate IDs so the renderer can namespace its elements
@@ -91,4 +92,5 @@ export function initializeApp(data) {
91
92
  }
92
93
 
93
94
  spreadsheet.DoOnResize();
95
+ setupMouseListener();
94
96
  }
@@ -30,6 +30,7 @@ export function enableCellEditModal() {
30
30
  if (typeof window !== "undefined" && window.SocialCalc) {
31
31
  window.SocialCalc.isCellEditModalEnabled = () => true;
32
32
  }
33
+ setupMouseListener();
33
34
  }
34
35
 
35
36
  /**
@@ -57,6 +58,9 @@ export function toggleCellEditModal(show) {
57
58
  if (typeof window !== "undefined" && window.SocialCalc) {
58
59
  window.SocialCalc.isCellEditModalEnabled = () => _cellEditModalEnabled;
59
60
  }
61
+ if (_cellEditModalEnabled) {
62
+ setupMouseListener();
63
+ }
60
64
  return _cellEditModalEnabled;
61
65
  }
62
66
 
@@ -167,7 +171,15 @@ export function setupMouseListener() {
167
171
  if (typeof window !== "undefined" && window.SocialCalc) {
168
172
  SocialCalc = window.SocialCalc;
169
173
  }
170
- if (!SocialCalc || !SocialCalc.EditorMouseInfo) return;
174
+ if (!SocialCalc || !SocialCalc.EditorMouseInfo) {
175
+ if (typeof window !== "undefined") {
176
+ setTimeout(setupMouseListener, 50);
177
+ }
178
+ return;
179
+ }
180
+
181
+ if (SocialCalc._customMouseListenerInstalled) return;
182
+ SocialCalc._customMouseListenerInstalled = true;
171
183
 
172
184
  const originalDblClick = SocialCalc.ProcessEditorDblClick;
173
185
 
@@ -292,7 +304,11 @@ export function setupMouseListener() {
292
304
 
293
305
  // Toggle Cell Callback
294
306
  if (SocialCalc.Callbacks && SocialCalc.Callbacks.ToggleCell) {
295
- SocialCalc.Callbacks.ToggleCell(result.coord);
307
+ try {
308
+ SocialCalc.Callbacks.ToggleCell(result.coord);
309
+ } catch (err) {
310
+ // Ignore toggle cell errors in custom environments
311
+ }
296
312
  }
297
313
 
298
314
  // Move ECell (Updates Selection)
@@ -367,7 +383,7 @@ export function setupMouseListener() {
367
383
  const originalKeyPress = SocialCalc.ProcessKeyPress;
368
384
 
369
385
  SocialCalc.ProcessKeyDown = function (e) {
370
- const isModalOpen = !!document.querySelector(".sc-cell-edit-backdrop");
386
+ const isModalOpen = !!document.querySelector(".sc-cell-modal-backdrop, .sc-cell-edit-backdrop, .sc-modal-container");
371
387
  const activeElement = document.activeElement;
372
388
  const isTypingInInput =
373
389
  activeElement &&
@@ -386,7 +402,7 @@ export function setupMouseListener() {
386
402
  };
387
403
 
388
404
  SocialCalc.ProcessKeyPress = function (e) {
389
- const isModalOpen = !!document.querySelector(".sc-cell-edit-backdrop");
405
+ const isModalOpen = !!document.querySelector(".sc-cell-modal-backdrop, .sc-cell-edit-backdrop, .sc-modal-container");
390
406
  const activeElement = document.activeElement;
391
407
  const isTypingInInput =
392
408
  activeElement &&
@@ -416,8 +432,8 @@ function triggerCustomModal(editor) {
416
432
  if (!editor || !editor.ecell) return;
417
433
 
418
434
  const coord = editor.ecell.coord;
419
- const sheet = editor.context.sheetobj;
420
- const cell = sheet.GetAssuredCell(coord);
435
+ const sheet = editor.context ? editor.context.sheetobj : null;
436
+ const cell = sheet && sheet.GetAssuredCell ? sheet.GetAssuredCell(coord) : null;
421
437
 
422
438
  // Hide the native input box and cancel its timer
423
439
  if (editor.inputBox) {
@@ -442,25 +458,35 @@ function triggerCustomModal(editor) {
442
458
 
443
459
  // Determine initial text
444
460
  let initialText = "";
445
- if (cell.datavalue !== undefined && cell.datavalue !== null) {
446
- initialText = String(cell.datavalue);
447
- } else if (cell.displaystring !== undefined && cell.displaystring !== null) {
448
- initialText = String(cell.displaystring);
461
+ if (cell) {
462
+ if (cell.formula) {
463
+ initialText = "=" + cell.formula;
464
+ } else if (cell.datavalue !== undefined && cell.datavalue !== null) {
465
+ initialText = String(cell.datavalue);
466
+ } else if (cell.displaystring !== undefined && cell.displaystring !== null) {
467
+ initialText = String(cell.displaystring);
468
+ }
469
+ }
470
+ if (!initialText && SocialCalc && SocialCalc.GetCellContents && sheet) {
471
+ initialText = SocialCalc.GetCellContents(sheet, coord) || "";
449
472
  }
450
473
 
451
474
  // Callback when user confirms edit in Modal
452
475
  const handleOk = (newValue) => {
453
476
  let cmd = "";
454
- const isNum = !isNaN(Number(newValue)) && newValue.trim() !== "";
455
-
456
- if (isNum) {
457
- cmd = `set ${coord} value n ${newValue}`;
477
+ if (typeof newValue === "string" && newValue.startsWith("=")) {
478
+ cmd = `set ${coord} formula ${newValue.slice(1)}`;
458
479
  } else {
459
- const isHtml = /<[a-z][\s\S]*>/i.test(newValue);
460
- if (isHtml) {
461
- cmd = `set ${coord} text th ${newValue}\nset ${coord} textvalueformat text-html`;
480
+ const isNum = !isNaN(Number(newValue)) && newValue.trim() !== "";
481
+ if (isNum) {
482
+ cmd = `set ${coord} value n ${newValue}`;
462
483
  } else {
463
- cmd = `set ${coord} text t ${newValue}`;
484
+ const isHtml = /<[a-z][\s\S]*>/i.test(newValue);
485
+ if (isHtml) {
486
+ cmd = `set ${coord} text th ${newValue}\nset ${coord} textvalueformat text-html`;
487
+ } else {
488
+ cmd = `set ${coord} text t ${newValue}`;
489
+ }
464
490
  }
465
491
  }
466
492
 
@@ -529,3 +555,8 @@ export function setupCellChangeListener(callback) {
529
555
  };
530
556
  }
531
557
 
558
+ // Auto-initialize mouse listener if in browser environment
559
+ if (typeof window !== "undefined") {
560
+ setupMouseListener();
561
+ }
562
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socialcalc-ai",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Modernized, modular spreadsheet calculation engine and React/Ionic UI suite with AI formula and template capabilities.",
5
5
  "type": "module",
6
6
  "main": "index.js",