diff --git a/packages/super-editor/src/index.d.ts b/packages/super-editor/src/index.d.ts index 5e21c11318..862a4c8ff7 100644 --- a/packages/super-editor/src/index.d.ts +++ b/packages/super-editor/src/index.d.ts @@ -77,7 +77,20 @@ export type Command = (props: CommandProps) => boolean; * Chainable command object returned by editor.chain() */ export interface ChainableCommandObject { - run: () => boolean; + /** Execute the chained commands */ + run(): boolean; + /** Chain any command - returns self for further chaining */ + toggleBold(): ChainableCommandObject; + toggleItalic(): ChainableCommandObject; + toggleUnderline(): ChainableCommandObject; + toggleStrike(): ChainableCommandObject; + setFontSize(size: string | number): ChainableCommandObject; + setFontFamily(family: string): ChainableCommandObject; + setTextColor(color: string): ChainableCommandObject; + setTextAlign(alignment: 'left' | 'center' | 'right' | 'justify'): ChainableCommandObject; + insertContent(content: any): ChainableCommandObject; + focus(position?: 'start' | 'end' | 'all' | number | boolean | null): ChainableCommandObject; + /** Allow any other command */ [commandName: string]: ((...args: any[]) => ChainableCommandObject) | (() => boolean); } @@ -199,6 +212,67 @@ export interface OpenOptions { fonts?: Record; } +// ============================================ +// COMMENT TYPES +// ============================================ + +/** A comment element (paragraph, text run, etc.) */ +export interface CommentElement { + type: string; + content?: CommentElement[]; + text?: string; +} + +/** A comment in the document */ +export interface Comment { + /** Unique comment identifier */ + commentId: string; + /** Timestamp when comment was created */ + createdTime: number; + /** Email of the comment author */ + creatorEmail: string; + /** Display name of the comment author */ + creatorName: string; + /** Comment content elements */ + elements: CommentElement[]; + /** Original ID from imported document */ + importedId?: string; + /** Whether the comment is resolved */ + isDone: boolean; + /** Parent comment ID for replies */ + parentCommentId?: string; + /** Raw JSON representation */ + commentJSON?: unknown; +} + +/** Event data for comments loaded event */ +export interface CommentsLoadedEventData { + comments: Comment[]; +} + +/** Event data for content error event */ +export interface ContentErrorEventData { + error: Error; +} + +/** Font configuration */ +export interface FontConfig { + key: string; + label: string; + fontWeight?: number; + props?: { + style?: { + fontFamily?: string; + }; + }; +} + +/** Font support information */ +export interface FontSupportInfo { + documentFonts: string[]; + unsupportedFonts: string[]; +} + // ============================================ // PRESENTATION EDITOR TYPES // ============================================ @@ -586,6 +660,131 @@ export declare class Editor { */ isEmpty: boolean; + // ============================================ + // EVENT METHODS + // ============================================ + + /** + * Register an event listener. + * @param event - Event name ('update', 'create', 'transaction', etc.) + * @param handler - Event handler function + */ + on(event: string, handler: (...args: any[]) => void): void; + + /** + * Remove an event listener. + * @param event - Event name + * @param handler - Event handler function to remove + */ + off(event: string, handler: (...args: any[]) => void): void; + + /** + * Emit an event. + * @param event - Event name + * @param args - Arguments to pass to handlers + */ + emit(event: string, ...args: any[]): void; + + // ============================================ + // DOCUMENT EXPORT METHODS + // ============================================ + + /** + * Export the document to DOCX format. + * @param options - Export options + * @returns Promise resolving to Blob (browser) or Buffer (Node.js) + */ + exportDocx(options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + fieldsHighlightColor?: string | null; + compression?: 'DEFLATE' | 'STORE'; + }): Promise; + + /** + * Export the document (alias for exportDocx). + */ + exportDocument(options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + }): Promise; + + /** + * Save the document to the original source path (Node.js only). + */ + save(options?: { isFinalDoc?: boolean; commentsType?: string; comments?: Comment[] }): Promise; + + /** + * Save the document to a specific path (Node.js only). + */ + saveTo( + path: string, + options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + }, + ): Promise; + + // ============================================ + // TOOLBAR & UI METHODS + // ============================================ + + /** + * Set the toolbar for this editor. + */ + setToolbar(toolbar: any): void; + + /** + * Set whether the editor is editable. + */ + setEditable(editable: boolean, emitUpdate?: boolean): void; + + /** + * Set the document mode. + */ + setDocumentMode(mode: 'editing' | 'viewing' | 'suggesting'): void; + + /** + * Focus the editor. + */ + focus(): void; + + /** + * Blur the editor. + */ + blur(): void; + + // ============================================ + // DOCUMENT METHODS + // ============================================ + + /** + * Open a document. + */ + open(source?: string | File | Blob | BinaryData, options?: OpenOptions): Promise; + + /** + * Close the current document. + */ + close(): void; + + /** + * Replace the current file. + */ + replaceFile(newFile: File | Blob | BinaryData): Promise; + + /** + * Get the document as Markdown. + */ + getMarkdown(): Promise; + + /** + * Check if the editor is currently active/focused. + */ + isFocused: boolean; // --- Tracked selection handle API --- /** Capture the live PM selection as a tracked handle. Local-only. */ @@ -997,8 +1196,15 @@ export declare class PresentationEditor { */ emit(event: string, ...args: any[]): void; - /** Allow additional properties */ - [key: string]: any; + /** + * Replace the current file. + */ + replaceFile(newFile: File | Blob | BinaryData): Promise; + + /** + * Set the toolbar for the editor. + */ + setToolbar(toolbar: any): void; } // ============================================ diff --git a/packages/superdoc/src/index.js b/packages/superdoc/src/index.js index 34047a9504..f5b87828c3 100644 --- a/packages/superdoc/src/index.js +++ b/packages/superdoc/src/index.js @@ -1,6 +1,8 @@ import { SuperConverter, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, createZip, Extensions, @@ -15,6 +17,37 @@ import { DOCX, PDF, HTML, getFileObject, compareVersions } from '@superdoc/commo import BlankDOCX from '@superdoc/common/data/blank.docx?url'; import { getSchemaIntrospection } from './helpers/schema-introspection.js'; +// ============================================ +// TYPE RE-EXPORTS +// These types are defined in @superdoc/super-editor and re-exported for consumers +// ============================================ + +/** + * @typedef {import('@superdoc/super-editor').EditorState} EditorState + * @typedef {import('@superdoc/super-editor').Transaction} Transaction + * @typedef {import('@superdoc/super-editor').Schema} Schema + * @typedef {import('@superdoc/super-editor').EditorView} EditorView + * @typedef {import('@superdoc/super-editor').EditorCommands} EditorCommands + * @typedef {import('@superdoc/super-editor').ChainedCommand} ChainedCommand + * @typedef {import('@superdoc/super-editor').ChainableCommandObject} ChainableCommandObject + * @typedef {import('@superdoc/super-editor').PresentationEditorOptions} PresentationEditorOptions + * @typedef {import('@superdoc/super-editor').LayoutEngineOptions} LayoutEngineOptions + * @typedef {import('@superdoc/super-editor').PageSize} PageSize + * @typedef {import('@superdoc/super-editor').PageMargins} PageMargins + * @typedef {import('@superdoc/super-editor').Layout} Layout + * @typedef {import('@superdoc/super-editor').LayoutPage} LayoutPage + * @typedef {import('@superdoc/super-editor').LayoutFragment} LayoutFragment + * @typedef {import('@superdoc/super-editor').RangeRect} RangeRect + * @typedef {import('@superdoc/super-editor').BoundingRect} BoundingRect + * @typedef {import('@superdoc/super-editor').OpenOptions} OpenOptions + * @typedef {import('@superdoc/super-editor').DocxFileEntry} DocxFileEntry + * @typedef {import('@superdoc/super-editor').Comment} Comment + * @typedef {import('@superdoc/super-editor').CommentElement} CommentElement + * @typedef {import('@superdoc/super-editor').CommentsLoadedEventData} CommentsLoadedEventData + * @typedef {import('@superdoc/super-editor').FontConfig} FontConfig + * @typedef {import('@superdoc/super-editor').FontSupportInfo} FontSupportInfo + */ + // Public exports export { SuperDoc } from './core/SuperDoc.js'; export { @@ -22,6 +55,8 @@ export { getFileObject, compareVersions, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, getSchemaIntrospection, diff --git a/tests/consumer-typecheck/package.json b/tests/consumer-typecheck/package.json new file mode 100644 index 0000000000..fd02e045d9 --- /dev/null +++ b/tests/consumer-typecheck/package.json @@ -0,0 +1,14 @@ +{ + "name": "consumer-typecheck", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "superdoc": "file:../../packages/superdoc/superdoc.tgz" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/tests/consumer-typecheck/src/index.ts b/tests/consumer-typecheck/src/index.ts new file mode 100644 index 0000000000..5464479e3a --- /dev/null +++ b/tests/consumer-typecheck/src/index.ts @@ -0,0 +1,299 @@ +/** + * Consumer TypeScript compilation test. + * + * This file attempts to use SuperDoc's public API the way a real customer would. + * If this file compiles, the types are sufficient. If it fails, we know what's missing. + * + * Based on customer workaround from SD-2227. + */ + +// ============================================ +// BASIC IMPORTS +// These should all resolve from the main entry point +// ============================================ + +import { + SuperDoc, + Editor, + SuperConverter, + PresentationEditor, + getRichTextExtensions, + getStarterExtensions, + DOCX, + PDF, + HTML, + Extensions, + createZip, +} from 'superdoc'; + +// ============================================ +// EDITOR CLASS - STATIC METHODS +// Customer uses Editor.loadXmlData() for headless processing +// ============================================ + +async function testEditorStaticMethods() { + const file = new File([''], 'test.docx'); + + // Static method to load DOCX data + const xmlData = await Editor.loadXmlData(file); + + // Static method to open a document + const editor = await Editor.open(file); +} + +// ============================================ +// EDITOR CLASS - INSTANCE METHODS +// Customer needs exportDocx, on/off event handlers, setToolbar +// ============================================ + +async function testEditorInstanceMethods() { + const editor = new Editor(); + + // Export to DOCX - customer's primary use case + const docxBlob = await editor.exportDocx({ + comments: [], + commentsType: 'none', + }); + + // Event handlers - customer uses these extensively + editor.on('update', () => {}); + editor.on('create', () => {}); + editor.off('update', () => {}); + + // Toolbar integration + editor.setToolbar({}); + + // Basic properties + const state = editor.state; + const schema = editor.schema; + const isEditable = editor.isEditable; + const commands = editor.commands; + + // Commands + editor.commands.toggleBold(); + editor.commands.setFontSize('14pt'); + + // Chain commands + editor.chain().toggleBold().toggleItalic().run(); + + // Content methods + const html = editor.getHTML(); + const json = editor.getJSON(); + const text = editor.getText(); + + // Cleanup + editor.destroy(); +} + +// ============================================ +// PRESENTATION EDITOR CLASS +// Customer's main entry point for paginated editing +// ============================================ + +async function testPresentationEditor() { + const container = document.createElement('div'); + + // Constructor with options - customer passes many options + const presentationEditor = new PresentationEditor({ + element: container, + documentMode: 'editing', + content: {}, + extensions: [], + editable: true, + isCommentsEnabled: true, + }); + + // Access underlying editor + const editor = presentationEditor.editor; + + // State and schema access + const state = presentationEditor.state; + const isEditable = presentationEditor.isEditable; + + // Commands - customer uses comment commands + presentationEditor.commands.insertComment?.({ id: '123' }); + presentationEditor.commands.insertContent?.('Hello'); + + // Event handlers + presentationEditor.on('update', () => {}); + presentationEditor.on('create', () => {}); + presentationEditor.off('update', () => {}); + + // Document mode switching + presentationEditor.setDocumentMode('viewing'); + presentationEditor.setDocumentMode('suggesting'); + presentationEditor.setDocumentMode('editing'); + + // File replacement + const newFile = new File([''], 'new.docx'); + await presentationEditor.replaceFile?.(newFile); + + // Zoom + presentationEditor.setZoom(1.5); + + // Layout methods + const pages = presentationEditor.getPages(); + const rects = presentationEditor.getSelectionRects(); + + // Cleanup + presentationEditor.destroy(); +} + +// ============================================ +// SUPER TOOLBAR CLASS +// Customer creates toolbar with specific options +// ============================================ + +function testSuperToolbar() { + const editor = new Editor(); + + // Toolbar construction - customer uses these options + // const toolbar = new SuperToolbar({ + // editor, + // selector: '#toolbar', + // fonts: [], + // toolbarGroups: ['formatting', 'lists'], + // hideButtons: false, + // pagination: true, + // icons: {}, + // }); + + // Note: SuperToolbar is typed as `any` in hand-written types + // This section tests if it's exported at all +} + +// ============================================ +// COMMENT TYPES +// Customer works extensively with comments +// ============================================ + +interface ExpectedCommentShape { + commentId: string; + createdTime: number; + creatorEmail: string; + creatorName: string; + elements: Array<{ + type: string; + content?: unknown[]; + text?: string; + }>; + importedId: string; + isDone: boolean; + parentCommentId: string; +} + +function testCommentTypes() { + // This tests if Comment type is exported + // Customer needs to type their comment handlers + + const handleCommentsLoaded = (data: { comments: ExpectedCommentShape[] }) => { + for (const comment of data.comments) { + console.log(comment.commentId, comment.creatorName); + } + }; +} + +// ============================================ +// EXTENSION HELPERS +// Customer uses these to configure the editor +// ============================================ + +function testExtensionHelpers() { + // Get default extensions + const starterExtensions = getStarterExtensions(); + const richTextExtensions = getRichTextExtensions(); + + // Extensions namespace + const { Node, Mark, Extension, Plugin, PluginKey } = Extensions; +} + +// ============================================ +// SUPERDOC CLASS (Vue component wrapper) +// ============================================ + +function testSuperDoc() { + // SuperDoc is the main Vue component + // Type should exist even if it's `any` + const superdoc: typeof SuperDoc = SuperDoc; +} + +// ============================================ +// UTILITY FUNCTIONS +// ============================================ + +async function testUtilities() { + // createZip for manual DOCX assembly + const files = [{ name: 'document.xml', content: '' }]; + const zip = await createZip(files); + + // MIME type constants + const docxMime: typeof DOCX = DOCX; + const pdfMime: typeof PDF = PDF; + const htmlMime: typeof HTML = HTML; +} + +// ============================================ +// TYPE EXPORTS +// These types should be importable by consumers +// ============================================ + +import type { + // Editor types + EditorState, + Transaction, + Schema, + EditorView, + + // Command types + EditorCommands, + ChainedCommand, + + // Presentation types + PresentationEditorOptions, + LayoutEngineOptions, + PageSize, + PageMargins, + + // Layout types + Layout, + LayoutPage, + LayoutFragment, + RangeRect, + BoundingRect, + + // Data types + OpenOptions, + DocxFileEntry, +} from 'superdoc'; + +function testTypeImports() { + // Verify types are usable + const options: PresentationEditorOptions = { + element: document.createElement('div'), + }; + + const pageSize: PageSize = { w: 612, h: 792 }; + const margins: PageMargins = { top: 72, right: 72, bottom: 72, left: 72 }; + + const layoutOptions: LayoutEngineOptions = { + pageSize, + margins, + zoom: 1, + }; +} + +// ============================================ +// Run type checks (these are just for TypeScript, not runtime) +// ============================================ + +export { + testEditorStaticMethods, + testEditorInstanceMethods, + testPresentationEditor, + testSuperToolbar, + testCommentTypes, + testExtensionHelpers, + testSuperDoc, + testUtilities, + testTypeImports, +}; diff --git a/tests/consumer-typecheck/tsconfig.json b/tests/consumer-typecheck/tsconfig.json new file mode 100644 index 0000000000..c38e3f9f65 --- /dev/null +++ b/tests/consumer-typecheck/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +}