@harbour-enterprises/superdoc 2.0.0-next.22 → 2.0.0-next.23

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.
@@ -1,8 +1,8 @@
1
1
  import { B as Buffer$2, g as getDefaultExportFromCjs } from "./jszip-B1fkPkPJ.es.js";
2
2
  import { t as twipsToInches, i as inchesToTwips, p as ptToTwips, l as linesToTwips, a as twipsToLines, b as pixelsToTwips, c as twipsToPixels$2, d as convertSizeToCSS, e as inchesToPixels } from "./helpers-CAUq8coh.es.js";
3
- import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as liftTarget, u as canJoin, v as joinPoint, x as replaceStep$1, R as ReplaceAroundStep$1, y as htmlHandler, z as ReplaceStep, B as getResolvedParagraphProperties, C as changeListLevel, E as isList$1, G as updateNumberingProperties, L as ListHelpers, H as inputRulesPlugin, I as TrackDeleteMarkName$1, J as TrackInsertMarkName$1, K as TrackFormatMarkName$1, O as AddMarkStep, Q as RemoveMarkStep, U as CommandService, S as SuperConverter, V as EditorState, W as unflattenListsInHtml, X as SelectionRange, Y as Transform, Z as resolveParagraphProperties, _ as _getReferencedTableStyles, $ as decodeRPrFromMarks, a0 as calculateResolvedParagraphProperties, a1 as resolveRunProperties, a2 as encodeCSSFromPPr, a3 as encodeCSSFromRPr, a4 as generateOrderedListIndex, a5 as docxNumberingHelpers, a6 as InputRule, a7 as insertNewRelationship, a8 as kebabCase$1, a9 as getUnderlineCssString } from "./SuperConverter-94ypJzq2.es.js";
4
- import "./jszip.min-DCl8qkFO.es.js";
3
+ import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as liftTarget, u as canJoin, v as joinPoint, x as replaceStep$1, R as ReplaceAroundStep$1, y as htmlHandler, z as ReplaceStep, B as getResolvedParagraphProperties, C as changeListLevel, E as isList$1, G as updateNumberingProperties, L as ListHelpers, H as inputRulesPlugin, I as TrackDeleteMarkName$1, J as TrackInsertMarkName$1, K as TrackFormatMarkName$1, O as AddMarkStep, Q as RemoveMarkStep, U as CommandService, S as SuperConverter, V as EditorState, W as unflattenListsInHtml, X as SelectionRange, Y as Transform, Z as resolveParagraphProperties, _ as _getReferencedTableStyles, $ as decodeRPrFromMarks, a0 as calculateResolvedParagraphProperties, a1 as resolveRunProperties, a2 as encodeCSSFromPPr, a3 as encodeCSSFromRPr, a4 as generateOrderedListIndex, a5 as docxNumberingHelpers, a6 as InputRule, a7 as insertNewRelationship, a8 as kebabCase$1, a9 as getUnderlineCssString } from "./SuperConverter-tJ_8LYGZ.es.js";
5
4
  import { p as process$1$1, r as ref, C as global$1, c as computed, E as createElementBlock, F as Fragment$1, S as renderList, O as withModifiers, G as openBlock, P as normalizeClass, M as createCommentVNode, H as toDisplayString, K as createBaseVNode, U as createApp, f as onMounted, X as onUnmounted, R as withDirectives, v as unref, Y as vModelText, y as nextTick, L as normalizeStyle, u as watch, Z as withKeys, _ as createTextVNode, I as createVNode, h, $ as readonly, s as getCurrentInstance, o as onBeforeUnmount, j as reactive, b as onBeforeMount, i as inject, a0 as onActivated, a1 as onDeactivated, a2 as Comment, d as defineComponent, a as provide, g as Teleport, t as toRef, a3 as renderSlot, a4 as isVNode, D as shallowRef, w as watchEffect, T as Transition, a5 as mergeProps, a6 as vShow, a7 as cloneVNode, a8 as Text$2, m as markRaw, N as createBlock, J as withCtx, a9 as useCssVars, V as resolveDynamicComponent, aa as normalizeProps, ab as guardReactiveProps } from "./vue-BnBKJwCW.es.js";
5
+ import "./jszip.min-DCl8qkFO.es.js";
6
6
  import { E as EventEmitter$1 } from "./eventemitter3-CwrdEv8r.es.js";
7
7
  import { v as v4 } from "./uuid-CjlX8hrF.es.js";
8
8
  import { B as BlankDOCX } from "./blank-docx-ABm6XYAA.es.js";
@@ -9036,6 +9036,37 @@ class OxmlNode extends Node$1 {
9036
9036
  return new OxmlNode(config);
9037
9037
  }
9038
9038
  }
9039
+ class EditorError extends Error {
9040
+ constructor(message) {
9041
+ super(message);
9042
+ this.name = "EditorError";
9043
+ }
9044
+ }
9045
+ class InvalidStateError extends EditorError {
9046
+ constructor(message) {
9047
+ super(message);
9048
+ this.name = "InvalidStateError";
9049
+ }
9050
+ }
9051
+ class NoSourcePathError extends EditorError {
9052
+ constructor(message) {
9053
+ super(message);
9054
+ this.name = "NoSourcePathError";
9055
+ }
9056
+ }
9057
+ class FileSystemNotAvailableError extends EditorError {
9058
+ constructor(message) {
9059
+ super(message);
9060
+ this.name = "FileSystemNotAvailableError";
9061
+ }
9062
+ }
9063
+ class DocumentLoadError extends EditorError {
9064
+ constructor(message, cause) {
9065
+ super(message);
9066
+ this.name = "DocumentLoadError";
9067
+ this.cause = cause;
9068
+ }
9069
+ }
9039
9070
  const first = (commands2) => (props) => {
9040
9071
  const items = typeof commands2 === "function" ? commands2(props) : commands2;
9041
9072
  for (let i = 0; i < items.length; i += 1) {
@@ -14867,7 +14898,7 @@ const canUseDOM = () => {
14867
14898
  return false;
14868
14899
  }
14869
14900
  };
14870
- const summaryVersion = "2.0.0-next.22";
14901
+ const summaryVersion = "2.0.0-next.23";
14871
14902
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
14872
14903
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
14873
14904
  function mapAttributes(attrs) {
@@ -15351,14 +15382,37 @@ class ProseMirrorRenderer {
15351
15382
  }
15352
15383
  class Editor extends EventEmitter {
15353
15384
  /**
15354
- * Create a new Editor instance
15385
+ * Create a new Editor instance.
15386
+ *
15387
+ * **Legacy mode (backward compatible):**
15388
+ * When `content` or `fileSource` is provided, the editor initializes synchronously
15389
+ * with the document loaded immediately. This preserves existing behavior where
15390
+ * `editor.view` is available right after construction.
15391
+ *
15392
+ * **New mode (document lifecycle API):**
15393
+ * When no `content` or `fileSource` is provided, only core services (extensions,
15394
+ * commands, schema) are initialized. Call `editor.open()` to load a document.
15395
+ *
15355
15396
  * @param options - Editor configuration options
15397
+ *
15398
+ * @example
15399
+ * ```typescript
15400
+ * // Legacy mode (still works)
15401
+ * const editor = new Editor({ content: docx, element: el });
15402
+ * console.log(editor.view.state.doc); // Works immediately
15403
+ *
15404
+ * // New mode
15405
+ * const editor = new Editor({ element: el });
15406
+ * await editor.open('/path/to/doc.docx');
15407
+ * ```
15356
15408
  */
15357
15409
  constructor(options) {
15358
15410
  super();
15359
15411
  this.extensionStorage = {};
15360
15412
  this.#renderer = null;
15361
15413
  this.#isDestroyed = false;
15414
+ this.#editorLifecycleState = "initialized";
15415
+ this.#sourcePath = null;
15362
15416
  this.presentationEditor = null;
15363
15417
  this.isFocused = false;
15364
15418
  this.fontsImported = [];
@@ -15471,18 +15525,25 @@ class Editor extends EventEmitter {
15471
15525
  this.#checkHeadless(resolvedOptions);
15472
15526
  this.setOptions(resolvedOptions);
15473
15527
  this.#renderer = resolvedOptions.renderer ?? (domAvailable ? new ProseMirrorRenderer() : null);
15474
- const modes = {
15475
- docx: () => this.#init(),
15476
- text: () => this.#initRichText(),
15477
- html: () => this.#initRichText(),
15478
- default: () => {
15479
- console.log("Not implemented.");
15480
- }
15481
- };
15482
- const initMode = modes[this.options.mode] ?? modes.default;
15483
15528
  const { setHighContrastMode } = useHighContrastMode();
15484
15529
  this.setHighContrastMode = setHighContrastMode;
15485
- initMode();
15530
+ const useNewApiMode = resolvedOptions.deferDocumentLoad === true;
15531
+ if (useNewApiMode) {
15532
+ this.#initCore();
15533
+ this.#editorLifecycleState = "initialized";
15534
+ } else {
15535
+ const modes = {
15536
+ docx: () => this.#init(),
15537
+ text: () => this.#initRichText(),
15538
+ html: () => this.#initRichText(),
15539
+ default: () => {
15540
+ console.log("Not implemented.");
15541
+ }
15542
+ };
15543
+ const initMode = modes[this.options.mode] ?? modes.default;
15544
+ initMode();
15545
+ this.#editorLifecycleState = "ready";
15546
+ }
15486
15547
  }
15487
15548
  /**
15488
15549
  * Command service for handling editor commands
@@ -15490,6 +15551,8 @@ class Editor extends EventEmitter {
15490
15551
  #commandService;
15491
15552
  #renderer;
15492
15553
  #isDestroyed;
15554
+ #editorLifecycleState;
15555
+ #sourcePath;
15493
15556
  /**
15494
15557
  * Getter which indicates if any changes happen in Editor
15495
15558
  */
@@ -15514,6 +15577,303 @@ class Editor extends EventEmitter {
15514
15577
  this.emit("create", { editor: this });
15515
15578
  }, 0);
15516
15579
  }
15580
+ /**
15581
+ * Assert that the editor is in one of the allowed states.
15582
+ * Throws InvalidStateError if not.
15583
+ */
15584
+ #assertState(...allowed) {
15585
+ if (!allowed.includes(this.#editorLifecycleState)) {
15586
+ throw new InvalidStateError(
15587
+ `Invalid operation: editor is in '${this.#editorLifecycleState}' state, expected one of: ${allowed.join(", ")}`
15588
+ );
15589
+ }
15590
+ }
15591
+ /**
15592
+ * Wraps an async operation with state transitions for safe lifecycle management.
15593
+ *
15594
+ * This method ensures atomic state transitions during async operations:
15595
+ * 1. Sets state to `during` before executing the operation
15596
+ * 2. On success: sets state to `success` and returns the operation result
15597
+ * 3. On error: sets state to `failure` and re-throws the error
15598
+ *
15599
+ * This prevents race conditions and ensures the editor is always in a valid state,
15600
+ * even when operations fail.
15601
+ *
15602
+ * @template T - The return type of the operation
15603
+ * @param during - State to set while the operation is running
15604
+ * @param success - State to set if the operation succeeds
15605
+ * @param failure - State to set if the operation fails
15606
+ * @param operation - Async operation to execute
15607
+ * @returns Promise resolving to the operation's return value
15608
+ * @throws Re-throws any error from the operation after setting failure state
15609
+ *
15610
+ * @example
15611
+ * ```typescript
15612
+ * // Used internally for save operations:
15613
+ * await this.#withState('saving', 'ready', 'ready', async () => {
15614
+ * const data = await this.exportDocument();
15615
+ * await this.#writeToPath(path, data);
15616
+ * });
15617
+ * ```
15618
+ */
15619
+ async #withState(during, success, failure, operation) {
15620
+ this.#editorLifecycleState = during;
15621
+ try {
15622
+ const result = await operation();
15623
+ this.#editorLifecycleState = success;
15624
+ return result;
15625
+ } catch (error) {
15626
+ this.#editorLifecycleState = failure;
15627
+ throw error;
15628
+ }
15629
+ }
15630
+ /**
15631
+ * Initialize core editor services for new lifecycle API mode.
15632
+ *
15633
+ * When `deferDocumentLoad: true` is set, this method initializes only the
15634
+ * document-independent components:
15635
+ * - Extension service (loads and configures all extensions)
15636
+ * - Command service (registers all editor commands)
15637
+ * - ProseMirror schema (derived from extensions, reusable across documents)
15638
+ *
15639
+ * These services are created once during construction and reused when opening
15640
+ * different documents via the `open()` method. This enables efficient document
15641
+ * switching without recreating the entire editor infrastructure.
15642
+ *
15643
+ * Called exclusively from the constructor when `deferDocumentLoad` is true.
15644
+ *
15645
+ * @remarks
15646
+ * This is part of the new lifecycle API that separates editor initialization
15647
+ * from document loading. The schema and extensions remain constant while
15648
+ * documents can be opened, closed, and reopened.
15649
+ *
15650
+ * @see #loadDocument - Loads document-specific state after core initialization
15651
+ */
15652
+ #initCore() {
15653
+ if (!this.options.extensions?.length) {
15654
+ this.options.extensions = this.options.mode === "docx" ? getStarterExtensions() : getRichTextExtensions();
15655
+ }
15656
+ this.#createExtensionService();
15657
+ this.#createCommandService();
15658
+ this.#createSchema();
15659
+ this.#registerEventListeners();
15660
+ }
15661
+ /**
15662
+ * Register all event listeners from options.
15663
+ *
15664
+ * Called once during core initialization. These listeners persist across
15665
+ * document open/close cycles since the callbacks are set at construction time.
15666
+ */
15667
+ #registerEventListeners() {
15668
+ this.on("create", this.options.onCreate);
15669
+ this.on("update", this.options.onUpdate);
15670
+ this.on("selectionUpdate", this.options.onSelectionUpdate);
15671
+ this.on("transaction", this.options.onTransaction);
15672
+ this.on("focus", this.#onFocus.bind(this));
15673
+ this.on("blur", this.options.onBlur);
15674
+ this.on("destroy", this.options.onDestroy);
15675
+ this.on("trackedChangesUpdate", this.options.onTrackedChangesUpdate);
15676
+ this.on("commentsLoaded", this.options.onCommentsLoaded);
15677
+ this.on("commentClick", this.options.onCommentClicked);
15678
+ this.on("commentsUpdate", this.options.onCommentsUpdate);
15679
+ this.on("locked", this.options.onDocumentLocked);
15680
+ this.on("collaborationReady", this.#onCollaborationReady.bind(this));
15681
+ this.on("comment-positions", this.options.onCommentLocationsUpdate);
15682
+ this.on("list-definitions-change", this.options.onListDefinitionsChange);
15683
+ this.on("fonts-resolved", this.options.onFontsResolved);
15684
+ this.on("exception", this.options.onException);
15685
+ }
15686
+ /**
15687
+ * Load a document into the editor from various source types.
15688
+ *
15689
+ * This method handles the complete document loading pipeline:
15690
+ * 1. **Source resolution**: Determines source type (path/File/Blob/Buffer/blank)
15691
+ * 2. **Content loading**:
15692
+ * - String path: Reads file from disk (Node.js) or fetches URL (browser)
15693
+ * - File/Blob: Extracts docx archive data
15694
+ * - Buffer: Processes binary data (Node.js)
15695
+ * - undefined/null: Creates blank document
15696
+ * 3. **Document initialization**: Creates converter, media, fonts, initial state
15697
+ * 4. **View mounting**: Attaches ProseMirror view (unless headless)
15698
+ * 5. **Event wiring**: Connects all lifecycle event handlers
15699
+ *
15700
+ * Called by `open()` after state validation, wrapped in `#withState()` for
15701
+ * atomic state transitions.
15702
+ *
15703
+ * @param source - Document source:
15704
+ * - `string`: File path (Node.js reads from disk, browser fetches as URL)
15705
+ * - `File | Blob`: Browser file object or blob
15706
+ * - `Buffer`: Node.js buffer containing docx data
15707
+ * - `undefined | null`: Creates a blank document
15708
+ * @param options - Document-level options (mode, comments, styles, etc.)
15709
+ * @returns Promise that resolves when document is fully loaded and ready
15710
+ * @throws {DocumentLoadError} If any step of document loading fails. The error
15711
+ * wraps the underlying cause for debugging.
15712
+ *
15713
+ * @remarks
15714
+ * - Sets `#sourcePath` for path-based sources (enables `save()`)
15715
+ * - Sets `#sourcePath = null` for Blob/Buffer sources (requires `saveTo()`)
15716
+ * - In browser, string paths are treated as URLs to fetch
15717
+ * - In Node.js, string paths are read from the filesystem
15718
+ *
15719
+ * @see open - Public API that calls this method
15720
+ * @see #unloadDocument - Cleanup counterpart that reverses this process
15721
+ */
15722
+ async #loadDocument(source, options) {
15723
+ try {
15724
+ const resolvedMode = options?.mode ?? this.options.mode ?? "docx";
15725
+ const resolvedOptions = {
15726
+ ...this.options,
15727
+ mode: resolvedMode,
15728
+ isCommentsEnabled: options?.isCommentsEnabled ?? this.options.isCommentsEnabled,
15729
+ suppressDefaultDocxStyles: options?.suppressDefaultDocxStyles ?? this.options.suppressDefaultDocxStyles,
15730
+ documentMode: options?.documentMode ?? this.options.documentMode ?? "editing",
15731
+ html: options?.html,
15732
+ markdown: options?.markdown,
15733
+ jsonOverride: options?.json ?? null
15734
+ };
15735
+ if (typeof source === "string") {
15736
+ if (typeof process$1$1 !== "undefined" && process$1$1.versions?.node) {
15737
+ const fs = require("fs");
15738
+ const buffer = fs.readFileSync(source);
15739
+ const [docx, _media, mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
15740
+ resolvedOptions.content = docx;
15741
+ resolvedOptions.mediaFiles = mediaFiles;
15742
+ resolvedOptions.fonts = fonts;
15743
+ resolvedOptions.fileSource = buffer;
15744
+ this.#sourcePath = source;
15745
+ } else {
15746
+ const response = await fetch(source);
15747
+ const blob = await response.blob();
15748
+ const [docx, _media, mediaFiles, fonts] = await Editor.loadXmlData(blob);
15749
+ resolvedOptions.content = docx;
15750
+ resolvedOptions.mediaFiles = mediaFiles;
15751
+ resolvedOptions.fonts = fonts;
15752
+ resolvedOptions.fileSource = blob;
15753
+ this.#sourcePath = source.split("/").pop() || null;
15754
+ }
15755
+ } else if (source != null && typeof source === "object") {
15756
+ const isNodeBuffer = typeof Buffer$2 !== "undefined" && (Buffer$2.isBuffer(source) || source instanceof Buffer$2);
15757
+ const isBlob = typeof Blob !== "undefined" && source instanceof Blob;
15758
+ const isArrayBuffer = source instanceof ArrayBuffer;
15759
+ const hasArrayBuffer = typeof source === "object" && "buffer" in source && source.buffer instanceof ArrayBuffer;
15760
+ if (isNodeBuffer || isBlob || isArrayBuffer || hasArrayBuffer) {
15761
+ const [docx, _media, mediaFiles, fonts] = await Editor.loadXmlData(
15762
+ source,
15763
+ isNodeBuffer
15764
+ );
15765
+ resolvedOptions.content = docx;
15766
+ resolvedOptions.mediaFiles = mediaFiles;
15767
+ resolvedOptions.fonts = fonts;
15768
+ resolvedOptions.fileSource = source;
15769
+ this.#sourcePath = null;
15770
+ } else {
15771
+ const [docx, _media, mediaFiles, fonts] = await Editor.loadXmlData(source, false);
15772
+ resolvedOptions.content = docx;
15773
+ resolvedOptions.mediaFiles = mediaFiles;
15774
+ resolvedOptions.fonts = fonts;
15775
+ resolvedOptions.fileSource = source;
15776
+ this.#sourcePath = null;
15777
+ }
15778
+ } else {
15779
+ resolvedOptions.content = options?.content ?? [];
15780
+ resolvedOptions.mediaFiles = options?.mediaFiles ?? {};
15781
+ resolvedOptions.fonts = options?.fonts ?? {};
15782
+ resolvedOptions.fileSource = null;
15783
+ resolvedOptions.isNewFile = !options?.content;
15784
+ this.#sourcePath = null;
15785
+ }
15786
+ this.setOptions(resolvedOptions);
15787
+ this.#createConverter();
15788
+ this.#initMedia();
15789
+ const shouldMountRenderer = this.#shouldMountRenderer();
15790
+ if (shouldMountRenderer) {
15791
+ this.#initContainerElement(this.options);
15792
+ this.#initFonts();
15793
+ }
15794
+ this.#createInitialState({ includePlugins: !shouldMountRenderer });
15795
+ if (!shouldMountRenderer) {
15796
+ const tr = this.state.tr.setMeta("forcePluginPass", true).setMeta("addToHistory", false);
15797
+ this.#dispatchTransaction(tr);
15798
+ }
15799
+ if (shouldMountRenderer) {
15800
+ this.mount(this.options.element);
15801
+ this.#configureStateWithExtensionPlugins();
15802
+ }
15803
+ if (!shouldMountRenderer) {
15804
+ this.#emitCreateAsync();
15805
+ }
15806
+ if (shouldMountRenderer) {
15807
+ this.initDefaultStyles();
15808
+ this.#checkFonts();
15809
+ }
15810
+ const shouldMigrateListsOnInit = Boolean(
15811
+ this.options.markdown || this.options.html || this.options.loadFromSchema || this.options.jsonOverride || this.options.mode === "html" || this.options.mode === "text"
15812
+ );
15813
+ if (shouldMigrateListsOnInit) {
15814
+ this.migrateListsToV2();
15815
+ }
15816
+ this.setDocumentMode(this.options.documentMode, "init");
15817
+ this.initializeCollaborationData();
15818
+ if (!this.options.ydoc && !this.options.isChildEditor) {
15819
+ this.#initComments();
15820
+ }
15821
+ if (shouldMountRenderer) {
15822
+ this.#initDevTools();
15823
+ this.#registerCopyHandler();
15824
+ }
15825
+ } catch (error) {
15826
+ const err = error instanceof Error ? error : new Error(String(error));
15827
+ throw new DocumentLoadError(`Failed to load document: ${err.message}`, err);
15828
+ }
15829
+ }
15830
+ /**
15831
+ * Unload the current document and clean up all document-specific resources.
15832
+ *
15833
+ * This method performs a complete cleanup of document state while preserving
15834
+ * the core editor services (schema, extensions, commands) for reuse:
15835
+ *
15836
+ * **Resources cleaned up:**
15837
+ * - ProseMirror view (unmounted from DOM)
15838
+ * - Header/footer editors (destroyed)
15839
+ * - Document converter instance
15840
+ * - Media references and image storage
15841
+ * - Source path reference
15842
+ * - Document-specific options (content, fileSource, initialState)
15843
+ * - ProseMirror editor state
15844
+ *
15845
+ * **Resources preserved:**
15846
+ * - ProseMirror schema
15847
+ * - Extension service and registered extensions
15848
+ * - Command service and registered commands
15849
+ * - Event listeners (registered once during core init, reused across documents)
15850
+ *
15851
+ * After cleanup, the editor transitions to 'closed' state and can be reopened
15852
+ * with a new document via `open()`.
15853
+ *
15854
+ * Called by `close()` after emitting the `documentClose` event.
15855
+ *
15856
+ * @remarks
15857
+ * This is a critical part of the document lifecycle API that enables efficient
15858
+ * document switching. By preserving schema and extensions, we avoid expensive
15859
+ * reinitialization when opening multiple documents sequentially.
15860
+ *
15861
+ * @see close - Public API that calls this method
15862
+ * @see #loadDocument - Counterpart method that loads document resources
15863
+ */
15864
+ #unloadDocument() {
15865
+ this.unmount();
15866
+ this.destroyHeaderFooterEditors();
15867
+ this.converter = void 0;
15868
+ if (this.storage.image) {
15869
+ this.storage.image.media = {};
15870
+ }
15871
+ this.#sourcePath = null;
15872
+ this.options.initialState = null;
15873
+ this.options.content = "";
15874
+ this.options.fileSource = null;
15875
+ this._state = void 0;
15876
+ }
15517
15877
  /**
15518
15878
  * Initialize the editor with the given options
15519
15879
  */
@@ -15678,6 +16038,25 @@ class Editor extends EventEmitter {
15678
16038
  get state() {
15679
16039
  return this._state;
15680
16040
  }
16041
+ /**
16042
+ * Get the current editor lifecycle state.
16043
+ *
16044
+ * @returns The current lifecycle state ('initialized', 'documentLoading', 'ready', 'saving', 'closed', 'destroyed')
16045
+ */
16046
+ get lifecycleState() {
16047
+ return this.#editorLifecycleState;
16048
+ }
16049
+ /**
16050
+ * Get the source path of the currently opened document.
16051
+ *
16052
+ * Returns the file path if the document was opened from a path (Node.js),
16053
+ * or null if opened from a Blob/Buffer or created as a blank document.
16054
+ *
16055
+ * In browsers, this is only a suggested filename, not an actual filesystem path.
16056
+ */
16057
+ get sourcePath() {
16058
+ return this.#sourcePath;
16059
+ }
15681
16060
  /**
15682
16061
  * Replace the editor state entirely.
15683
16062
  *
@@ -15775,19 +16154,19 @@ class Editor extends EventEmitter {
15775
16154
  if (this.options.role === "viewer") cleanedMode = "viewing";
15776
16155
  if (this.options.role === "suggester" && cleanedMode === "editing") cleanedMode = "suggesting";
15777
16156
  if (cleanedMode === "viewing") {
15778
- this.commands.toggleTrackChangesShowOriginal();
16157
+ this.commands.toggleTrackChangesShowOriginal?.();
15779
16158
  this.setEditable(false, false);
15780
16159
  this.setOptions({ documentMode: "viewing" });
15781
16160
  if (pm) pm.classList.add("view-mode");
15782
16161
  } else if (cleanedMode === "suggesting") {
15783
- this.commands.disableTrackChangesShowOriginal();
15784
- this.commands.enableTrackChanges();
16162
+ this.commands.disableTrackChangesShowOriginal?.();
16163
+ this.commands.enableTrackChanges?.();
15785
16164
  this.setOptions({ documentMode: "suggesting" });
15786
16165
  this.setEditable(true, false);
15787
16166
  if (pm) pm.classList.remove("view-mode");
15788
16167
  } else if (cleanedMode === "editing") {
15789
- this.commands.disableTrackChangesShowOriginal();
15790
- this.commands.disableTrackChanges();
16168
+ this.commands.disableTrackChangesShowOriginal?.();
16169
+ this.commands.disableTrackChanges?.();
15791
16170
  this.setEditable(true, false);
15792
16171
  this.setOptions({ documentMode: "editing" });
15793
16172
  if (pm) pm.classList.remove("view-mode");
@@ -16835,16 +17214,289 @@ class Editor extends EventEmitter {
16835
17214
  console.error(err);
16836
17215
  }
16837
17216
  }
17217
+ // ============================================================================
17218
+ // Document Lifecycle API
17219
+ // ============================================================================
17220
+ /**
17221
+ * Open a document in the editor.
17222
+ *
17223
+ * @param source - Document source:
17224
+ * - `string` - File path (Node.js reads from disk, browser fetches URL)
17225
+ * - `File | Blob` - Browser file object
17226
+ * - `Buffer` - Node.js buffer
17227
+ * - `undefined` - Creates a blank document
17228
+ * @param options - Document options (mode, comments, etc.)
17229
+ * @returns Promise that resolves when document is loaded
17230
+ *
17231
+ * @throws {InvalidStateError} If editor is not in 'initialized' or 'closed' state
17232
+ * @throws {DocumentLoadError} If document loading fails
17233
+ *
17234
+ * @example
17235
+ * ```typescript
17236
+ * const editor = new Editor({ element: myDiv });
17237
+ *
17238
+ * // Open from file path (Node.js)
17239
+ * await editor.open('/path/to/document.docx');
17240
+ *
17241
+ * // Open from File object (browser)
17242
+ * await editor.open(fileInput.files[0]);
17243
+ *
17244
+ * // Open blank document
17245
+ * await editor.open();
17246
+ *
17247
+ * // Open with options
17248
+ * await editor.open('/path/to/doc.docx', { isCommentsEnabled: true });
17249
+ * ```
17250
+ */
17251
+ async open(source, options) {
17252
+ this.#assertState("initialized", "closed");
17253
+ await this.#withState("documentLoading", "ready", "closed", async () => {
17254
+ await this.#loadDocument(source, options);
17255
+ });
17256
+ this.emit("documentOpen", { editor: this, sourcePath: this.#sourcePath });
17257
+ }
17258
+ /**
17259
+ * Static factory method for one-liner document opening.
17260
+ * Creates an Editor instance and opens the document in one call.
17261
+ *
17262
+ * Smart defaults enable minimal configuration:
17263
+ * - No element/selector → headless mode
17264
+ * - No extensions → uses getStarterExtensions() for docx, getRichTextExtensions() for text/html
17265
+ * - No mode → defaults to 'docx'
17266
+ *
17267
+ * @param source - Document source (path, File, Blob, Buffer, or undefined for blank)
17268
+ * @param config - Combined editor and document options (all optional)
17269
+ * @returns Promise resolving to the ready Editor instance
17270
+ *
17271
+ * @example
17272
+ * ```typescript
17273
+ * // Minimal headless usage - just works!
17274
+ * const editor = await Editor.open('/path/to/doc.docx');
17275
+ *
17276
+ * // With options
17277
+ * const editor = await Editor.open('/path/to/doc.docx', {
17278
+ * isCommentsEnabled: true,
17279
+ * });
17280
+ *
17281
+ * // With UI element (automatically not headless)
17282
+ * const editor = await Editor.open('/path/to/doc.docx', {
17283
+ * element: document.getElementById('editor'),
17284
+ * });
17285
+ *
17286
+ * // Blank document
17287
+ * const editor = await Editor.open();
17288
+ * ```
17289
+ */
17290
+ static async open(source, config) {
17291
+ const hasElement = config?.element != null || config?.selector != null;
17292
+ const resolvedConfig = {
17293
+ mode: "docx",
17294
+ isHeadless: !hasElement,
17295
+ ...config
17296
+ };
17297
+ const {
17298
+ // OpenOptions (document-level)
17299
+ html,
17300
+ markdown,
17301
+ isCommentsEnabled,
17302
+ suppressDefaultDocxStyles,
17303
+ documentMode,
17304
+ content,
17305
+ mediaFiles,
17306
+ fonts,
17307
+ // Everything else is EditorOptions
17308
+ ...editorConfig
17309
+ } = resolvedConfig;
17310
+ const openOptions = {
17311
+ mode: resolvedConfig.mode,
17312
+ html,
17313
+ markdown,
17314
+ isCommentsEnabled,
17315
+ suppressDefaultDocxStyles,
17316
+ documentMode,
17317
+ content,
17318
+ mediaFiles,
17319
+ fonts
17320
+ };
17321
+ const editor = new Editor({ ...editorConfig, deferDocumentLoad: true });
17322
+ await editor.open(source, openOptions);
17323
+ return editor;
17324
+ }
17325
+ /**
17326
+ * Close the current document.
17327
+ *
17328
+ * This unloads the document but keeps the editor instance alive.
17329
+ * The editor can be reused by calling `open()` again.
17330
+ *
17331
+ * This method is idempotent - calling it when already closed is a no-op.
17332
+ *
17333
+ * @example
17334
+ * ```typescript
17335
+ * await editor.open('/doc1.docx');
17336
+ * // ... work with document ...
17337
+ * editor.close();
17338
+ *
17339
+ * await editor.open('/doc2.docx'); // Reuse the same editor
17340
+ * ```
17341
+ */
17342
+ close() {
17343
+ if (this.#editorLifecycleState === "closed" || this.#editorLifecycleState === "initialized") {
17344
+ return;
17345
+ }
17346
+ if (this.#editorLifecycleState === "destroyed") {
17347
+ return;
17348
+ }
17349
+ this.#assertState("ready");
17350
+ this.emit("documentClose", { editor: this });
17351
+ this.#unloadDocument();
17352
+ this.#editorLifecycleState = "closed";
17353
+ }
17354
+ /**
17355
+ * Save the document to the original source path.
17356
+ *
17357
+ * Only works if the document was opened from a file path.
17358
+ * If opened from Blob/Buffer or created blank, use `saveTo()` or `exportDocument()`.
17359
+ *
17360
+ * @param options - Save options (comments, final doc, etc.)
17361
+ * @throws {InvalidStateError} If editor is not in 'ready' state
17362
+ * @throws {NoSourcePathError} If no source path is available
17363
+ * @throws {FileSystemNotAvailableError} If file system access is not available
17364
+ *
17365
+ * @example
17366
+ * ```typescript
17367
+ * const editor = await Editor.open('/path/to/doc.docx');
17368
+ * // ... make changes ...
17369
+ * await editor.save(); // Saves back to /path/to/doc.docx
17370
+ * ```
17371
+ */
17372
+ async save(options) {
17373
+ this.#assertState("ready");
17374
+ if (!this.#sourcePath) {
17375
+ throw new NoSourcePathError("No source path. Use saveTo(path) or exportDocument() instead.");
17376
+ }
17377
+ await this.#withState("saving", "ready", "ready", async () => {
17378
+ const data = await this.exportDocument(options);
17379
+ await this.#writeToPath(this.#sourcePath, data);
17380
+ });
17381
+ }
17382
+ /**
17383
+ * Save the document to a specific path.
17384
+ *
17385
+ * Updates the source path to the new location after saving.
17386
+ *
17387
+ * @param path - File path to save to
17388
+ * @param options - Save options
17389
+ * @throws {InvalidStateError} If editor is not in 'ready' state
17390
+ * @throws {FileSystemNotAvailableError} If file system access is not available
17391
+ *
17392
+ * @example
17393
+ * ```typescript
17394
+ * const editor = await Editor.open(blobData); // No source path
17395
+ * await editor.saveTo('/path/to/new-doc.docx');
17396
+ * await editor.save(); // Now saves to /path/to/new-doc.docx
17397
+ * ```
17398
+ */
17399
+ async saveTo(path, options) {
17400
+ this.#assertState("ready");
17401
+ await this.#withState("saving", "ready", "ready", async () => {
17402
+ const data = await this.exportDocument(options);
17403
+ await this.#writeToPath(path, data);
17404
+ this.#sourcePath = path;
17405
+ });
17406
+ }
17407
+ /**
17408
+ * Export the document as a Blob or Buffer.
17409
+ *
17410
+ * This is a convenience wrapper around `exportDocx()` that returns
17411
+ * the document data without writing to a file.
17412
+ *
17413
+ * @param options - Export options
17414
+ * @returns Promise resolving to Blob (browser) or Buffer (Node.js)
17415
+ * @throws {InvalidStateError} If editor is not in 'ready' state
17416
+ *
17417
+ * @example
17418
+ * ```typescript
17419
+ * const blob = await editor.exportDocument();
17420
+ *
17421
+ * // Create download link in browser
17422
+ * const url = URL.createObjectURL(blob);
17423
+ * const a = document.createElement('a');
17424
+ * a.href = url;
17425
+ * a.download = 'document.docx';
17426
+ * a.click();
17427
+ * ```
17428
+ */
17429
+ async exportDocument(options) {
17430
+ this.#assertState("ready", "saving");
17431
+ const result = await this.exportDocx({
17432
+ isFinalDoc: options?.isFinalDoc,
17433
+ commentsType: options?.commentsType,
17434
+ comments: options?.comments,
17435
+ fieldsHighlightColor: options?.fieldsHighlightColor
17436
+ });
17437
+ return result;
17438
+ }
17439
+ /**
17440
+ * Writes document data to a file path.
17441
+ *
17442
+ * **Browser behavior:**
17443
+ * In browsers, the `path` parameter is only used as a suggested filename.
17444
+ * The File System Access API shows a save dialog and the user chooses the actual location.
17445
+ *
17446
+ * **Node.js behavior:**
17447
+ * The path is an actual filesystem path, written directly.
17448
+ */
17449
+ async #writeToPath(path, data) {
17450
+ const isNode2 = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && globalThis.process.versions?.node != null;
17451
+ const hasNodeBuffer = typeof Buffer$2 !== "undefined" && typeof Buffer$2.isBuffer === "function";
17452
+ if (isNode2 || hasNodeBuffer) {
17453
+ try {
17454
+ const fs = require("fs");
17455
+ const buffer = Buffer$2.isBuffer(data) ? data : Buffer$2.from(await data.arrayBuffer());
17456
+ fs.writeFileSync(path, buffer);
17457
+ return;
17458
+ } catch {
17459
+ }
17460
+ }
17461
+ if (typeof window !== "undefined" && "showSaveFilePicker" in window) {
17462
+ const handle = await window.showSaveFilePicker({
17463
+ suggestedName: path.split("/").pop() || "document.docx",
17464
+ types: [
17465
+ {
17466
+ description: "Word Document",
17467
+ accept: { "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"] }
17468
+ }
17469
+ ]
17470
+ });
17471
+ const writable = await handle.createWritable();
17472
+ await writable.write(data);
17473
+ await writable.close();
17474
+ return;
17475
+ }
17476
+ throw new FileSystemNotAvailableError(
17477
+ "File System Access API not available. Use exportDocument() to get the document data and handle the download manually."
17478
+ );
17479
+ }
16838
17480
  /**
16839
17481
  * Destroy the editor and clean up resources
16840
17482
  */
16841
17483
  destroy() {
17484
+ if (this.#editorLifecycleState === "ready") {
17485
+ this.close();
17486
+ }
17487
+ if (this.#editorLifecycleState === "destroyed") {
17488
+ return;
17489
+ }
16842
17490
  this.#isDestroyed = true;
16843
17491
  this.emit("destroy");
16844
17492
  this.unmount();
16845
17493
  this.destroyHeaderFooterEditors();
16846
17494
  this.#endCollaboration();
16847
17495
  this.removeAllListeners();
17496
+ this.extensionService = void 0;
17497
+ this.schema = void 0;
17498
+ this.#commandService = void 0;
17499
+ this.#editorLifecycleState = "destroyed";
16848
17500
  }
16849
17501
  destroyHeaderFooterEditors() {
16850
17502
  try {
@@ -16876,7 +17528,7 @@ class Editor extends EventEmitter {
16876
17528
  * Process collaboration migrations
16877
17529
  */
16878
17530
  processCollaborationMigrations() {
16879
- console.debug("[checkVersionMigrations] Current editor version", "2.0.0-next.22");
17531
+ console.debug("[checkVersionMigrations] Current editor version", "2.0.0-next.23");
16880
17532
  if (!this.options.ydoc) return;
16881
17533
  const metaMap = this.options.ydoc.getMap("meta");
16882
17534
  let docVersion = metaMap.get("version");