@harbour-enterprises/superdoc 1.8.0-next.3 → 1.8.0-next.4

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.
@@ -38710,7 +38710,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
38710
38710
  static getStoredSuperdocVersion(docx) {
38711
38711
  return SuperConverter.getStoredCustomProperty(docx, "SuperdocVersion");
38712
38712
  }
38713
- static setStoredSuperdocVersion(docx = this.convertedXml, version2 = "1.8.0-next.3") {
38713
+ static setStoredSuperdocVersion(docx = this.convertedXml, version2 = "1.8.0-next.4") {
38714
38714
  return SuperConverter.setStoredCustomProperty(docx, "SuperdocVersion", version2, false);
38715
38715
  }
38716
38716
  /**
@@ -65052,7 +65052,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
65052
65052
  return false;
65053
65053
  }
65054
65054
  };
65055
- const summaryVersion = "1.8.0-next.3";
65055
+ const summaryVersion = "1.8.0-next.4";
65056
65056
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
65057
65057
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
65058
65058
  function mapAttributes(attrs) {
@@ -67710,7 +67710,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
67710
67710
  * Process collaboration migrations
67711
67711
  */
67712
67712
  processCollaborationMigrations() {
67713
- console.debug("[checkVersionMigrations] Current editor version", "1.8.0-next.3");
67713
+ console.debug("[checkVersionMigrations] Current editor version", "1.8.0-next.4");
67714
67714
  if (!this.options.ydoc) return;
67715
67715
  const metaMap = this.options.ydoc.getMap("meta");
67716
67716
  let docVersion = metaMap.get("version");
@@ -77754,24 +77754,6 @@ ${o}
77754
77754
  }
77755
77755
  elem.setAttribute("role", "link");
77756
77756
  elem.setAttribute("tabindex", "0");
77757
- elem.addEventListener("click", (event) => {
77758
- event.preventDefault();
77759
- event.stopPropagation();
77760
- const linkClickEvent = new CustomEvent("superdoc-link-click", {
77761
- bubbles: true,
77762
- composed: true,
77763
- detail: {
77764
- href: linkData.href,
77765
- target: linkData.target,
77766
- rel: linkData.rel,
77767
- tooltip: linkData.tooltip,
77768
- element: elem,
77769
- clientX: event.clientX,
77770
- clientY: event.clientY
77771
- }
77772
- });
77773
- elem.dispatchEvent(linkClickEvent);
77774
- });
77775
77757
  }
77776
77758
  /**
77777
77759
  * Render a single run as an HTML element (span or anchor).
@@ -89110,255 +89092,6 @@ ${o}
89110
89092
  /** P3: Heavy debounce for full document layout */
89111
89093
  [Priority.P3]: 150
89112
89094
  });
89113
- const DEFAULT_MIME_TYPE$1 = "application/x-field-annotation";
89114
- const LEGACY_MIME_TYPE = "fieldAnnotation";
89115
- function parseIntSafe$1(value) {
89116
- if (!value) return void 0;
89117
- const parsed = parseInt(value, 10);
89118
- return Number.isFinite(parsed) ? parsed : void 0;
89119
- }
89120
- function extractFieldAnnotationData(element2) {
89121
- const dataset = element2.dataset;
89122
- const attributes = {};
89123
- for (const key2 in dataset) {
89124
- const value = dataset[key2];
89125
- if (value !== void 0) {
89126
- attributes[key2] = value;
89127
- }
89128
- }
89129
- return {
89130
- fieldId: dataset.fieldId,
89131
- fieldType: dataset.fieldType,
89132
- variant: dataset.variant ?? dataset.type,
89133
- displayLabel: dataset.displayLabel,
89134
- pmStart: parseIntSafe$1(dataset.pmStart),
89135
- pmEnd: parseIntSafe$1(dataset.pmEnd),
89136
- attributes
89137
- };
89138
- }
89139
- class DragHandler {
89140
- /**
89141
- * Creates a new DragHandler instance.
89142
- *
89143
- * @param container - The DOM container element (typically .superdoc-layout)
89144
- * @param config - Configuration options and callbacks
89145
- */
89146
- constructor(container, config2 = {}) {
89147
- this.container = container;
89148
- this.config = config2;
89149
- this.mimeType = config2.mimeType ?? DEFAULT_MIME_TYPE$1;
89150
- this.boundHandlers = {
89151
- dragstart: this.handleDragStart.bind(this),
89152
- dragover: this.handleDragOver.bind(this),
89153
- drop: this.handleDrop.bind(this),
89154
- dragend: this.handleDragEnd.bind(this),
89155
- dragleave: this.handleDragLeave.bind(this)
89156
- };
89157
- this.windowDragoverHandler = this.handleWindowDragOver.bind(this);
89158
- this.windowDropHandler = this.handleWindowDrop.bind(this);
89159
- this.attachListeners();
89160
- }
89161
- /**
89162
- * Attaches event listeners to the container and window.
89163
- */
89164
- attachListeners() {
89165
- this.container.addEventListener("dragstart", this.boundHandlers.dragstart);
89166
- this.container.addEventListener("dragover", this.boundHandlers.dragover);
89167
- this.container.addEventListener("drop", this.boundHandlers.drop);
89168
- this.container.addEventListener("dragend", this.boundHandlers.dragend);
89169
- this.container.addEventListener("dragleave", this.boundHandlers.dragleave);
89170
- window.addEventListener("dragover", this.windowDragoverHandler, false);
89171
- window.addEventListener("drop", this.windowDropHandler, false);
89172
- }
89173
- /**
89174
- * Removes event listeners from the container and window.
89175
- */
89176
- removeListeners() {
89177
- this.container.removeEventListener("dragstart", this.boundHandlers.dragstart);
89178
- this.container.removeEventListener("dragover", this.boundHandlers.dragover);
89179
- this.container.removeEventListener("drop", this.boundHandlers.drop);
89180
- this.container.removeEventListener("dragend", this.boundHandlers.dragend);
89181
- this.container.removeEventListener("dragleave", this.boundHandlers.dragleave);
89182
- window.removeEventListener("dragover", this.windowDragoverHandler, false);
89183
- window.removeEventListener("drop", this.windowDropHandler, false);
89184
- }
89185
- /**
89186
- * Handles dragover at window level to allow drops on overlay elements.
89187
- * This ensures preventDefault is called even when dragging over selection
89188
- * highlights or other UI elements that sit on top of the layout content.
89189
- */
89190
- handleWindowDragOver(event) {
89191
- if (this.hasFieldAnnotationData(event)) {
89192
- event.preventDefault();
89193
- if (event.dataTransfer) {
89194
- event.dataTransfer.dropEffect = "move";
89195
- }
89196
- const target = event.target;
89197
- if (!this.container.contains(target)) {
89198
- this.config.onDragOver?.({
89199
- event,
89200
- clientX: event.clientX,
89201
- clientY: event.clientY,
89202
- hasFieldAnnotation: true
89203
- });
89204
- }
89205
- }
89206
- }
89207
- /**
89208
- * Handles drop at window level to catch drops on overlay elements.
89209
- * If the drop target is outside the container, we process it here.
89210
- */
89211
- handleWindowDrop(event) {
89212
- if (this.hasFieldAnnotationData(event)) {
89213
- const target = event.target;
89214
- if (!this.container.contains(target)) {
89215
- this.handleDrop(event);
89216
- }
89217
- }
89218
- }
89219
- /**
89220
- * Handles the dragstart event.
89221
- * Sets up dataTransfer with field annotation data and drag image.
89222
- */
89223
- handleDragStart(event) {
89224
- const target = event.target;
89225
- if (!target?.dataset?.draggable || target.dataset.draggable !== "true") {
89226
- return;
89227
- }
89228
- const data = extractFieldAnnotationData(target);
89229
- if (event.dataTransfer) {
89230
- const jsonData = JSON.stringify({
89231
- attributes: data.attributes,
89232
- sourceField: data
89233
- });
89234
- event.dataTransfer.setData(this.mimeType, jsonData);
89235
- event.dataTransfer.setData(LEGACY_MIME_TYPE, jsonData);
89236
- event.dataTransfer.setData("text/plain", data.displayLabel ?? "Field Annotation");
89237
- event.dataTransfer.setDragImage(target, 0, 0);
89238
- event.dataTransfer.effectAllowed = "move";
89239
- }
89240
- this.config.onDragStart?.({
89241
- event,
89242
- element: target,
89243
- data
89244
- });
89245
- }
89246
- /**
89247
- * Handles the dragover event.
89248
- * Provides visual feedback and determines if drop is allowed.
89249
- */
89250
- handleDragOver(event) {
89251
- const hasFieldAnnotation = this.hasFieldAnnotationData(event);
89252
- if (hasFieldAnnotation) {
89253
- event.preventDefault();
89254
- if (event.dataTransfer) {
89255
- event.dataTransfer.dropEffect = "move";
89256
- }
89257
- this.container.classList.add("drag-over");
89258
- }
89259
- this.config.onDragOver?.({
89260
- event,
89261
- clientX: event.clientX,
89262
- clientY: event.clientY,
89263
- hasFieldAnnotation
89264
- });
89265
- }
89266
- /**
89267
- * Handles the dragleave event.
89268
- * Removes visual feedback when drag leaves the container.
89269
- */
89270
- handleDragLeave(event) {
89271
- const relatedTarget = event.relatedTarget;
89272
- if (!relatedTarget || !this.container.contains(relatedTarget)) {
89273
- this.container.classList.remove("drag-over");
89274
- }
89275
- }
89276
- /**
89277
- * Handles the drop event.
89278
- * Maps drop coordinates to ProseMirror position and emits drop event.
89279
- */
89280
- handleDrop(event) {
89281
- this.container.classList.remove("drag-over");
89282
- if (!this.hasFieldAnnotationData(event)) {
89283
- return;
89284
- }
89285
- event.preventDefault();
89286
- const data = this.extractDragData(event);
89287
- if (!data) {
89288
- return;
89289
- }
89290
- const pmPosition = clickToPositionDom(this.container, event.clientX, event.clientY);
89291
- this.config.onDrop?.({
89292
- event,
89293
- data,
89294
- pmPosition,
89295
- clientX: event.clientX,
89296
- clientY: event.clientY
89297
- });
89298
- }
89299
- /**
89300
- * Handles the dragend event.
89301
- * Cleans up drag state.
89302
- */
89303
- handleDragEnd(event) {
89304
- this.container.classList.remove("drag-over");
89305
- this.config.onDragEnd?.(event);
89306
- }
89307
- /**
89308
- * Checks if a drag event contains field annotation data.
89309
- */
89310
- hasFieldAnnotationData(event) {
89311
- if (!event.dataTransfer) {
89312
- return false;
89313
- }
89314
- const types2 = event.dataTransfer.types;
89315
- return types2.includes(this.mimeType) || types2.includes(LEGACY_MIME_TYPE);
89316
- }
89317
- /**
89318
- * Extracts field annotation data from a drag event's dataTransfer.
89319
- */
89320
- extractDragData(event) {
89321
- if (!event.dataTransfer) {
89322
- return null;
89323
- }
89324
- let jsonData = event.dataTransfer.getData(this.mimeType);
89325
- if (!jsonData) {
89326
- jsonData = event.dataTransfer.getData(LEGACY_MIME_TYPE);
89327
- }
89328
- if (!jsonData) {
89329
- return null;
89330
- }
89331
- try {
89332
- const parsed = JSON.parse(jsonData);
89333
- return parsed.sourceField ?? parsed.attributes ?? parsed;
89334
- } catch {
89335
- return null;
89336
- }
89337
- }
89338
- /**
89339
- * Updates the configuration options.
89340
- *
89341
- * @param config - New configuration options to merge
89342
- */
89343
- updateConfig(config2) {
89344
- this.config = { ...this.config, ...config2 };
89345
- if (config2.mimeType) {
89346
- this.mimeType = config2.mimeType;
89347
- }
89348
- }
89349
- /**
89350
- * Destroys the drag handler and removes all event listeners.
89351
- * Call this when the layout engine is unmounted or the container is removed.
89352
- */
89353
- destroy() {
89354
- this.removeListeners();
89355
- this.container.classList.remove("drag-over");
89356
- }
89357
- }
89358
- function createDragHandler(container, config2 = {}) {
89359
- const handler2 = new DragHandler(container, config2);
89360
- return () => handler2.destroy();
89361
- }
89362
89095
  const isAtomicFragment = (fragment) => {
89363
89096
  return fragment.kind === "drawing" || fragment.kind === "image";
89364
89097
  };
@@ -90922,229 +90655,1425 @@ ${o}
90922
90655
  this.#isSetup = false;
90923
90656
  }
90924
90657
  }
90925
- const createDefaultScheduler = () => {
90926
- if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
90927
- return {
90928
- requestAnimationFrame: (cb) => window.requestAnimationFrame(cb),
90929
- cancelAnimationFrame: (handle2) => window.cancelAnimationFrame(handle2)
90930
- };
90658
+ const WORD_CHARACTER_REGEX = /[\p{L}\p{N}'\u2018\u2019_~-]/u;
90659
+ function isWordCharacter(char) {
90660
+ if (!char) {
90661
+ return false;
90931
90662
  }
90932
- const anyGlobal = globalThis;
90933
- if (typeof anyGlobal.requestAnimationFrame === "function" && typeof anyGlobal.cancelAnimationFrame === "function") {
90934
- return {
90935
- requestAnimationFrame: (cb) => anyGlobal.requestAnimationFrame(cb),
90936
- cancelAnimationFrame: (handle2) => anyGlobal.cancelAnimationFrame(handle2)
90937
- };
90663
+ return WORD_CHARACTER_REGEX.test(char);
90664
+ }
90665
+ function calculateExtendedSelection(blocks2, anchor, head, mode) {
90666
+ if (mode === "word") {
90667
+ const anchorBounds = findWordBoundaries(blocks2, anchor);
90668
+ const headBounds = findWordBoundaries(blocks2, head);
90669
+ if (anchorBounds && headBounds) {
90670
+ if (head >= anchor) {
90671
+ return { selAnchor: anchorBounds.from, selHead: headBounds.to };
90672
+ } else {
90673
+ return { selAnchor: anchorBounds.to, selHead: headBounds.from };
90674
+ }
90675
+ }
90676
+ } else if (mode === "para") {
90677
+ const anchorBounds = findParagraphBoundaries(blocks2, anchor);
90678
+ const headBounds = findParagraphBoundaries(blocks2, head);
90679
+ if (anchorBounds && headBounds) {
90680
+ if (head >= anchor) {
90681
+ return { selAnchor: anchorBounds.from, selHead: headBounds.to };
90682
+ } else {
90683
+ return { selAnchor: anchorBounds.to, selHead: headBounds.from };
90684
+ }
90685
+ }
90938
90686
  }
90687
+ return { selAnchor: anchor, selHead: head };
90688
+ }
90689
+ function registerPointerClick(event, previous2, options) {
90690
+ const time2 = event.timeStamp ?? performance.now();
90691
+ const timeDelta = time2 - previous2.lastClickTime;
90692
+ const withinTime = timeDelta <= options.timeThresholdMs;
90693
+ const distanceX = Math.abs(event.clientX - previous2.lastClickPosition.x);
90694
+ const distanceY = Math.abs(event.clientY - previous2.lastClickPosition.y);
90695
+ const withinDistance = distanceX <= options.distanceThresholdPx && distanceY <= options.distanceThresholdPx;
90696
+ const clickCount = withinTime && withinDistance ? Math.min(previous2.clickCount + 1, options.maxClickCount) : 1;
90939
90697
  return {
90940
- requestAnimationFrame: (cb) => {
90941
- const handle2 = anyGlobal.setTimeout?.(() => cb(Date.now()), 0);
90942
- return handle2;
90943
- },
90944
- cancelAnimationFrame: (handle2) => {
90945
- anyGlobal.clearTimeout?.(handle2);
90946
- }
90698
+ clickCount,
90699
+ lastClickTime: time2,
90700
+ lastClickPosition: { x: event.clientX, y: event.clientY }
90947
90701
  };
90948
- };
90949
- class SelectionSyncCoordinator extends EventEmitter$1 {
90950
- #docEpoch = 0;
90951
- #layoutEpoch = 0;
90952
- #layoutUpdating = false;
90953
- #pending = false;
90954
- #scheduled = false;
90955
- #rafHandle = null;
90956
- #scheduler;
90957
- /**
90958
- * Creates a new SelectionSyncCoordinator.
90959
- *
90960
- * @param options - Configuration options
90961
- * @param options.scheduler - Custom scheduler for animation frames (useful for testing), defaults to platform scheduler
90962
- */
90963
- constructor(options) {
90964
- super();
90965
- this.#scheduler = options?.scheduler ?? createDefaultScheduler();
90702
+ }
90703
+ function getFirstTextPosition(doc2) {
90704
+ if (!doc2 || !doc2.content) {
90705
+ return 1;
90966
90706
  }
90967
- /**
90968
- * Gets the current document epoch.
90969
- *
90970
- * @returns The document epoch (increments on each document-changing transaction)
90971
- */
90972
- getDocEpoch() {
90973
- return this.#docEpoch;
90707
+ let validPos = 1;
90708
+ doc2.nodesBetween(0, doc2.content.size, (node2, pos) => {
90709
+ if (node2.isTextblock) {
90710
+ validPos = pos + 1;
90711
+ return false;
90712
+ }
90713
+ return true;
90714
+ });
90715
+ return validPos;
90716
+ }
90717
+ function computeWordSelectionRangeAt(state, pos) {
90718
+ if (!state?.doc) {
90719
+ return null;
90974
90720
  }
90975
- /**
90976
- * Gets the current layout epoch.
90977
- *
90978
- * @returns The epoch of the document version currently painted in the DOM
90979
- */
90980
- getLayoutEpoch() {
90981
- return this.#layoutEpoch;
90721
+ if (pos < 0 || pos > state.doc.content.size) {
90722
+ return null;
90982
90723
  }
90983
- /**
90984
- * Checks if a layout update is currently in progress.
90985
- *
90986
- * @returns True if between onLayoutStart() and onLayoutComplete(), false otherwise
90987
- */
90988
- isLayoutUpdating() {
90989
- return this.#layoutUpdating;
90724
+ const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
90725
+ if (!textblockPos) {
90726
+ return null;
90990
90727
  }
90991
- /**
90992
- * Updates the document epoch and triggers conditional rendering.
90993
- *
90994
- * @param epoch - The new document epoch (must be finite and non-negative)
90995
- *
90996
- * @remarks
90997
- * When the document epoch changes:
90998
- * 1. Any scheduled render is cancelled (layout will be out of sync)
90999
- * 2. If layout has already caught up, rendering is rescheduled
91000
- *
91001
- * Calling with the same epoch as the current value is a no-op.
91002
- * Invalid epoch values are silently ignored.
91003
- */
91004
- setDocEpoch(epoch) {
91005
- if (!Number.isFinite(epoch) || epoch < 0) return;
91006
- if (epoch === this.#docEpoch) return;
91007
- this.#docEpoch = epoch;
91008
- this.#cancelScheduledRender();
91009
- this.#maybeSchedule();
90728
+ const parentStart = textblockPos.start();
90729
+ const parentEnd = textblockPos.end();
90730
+ const sampleEnd = Math.min(pos + 1, parentEnd);
90731
+ const charAtPos = state.doc.textBetween(pos, sampleEnd, "\0", "\0");
90732
+ if (!isWordCharacter(charAtPos)) {
90733
+ return null;
91010
90734
  }
91011
- /**
91012
- * Notifies the coordinator that layout computation has started.
91013
- *
91014
- * @remarks
91015
- * Marks the layout as updating and cancels any scheduled renders, since the DOM
91016
- * is about to change and current position data will be stale.
91017
- *
91018
- * Safe to call multiple times (e.g., if layouts overlap) - subsequent calls are ignored
91019
- * until onLayoutComplete() is called.
91020
- */
91021
- onLayoutStart() {
91022
- if (this.#layoutUpdating) return;
91023
- this.#layoutUpdating = true;
91024
- this.#cancelScheduledRender();
90735
+ let startPos = pos;
90736
+ while (startPos > parentStart) {
90737
+ const prevChar = state.doc.textBetween(startPos - 1, startPos, "\0", "\0");
90738
+ if (!isWordCharacter(prevChar)) {
90739
+ break;
90740
+ }
90741
+ startPos -= 1;
91025
90742
  }
91026
- /**
91027
- * Notifies the coordinator that layout painting has completed.
91028
- *
91029
- * @param layoutEpoch - The document epoch that was just painted to the DOM
91030
- *
91031
- * @remarks
91032
- * Marks the layout as no longer updating, records the new layout epoch, and attempts
91033
- * to schedule rendering if conditions are now safe.
91034
- *
91035
- * If the layoutEpoch is invalid (not a finite non-negative number), it is ignored and
91036
- * the previous layoutEpoch value is retained.
91037
- *
91038
- * This method is the primary trigger for selection rendering - if there's a pending
91039
- * render request and layoutEpoch >= docEpoch, a render event will be scheduled.
91040
- */
91041
- onLayoutComplete(layoutEpoch) {
91042
- this.#layoutUpdating = false;
91043
- if (Number.isFinite(layoutEpoch) && layoutEpoch >= 0) {
91044
- this.#layoutEpoch = layoutEpoch;
90743
+ let endPos = pos;
90744
+ while (endPos < parentEnd) {
90745
+ const nextChar = state.doc.textBetween(endPos, endPos + 1, "\0", "\0");
90746
+ if (!isWordCharacter(nextChar)) {
90747
+ break;
91045
90748
  }
91046
- this.#maybeSchedule();
90749
+ endPos += 1;
91047
90750
  }
91048
- /**
91049
- * Notifies the coordinator that layout was aborted without completing.
91050
- *
91051
- * @remarks
91052
- * Marks the layout as no longer updating (without updating layoutEpoch) and attempts
91053
- * to schedule rendering if conditions are safe.
91054
- *
91055
- * Use this when layout computation is cancelled or fails partway through.
91056
- */
91057
- onLayoutAbort() {
91058
- this.#layoutUpdating = false;
91059
- this.#maybeSchedule();
90751
+ if (startPos === endPos) {
90752
+ return null;
91060
90753
  }
91061
- /**
91062
- * Requests that selection rendering occur when conditions become safe.
91063
- *
91064
- * @param options - Rendering options
91065
- * @param options.immediate - If true, attempts to render immediately (synchronously) if safe, defaults to false
91066
- *
91067
- * @remarks
91068
- * Marks a render as pending and schedules it to occur on the next animation frame if
91069
- * conditions are safe (layout not updating, layoutEpoch >= docEpoch).
91070
- *
91071
- * If options.immediate is true, also attempts a synchronous render before scheduling.
91072
- * Use immediate rendering sparingly, as it can cause multiple renders per frame.
91073
- *
91074
- * Multiple calls are coalesced - only one render will occur per animation frame.
91075
- */
91076
- requestRender(options) {
91077
- this.#pending = true;
91078
- if (options?.immediate) {
91079
- this.flushNow();
90754
+ return { from: startPos, to: endPos };
90755
+ }
90756
+ function computeParagraphSelectionRangeAt(state, pos) {
90757
+ if (!state?.doc) {
90758
+ return null;
90759
+ }
90760
+ const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
90761
+ if (!textblockPos) {
90762
+ return null;
90763
+ }
90764
+ return { from: textblockPos.start(), to: textblockPos.end() };
90765
+ }
90766
+ function findNearestTextblockResolvedPos(doc2, pos) {
90767
+ const $pos = doc2.resolve(pos);
90768
+ let textblockPos = $pos;
90769
+ while (textblockPos.depth > 0) {
90770
+ if (textblockPos.parent?.isTextblock) {
90771
+ break;
91080
90772
  }
91081
- this.#maybeSchedule();
90773
+ if (!textblockPos.parent || textblockPos.depth === 0) {
90774
+ break;
90775
+ }
90776
+ const beforePos = textblockPos.before();
90777
+ if (beforePos < 0 || beforePos > doc2.content.size) {
90778
+ return null;
90779
+ }
90780
+ textblockPos = doc2.resolve(beforePos);
91082
90781
  }
91083
- /**
91084
- * Attempts to render selection immediately (synchronously) if conditions are safe.
91085
- *
91086
- * @remarks
91087
- * If there's a pending render request and conditions are safe (layout not updating,
91088
- * layoutEpoch >= docEpoch), this method:
91089
- * 1. Cancels any scheduled asynchronous render
91090
- * 2. Clears the pending flag
91091
- * 3. Emits the 'render' event synchronously
91092
- *
91093
- * If no render is pending or conditions are not safe, this is a no-op.
91094
- *
91095
- * Use this for immediate selection updates in response to user actions (e.g., click
91096
- * handlers) where waiting for the next animation frame would cause noticeable lag.
91097
- */
91098
- flushNow() {
91099
- if (!this.#pending) return;
91100
- if (!this.#isSafeToRender()) return;
91101
- this.#cancelScheduledRender();
91102
- this.#pending = false;
91103
- this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
90782
+ if (!textblockPos.parent?.isTextblock) {
90783
+ return null;
91104
90784
  }
91105
- /**
91106
- * Permanently tears down the coordinator, cancelling pending renders and removing all listeners.
91107
- *
91108
- * @remarks
91109
- * After calling destroy(), this instance should not be used again. All scheduled renders
91110
- * are cancelled and all event listeners are removed.
91111
- *
91112
- * Safe to call multiple times.
91113
- */
91114
- destroy() {
91115
- this.#cancelScheduledRender();
91116
- this.removeAllListeners();
90785
+ return textblockPos;
90786
+ }
90787
+ function getCellPosFromTableHit(tableHit, doc2, blocks2) {
90788
+ if (!tableHit || !tableHit.block || typeof tableHit.block.id !== "string") {
90789
+ console.warn("[getCellPosFromTableHit] Invalid tableHit input:", tableHit);
90790
+ return null;
91117
90791
  }
91118
- #isSafeToRender() {
91119
- return !this.#layoutUpdating && this.#layoutEpoch >= this.#docEpoch;
90792
+ if (typeof tableHit.cellRowIndex !== "number" || typeof tableHit.cellColIndex !== "number" || tableHit.cellRowIndex < 0 || tableHit.cellColIndex < 0) {
90793
+ console.warn("[getCellPosFromTableHit] Invalid cell indices:", {
90794
+ row: tableHit.cellRowIndex,
90795
+ col: tableHit.cellColIndex
90796
+ });
90797
+ return null;
91120
90798
  }
91121
- #maybeSchedule() {
91122
- if (!this.#pending) return;
91123
- if (!this.#isSafeToRender()) return;
91124
- if (this.#scheduled) return;
91125
- this.#scheduled = true;
91126
- this.#rafHandle = this.#scheduler.requestAnimationFrame(() => {
91127
- this.#scheduled = false;
91128
- this.#rafHandle = null;
91129
- if (!this.#pending) return;
91130
- if (!this.#isSafeToRender()) return;
91131
- this.#pending = false;
91132
- this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
90799
+ if (!doc2) return null;
90800
+ const tableBlocks = blocks2.filter((b2) => b2.kind === "table");
90801
+ const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
90802
+ if (targetTableIndex === -1) return null;
90803
+ let tablePos = null;
90804
+ let currentTableIndex = 0;
90805
+ try {
90806
+ doc2.descendants((node2, pos) => {
90807
+ if (node2.type.name === "table") {
90808
+ if (currentTableIndex === targetTableIndex) {
90809
+ tablePos = pos;
90810
+ return false;
90811
+ }
90812
+ currentTableIndex++;
90813
+ }
90814
+ return true;
91133
90815
  });
90816
+ } catch (error) {
90817
+ console.error("[getCellPosFromTableHit] Error during document traversal:", error);
90818
+ return null;
91134
90819
  }
91135
- #cancelScheduledRender() {
91136
- if (this.#rafHandle != null) {
91137
- try {
91138
- this.#scheduler.cancelAnimationFrame(this.#rafHandle);
91139
- } finally {
91140
- this.#rafHandle = null;
90820
+ if (tablePos === null) return null;
90821
+ const tableNode = doc2.nodeAt(tablePos);
90822
+ if (!tableNode || tableNode.type.name !== "table") return null;
90823
+ const targetRowIndex = tableHit.cellRowIndex;
90824
+ const targetColIndex = tableHit.cellColIndex;
90825
+ if (targetRowIndex >= tableNode.childCount) {
90826
+ console.warn("[getCellPosFromTableHit] Target row index out of bounds:", {
90827
+ targetRowIndex,
90828
+ tableChildCount: tableNode.childCount
90829
+ });
90830
+ return null;
90831
+ }
90832
+ let currentPos = tablePos + 1;
90833
+ for (let r2 = 0; r2 < tableNode.childCount && r2 <= targetRowIndex; r2++) {
90834
+ const row2 = tableNode.child(r2);
90835
+ if (r2 === targetRowIndex) {
90836
+ currentPos += 1;
90837
+ let logicalCol = 0;
90838
+ for (let cellIndex = 0; cellIndex < row2.childCount; cellIndex++) {
90839
+ const cell2 = row2.child(cellIndex);
90840
+ const rawColspan = cell2.attrs?.colspan;
90841
+ const colspan = typeof rawColspan === "number" && Number.isFinite(rawColspan) && rawColspan > 0 ? rawColspan : 1;
90842
+ if (targetColIndex >= logicalCol && targetColIndex < logicalCol + colspan) {
90843
+ return currentPos;
90844
+ }
90845
+ currentPos += cell2.nodeSize;
90846
+ logicalCol += colspan;
91141
90847
  }
90848
+ console.warn("[getCellPosFromTableHit] Target column not found in row:", {
90849
+ targetColIndex,
90850
+ logicalColReached: logicalCol,
90851
+ rowCellCount: row2.childCount
90852
+ });
90853
+ return null;
90854
+ } else {
90855
+ currentPos += row2.nodeSize;
91142
90856
  }
91143
- this.#scheduled = false;
91144
90857
  }
90858
+ return null;
91145
90859
  }
91146
- const uiSurfaces = /* @__PURE__ */ new WeakSet();
91147
- function isInRegisteredSurface(event) {
90860
+ function getTablePosFromHit(tableHit, doc2, blocks2) {
90861
+ if (!doc2) return null;
90862
+ const tableBlocks = blocks2.filter((b2) => b2.kind === "table");
90863
+ const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
90864
+ if (targetTableIndex === -1) return null;
90865
+ let tablePos = null;
90866
+ let currentTableIndex = 0;
90867
+ doc2.descendants((node2, pos) => {
90868
+ if (node2.type.name === "table") {
90869
+ if (currentTableIndex === targetTableIndex) {
90870
+ tablePos = pos;
90871
+ return false;
90872
+ }
90873
+ currentTableIndex++;
90874
+ }
90875
+ return true;
90876
+ });
90877
+ return tablePos;
90878
+ }
90879
+ function shouldUseCellSelection(currentTableHit, cellAnchor, cellDragMode) {
90880
+ if (!cellAnchor) return false;
90881
+ if (!currentTableHit) return cellDragMode === "active";
90882
+ if (currentTableHit.block.id !== cellAnchor.tableBlockId) {
90883
+ return cellDragMode === "active";
90884
+ }
90885
+ const sameCell = currentTableHit.cellRowIndex === cellAnchor.cellRowIndex && currentTableHit.cellColIndex === cellAnchor.cellColIndex;
90886
+ if (!sameCell) {
90887
+ return true;
90888
+ }
90889
+ return cellDragMode === "active";
90890
+ }
90891
+ function hitTestTable(layout, blocks2, measures, normalizedX, normalizedY, configuredPageHeight, pageGapFallback, geometryHelper) {
90892
+ if (!layout) {
90893
+ return null;
90894
+ }
90895
+ let pageY = 0;
90896
+ let pageHit = null;
90897
+ if (geometryHelper) {
90898
+ const idx = geometryHelper.getPageIndexAtY(normalizedY) ?? geometryHelper.getNearestPageIndex(normalizedY);
90899
+ if (idx != null && layout.pages[idx]) {
90900
+ pageHit = { pageIndex: idx, page: layout.pages[idx] };
90901
+ pageY = geometryHelper.getPageTop(idx);
90902
+ }
90903
+ }
90904
+ if (!pageHit) {
90905
+ const gap = layout.pageGap ?? pageGapFallback;
90906
+ for (let i2 = 0; i2 < layout.pages.length; i2++) {
90907
+ const page = layout.pages[i2];
90908
+ const pageHeight = page.size?.h ?? configuredPageHeight;
90909
+ if (normalizedY >= pageY && normalizedY < pageY + pageHeight) {
90910
+ pageHit = { pageIndex: i2, page };
90911
+ break;
90912
+ }
90913
+ pageY += pageHeight + gap;
90914
+ }
90915
+ }
90916
+ if (!pageHit) {
90917
+ return null;
90918
+ }
90919
+ const pageRelativeY = normalizedY - pageY;
90920
+ const point2 = { x: normalizedX, y: pageRelativeY };
90921
+ return hitTestTableFragment(pageHit, blocks2, measures, point2);
90922
+ }
90923
+ const MULTI_CLICK_TIME_THRESHOLD_MS = 400;
90924
+ const MULTI_CLICK_DISTANCE_THRESHOLD_PX = 5;
90925
+ class EditorInputManager {
90926
+ // Dependencies
90927
+ #deps = null;
90928
+ #callbacks = {};
90929
+ // Drag selection state
90930
+ #isDragging = false;
90931
+ #dragAnchor = null;
90932
+ #dragAnchorPageIndex = null;
90933
+ #dragExtensionMode = "char";
90934
+ #dragLastPointer = null;
90935
+ #dragLastRawHit = null;
90936
+ #dragUsedPageNotMountedFallback = false;
90937
+ // Click tracking for multi-click detection
90938
+ #clickCount = 0;
90939
+ #lastClickTime = 0;
90940
+ #lastClickPosition = null;
90941
+ // Cell selection state
90942
+ #cellAnchor = null;
90943
+ #cellDragMode = "none";
90944
+ // Margin click state
90945
+ #pendingMarginClick = null;
90946
+ // Image selection state
90947
+ #lastSelectedImageBlockId = null;
90948
+ // Focus suppression (for draggable annotations)
90949
+ #suppressFocusInFromDraggable = false;
90950
+ // Debug state
90951
+ #debugLastPointer = null;
90952
+ #debugLastHit = null;
90953
+ // Bound handlers for event listener cleanup
90954
+ #boundHandlePointerDown = null;
90955
+ #boundHandlePointerMove = null;
90956
+ #boundHandlePointerUp = null;
90957
+ #boundHandlePointerLeave = null;
90958
+ #boundHandleDoubleClick = null;
90959
+ #boundHandleClick = null;
90960
+ #boundHandleKeyDown = null;
90961
+ #boundHandleFocusIn = null;
90962
+ // ==========================================================================
90963
+ // Constructor
90964
+ // ==========================================================================
90965
+ constructor() {
90966
+ }
90967
+ // ==========================================================================
90968
+ // Setup Methods
90969
+ // ==========================================================================
90970
+ /**
90971
+ * Set dependencies from PresentationEditor.
90972
+ */
90973
+ setDependencies(deps) {
90974
+ this.#deps = deps;
90975
+ }
90976
+ /**
90977
+ * Set callbacks for events.
90978
+ */
90979
+ setCallbacks(callbacks2) {
90980
+ this.#callbacks = callbacks2;
90981
+ }
90982
+ /**
90983
+ * Bind event listeners to DOM elements.
90984
+ */
90985
+ bind() {
90986
+ if (!this.#deps) return;
90987
+ const viewportHost = this.#deps.getViewportHost();
90988
+ const visibleHost = this.#deps.getVisibleHost();
90989
+ viewportHost.ownerDocument ?? document;
90990
+ this.#boundHandlePointerDown = this.#handlePointerDown.bind(this);
90991
+ this.#boundHandlePointerMove = this.#handlePointerMove.bind(this);
90992
+ this.#boundHandlePointerUp = this.#handlePointerUp.bind(this);
90993
+ this.#boundHandlePointerLeave = this.#handlePointerLeave.bind(this);
90994
+ this.#boundHandleDoubleClick = this.#handleDoubleClick.bind(this);
90995
+ this.#boundHandleClick = this.#handleClick.bind(this);
90996
+ this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
90997
+ this.#boundHandleFocusIn = this.#handleFocusIn.bind(this);
90998
+ viewportHost.addEventListener("pointerdown", this.#boundHandlePointerDown);
90999
+ viewportHost.addEventListener("pointermove", this.#boundHandlePointerMove);
91000
+ viewportHost.addEventListener("pointerup", this.#boundHandlePointerUp);
91001
+ viewportHost.addEventListener("pointerleave", this.#boundHandlePointerLeave);
91002
+ viewportHost.addEventListener("dblclick", this.#boundHandleDoubleClick);
91003
+ viewportHost.addEventListener("click", this.#boundHandleClick);
91004
+ const container = viewportHost.closest(".presentation-editor");
91005
+ if (container) {
91006
+ container.addEventListener("keydown", this.#boundHandleKeyDown);
91007
+ }
91008
+ visibleHost.addEventListener("focusin", this.#boundHandleFocusIn);
91009
+ }
91010
+ /**
91011
+ * Unbind event listeners.
91012
+ */
91013
+ unbind() {
91014
+ if (!this.#deps) return;
91015
+ const viewportHost = this.#deps.getViewportHost();
91016
+ const visibleHost = this.#deps.getVisibleHost();
91017
+ if (this.#boundHandlePointerDown) {
91018
+ viewportHost.removeEventListener("pointerdown", this.#boundHandlePointerDown);
91019
+ }
91020
+ if (this.#boundHandlePointerMove) {
91021
+ viewportHost.removeEventListener("pointermove", this.#boundHandlePointerMove);
91022
+ }
91023
+ if (this.#boundHandlePointerUp) {
91024
+ viewportHost.removeEventListener("pointerup", this.#boundHandlePointerUp);
91025
+ }
91026
+ if (this.#boundHandlePointerLeave) {
91027
+ viewportHost.removeEventListener("pointerleave", this.#boundHandlePointerLeave);
91028
+ }
91029
+ if (this.#boundHandleDoubleClick) {
91030
+ viewportHost.removeEventListener("dblclick", this.#boundHandleDoubleClick);
91031
+ }
91032
+ if (this.#boundHandleClick) {
91033
+ viewportHost.removeEventListener("click", this.#boundHandleClick);
91034
+ }
91035
+ if (this.#boundHandleKeyDown) {
91036
+ const container = viewportHost.closest(".presentation-editor");
91037
+ if (container) {
91038
+ container.removeEventListener("keydown", this.#boundHandleKeyDown);
91039
+ }
91040
+ }
91041
+ if (this.#boundHandleFocusIn) {
91042
+ visibleHost.removeEventListener("focusin", this.#boundHandleFocusIn);
91043
+ }
91044
+ this.#boundHandlePointerDown = null;
91045
+ this.#boundHandlePointerMove = null;
91046
+ this.#boundHandlePointerUp = null;
91047
+ this.#boundHandlePointerLeave = null;
91048
+ this.#boundHandleDoubleClick = null;
91049
+ this.#boundHandleClick = null;
91050
+ this.#boundHandleKeyDown = null;
91051
+ this.#boundHandleFocusIn = null;
91052
+ }
91053
+ /**
91054
+ * Destroy the manager and clean up.
91055
+ */
91056
+ destroy() {
91057
+ this.unbind();
91058
+ this.#deps = null;
91059
+ this.#callbacks = {};
91060
+ this.#clearDragState();
91061
+ this.#clearCellAnchor();
91062
+ }
91063
+ // ==========================================================================
91064
+ // Public Getters
91065
+ // ==========================================================================
91066
+ /** Whether currently dragging */
91067
+ get isDragging() {
91068
+ return this.#isDragging;
91069
+ }
91070
+ /** Current drag anchor position */
91071
+ get dragAnchor() {
91072
+ return this.#dragAnchor;
91073
+ }
91074
+ /** Cell anchor state for table selection */
91075
+ get cellAnchor() {
91076
+ return this.#cellAnchor;
91077
+ }
91078
+ /** Debug last pointer position */
91079
+ get debugLastPointer() {
91080
+ return this.#debugLastPointer;
91081
+ }
91082
+ /** Debug last hit */
91083
+ get debugLastHit() {
91084
+ return this.#debugLastHit;
91085
+ }
91086
+ /** Last selected image block ID */
91087
+ get lastSelectedImageBlockId() {
91088
+ return this.#lastSelectedImageBlockId;
91089
+ }
91090
+ /** Drag anchor page index */
91091
+ get dragAnchorPageIndex() {
91092
+ return this.#dragAnchorPageIndex;
91093
+ }
91094
+ /** Get the page index from the last raw hit during drag */
91095
+ get dragLastHitPageIndex() {
91096
+ return this.#dragLastRawHit?.pageIndex ?? null;
91097
+ }
91098
+ /** Get the last raw hit during drag (for finalization) */
91099
+ get dragLastRawHit() {
91100
+ return this.#dragLastRawHit;
91101
+ }
91102
+ // ==========================================================================
91103
+ // Public Methods
91104
+ // ==========================================================================
91105
+ /**
91106
+ * Clear cell anchor (used when document changes).
91107
+ */
91108
+ clearCellAnchor() {
91109
+ this.#clearCellAnchor();
91110
+ }
91111
+ /**
91112
+ * Set suppress focus in flag (for draggable annotations).
91113
+ */
91114
+ setSuppressFocusInFromDraggable(value) {
91115
+ this.#suppressFocusInFromDraggable = value;
91116
+ }
91117
+ // ==========================================================================
91118
+ // Private Helper Methods
91119
+ // ==========================================================================
91120
+ #clearDragState() {
91121
+ this.#isDragging = false;
91122
+ this.#dragAnchor = null;
91123
+ this.#dragAnchorPageIndex = null;
91124
+ this.#dragExtensionMode = "char";
91125
+ this.#dragLastPointer = null;
91126
+ this.#dragLastRawHit = null;
91127
+ this.#dragUsedPageNotMountedFallback = false;
91128
+ }
91129
+ #clearCellAnchor() {
91130
+ this.#cellAnchor = null;
91131
+ this.#cellDragMode = "none";
91132
+ }
91133
+ #registerPointerClick(event) {
91134
+ const nextState = registerPointerClick(
91135
+ event,
91136
+ {
91137
+ clickCount: this.#clickCount,
91138
+ lastClickTime: this.#lastClickTime,
91139
+ lastClickPosition: this.#lastClickPosition ?? { x: 0, y: 0 }
91140
+ },
91141
+ {
91142
+ timeThresholdMs: MULTI_CLICK_TIME_THRESHOLD_MS,
91143
+ distanceThresholdPx: MULTI_CLICK_DISTANCE_THRESHOLD_PX,
91144
+ maxClickCount: 3
91145
+ }
91146
+ );
91147
+ this.#clickCount = nextState.clickCount;
91148
+ this.#lastClickTime = nextState.lastClickTime;
91149
+ this.#lastClickPosition = nextState.lastClickPosition;
91150
+ return nextState.clickCount;
91151
+ }
91152
+ #getFirstTextPosition() {
91153
+ const editor = this.#deps?.getEditor();
91154
+ return getFirstTextPosition(editor?.state?.doc ?? null);
91155
+ }
91156
+ #calculateExtendedSelection(anchor, head, mode) {
91157
+ const layoutState = this.#deps?.getLayoutState();
91158
+ return calculateExtendedSelection(layoutState?.blocks ?? [], anchor, head, mode);
91159
+ }
91160
+ #shouldUseCellSelection(currentTableHit) {
91161
+ return shouldUseCellSelection(currentTableHit, this.#cellAnchor, this.#cellDragMode);
91162
+ }
91163
+ #getCellPosFromTableHit(tableHit) {
91164
+ const editor = this.#deps?.getEditor();
91165
+ const layoutState = this.#deps?.getLayoutState();
91166
+ return getCellPosFromTableHit(tableHit, editor?.state?.doc ?? null, layoutState?.blocks ?? []);
91167
+ }
91168
+ #getTablePosFromHit(tableHit) {
91169
+ const editor = this.#deps?.getEditor();
91170
+ const layoutState = this.#deps?.getLayoutState();
91171
+ return getTablePosFromHit(tableHit, editor?.state?.doc ?? null, layoutState?.blocks ?? []);
91172
+ }
91173
+ #setCellAnchor(tableHit, tablePos) {
91174
+ const cellPos = this.#getCellPosFromTableHit(tableHit);
91175
+ if (cellPos === null) return;
91176
+ this.#cellAnchor = {
91177
+ tablePos,
91178
+ cellPos,
91179
+ cellRowIndex: tableHit.cellRowIndex,
91180
+ cellColIndex: tableHit.cellColIndex,
91181
+ tableBlockId: tableHit.block.id
91182
+ };
91183
+ this.#cellDragMode = "pending";
91184
+ }
91185
+ #hitTestTable(x2, y2) {
91186
+ return this.#callbacks.hitTestTable?.(x2, y2) ?? null;
91187
+ }
91188
+ // ==========================================================================
91189
+ // Event Handlers
91190
+ // ==========================================================================
91191
+ /**
91192
+ * Handle click events - specifically for link navigation prevention.
91193
+ *
91194
+ * Link handling is split between pointerdown and click:
91195
+ * - pointerdown: dispatches superdoc-link-click event (for popover/UI response)
91196
+ * - click: prevents default navigation (preventDefault only works on click, not pointerdown)
91197
+ *
91198
+ * This also handles keyboard activation (Enter/Space) which triggers click but not pointerdown.
91199
+ */
91200
+ #handleClick(event) {
91201
+ const target = event.target;
91202
+ const linkEl = target?.closest?.("a.superdoc-link");
91203
+ if (linkEl) {
91204
+ event.preventDefault();
91205
+ if (!event.pointerId && event.detail === 0) {
91206
+ this.#handleLinkClick(event, linkEl);
91207
+ }
91208
+ }
91209
+ }
91210
+ #handlePointerDown(event) {
91211
+ if (!this.#deps) return;
91212
+ if (event.button !== 0) return;
91213
+ if (event.ctrlKey && navigator.platform.includes("Mac")) return;
91214
+ this.#pendingMarginClick = null;
91215
+ const target = event.target;
91216
+ if (target?.closest?.(".superdoc-ruler-handle") != null) return;
91217
+ const linkEl = target?.closest?.("a.superdoc-link");
91218
+ if (linkEl) {
91219
+ this.#handleLinkClick(event, linkEl);
91220
+ return;
91221
+ }
91222
+ const annotationEl = target?.closest?.(".annotation[data-pm-start]");
91223
+ const isDraggableAnnotation = target?.closest?.('[data-draggable="true"]') != null;
91224
+ this.#suppressFocusInFromDraggable = isDraggableAnnotation;
91225
+ if (annotationEl) {
91226
+ this.#handleAnnotationClick(event, annotationEl);
91227
+ return;
91228
+ }
91229
+ const layoutState = this.#deps.getLayoutState();
91230
+ if (!layoutState.layout) {
91231
+ this.#handleClickWithoutLayout(event, isDraggableAnnotation);
91232
+ return;
91233
+ }
91234
+ const normalizedPoint = this.#callbacks.normalizeClientPoint?.(event.clientX, event.clientY);
91235
+ if (!normalizedPoint) return;
91236
+ const { x: x2, y: y2 } = normalizedPoint;
91237
+ this.#debugLastPointer = { clientX: event.clientX, clientY: event.clientY, x: x2, y: y2 };
91238
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
91239
+ if (sessionMode !== "body") {
91240
+ if (this.#handleClickInHeaderFooterMode(event, x2, y2)) return;
91241
+ }
91242
+ const headerFooterRegion = this.#callbacks.hitTestHeaderFooterRegion?.(x2, y2);
91243
+ if (headerFooterRegion) return;
91244
+ const viewportHost = this.#deps.getViewportHost();
91245
+ const pageGeometryHelper = this.#deps.getPageGeometryHelper();
91246
+ const rawHit = clickToPosition(
91247
+ layoutState.layout,
91248
+ layoutState.blocks,
91249
+ layoutState.measures,
91250
+ { x: x2, y: y2 },
91251
+ viewportHost,
91252
+ event.clientX,
91253
+ event.clientY,
91254
+ pageGeometryHelper ?? void 0
91255
+ );
91256
+ const editor = this.#deps.getEditor();
91257
+ const doc2 = editor.state?.doc;
91258
+ const epochMapper = this.#deps.getEpochMapper();
91259
+ const mapped = rawHit && doc2 ? epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1) : null;
91260
+ if (mapped && !mapped.ok) {
91261
+ debugLog("warn", "pointerdown mapping failed", mapped);
91262
+ }
91263
+ const hit = rawHit && doc2 && mapped?.ok ? { ...rawHit, pos: Math.max(0, Math.min(mapped.pos, doc2.content.size)), layoutEpoch: mapped.toEpoch } : null;
91264
+ this.#debugLastHit = hit ? { source: "dom", pos: rawHit?.pos ?? null, layoutEpoch: rawHit?.layoutEpoch ?? null, mappedPos: hit.pos } : { source: "none", pos: rawHit?.pos ?? null, layoutEpoch: rawHit?.layoutEpoch ?? null, mappedPos: null };
91265
+ this.#callbacks.updateSelectionDebugHud?.();
91266
+ if (!isDraggableAnnotation) {
91267
+ event.preventDefault();
91268
+ }
91269
+ if (!rawHit) {
91270
+ this.#focusEditorAtFirstPosition();
91271
+ return;
91272
+ }
91273
+ if (!hit || !doc2) {
91274
+ this.#callbacks.setPendingDocChange?.();
91275
+ this.#callbacks.scheduleRerender?.();
91276
+ return;
91277
+ }
91278
+ const fragmentHit = getFragmentAtPosition(layoutState.layout, layoutState.blocks, layoutState.measures, rawHit.pos);
91279
+ const targetImg = event.target?.closest?.("img");
91280
+ if (this.#handleInlineImageClick(event, targetImg, rawHit, doc2, epochMapper)) return;
91281
+ if (this.#handleFragmentClick(event, fragmentHit, hit, doc2)) return;
91282
+ if (this.#lastSelectedImageBlockId) {
91283
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
91284
+ this.#lastSelectedImageBlockId = null;
91285
+ }
91286
+ if (event.shiftKey && editor.state.selection.$anchor) {
91287
+ this.#handleShiftClick(event, hit.pos);
91288
+ return;
91289
+ }
91290
+ const clickDepth = this.#registerPointerClick(event);
91291
+ if (clickDepth === 1) {
91292
+ this.#dragAnchor = hit.pos;
91293
+ this.#dragAnchorPageIndex = hit.pageIndex;
91294
+ this.#pendingMarginClick = this.#callbacks.computePendingMarginClick?.(event.pointerId, x2, y2) ?? null;
91295
+ const tableHit = this.#hitTestTable(x2, y2);
91296
+ if (tableHit) {
91297
+ const tablePos = this.#getTablePosFromHit(tableHit);
91298
+ if (tablePos !== null) {
91299
+ this.#setCellAnchor(tableHit, tablePos);
91300
+ }
91301
+ } else {
91302
+ this.#clearCellAnchor();
91303
+ }
91304
+ } else {
91305
+ this.#pendingMarginClick = null;
91306
+ }
91307
+ this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: x2, y: y2 };
91308
+ this.#dragLastRawHit = hit;
91309
+ this.#dragUsedPageNotMountedFallback = false;
91310
+ this.#isDragging = true;
91311
+ if (clickDepth >= 3) {
91312
+ this.#dragExtensionMode = "para";
91313
+ } else if (clickDepth === 2) {
91314
+ this.#dragExtensionMode = "word";
91315
+ } else {
91316
+ this.#dragExtensionMode = "char";
91317
+ }
91318
+ if (typeof viewportHost.setPointerCapture === "function") {
91319
+ viewportHost.setPointerCapture(event.pointerId);
91320
+ }
91321
+ let handledByDepth = false;
91322
+ const sessionModeForDepth = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
91323
+ if (sessionModeForDepth === "body") {
91324
+ const selectionPos = clickDepth >= 2 && this.#dragAnchor !== null ? this.#dragAnchor : hit.pos;
91325
+ if (clickDepth >= 3) {
91326
+ handledByDepth = this.#callbacks.selectParagraphAt?.(selectionPos) ?? false;
91327
+ } else if (clickDepth === 2) {
91328
+ handledByDepth = this.#callbacks.selectWordAt?.(selectionPos) ?? false;
91329
+ }
91330
+ }
91331
+ if (!handledByDepth) {
91332
+ try {
91333
+ let nextSelection = TextSelection$1.create(doc2, hit.pos);
91334
+ if (!nextSelection.$from.parent.inlineContent) {
91335
+ nextSelection = Selection.near(doc2.resolve(hit.pos), 1);
91336
+ }
91337
+ const tr = editor.state.tr.setSelection(nextSelection);
91338
+ editor.view?.dispatch(tr);
91339
+ } catch {
91340
+ }
91341
+ }
91342
+ this.#callbacks.scheduleSelectionUpdate?.();
91343
+ this.#focusEditor();
91344
+ }
91345
+ #handlePointerMove(event) {
91346
+ if (!this.#deps) return;
91347
+ const layoutState = this.#deps.getLayoutState();
91348
+ if (!layoutState.layout) return;
91349
+ const normalized = this.#callbacks.normalizeClientPoint?.(event.clientX, event.clientY);
91350
+ if (!normalized) return;
91351
+ if (this.#isDragging && this.#dragAnchor !== null && event.buttons & 1) {
91352
+ this.#handleDragSelection(event, normalized);
91353
+ return;
91354
+ }
91355
+ this.#handleHover(normalized);
91356
+ }
91357
+ #handlePointerUp(event) {
91358
+ if (!this.#deps) return;
91359
+ this.#suppressFocusInFromDraggable = false;
91360
+ if (!this.#isDragging) return;
91361
+ const viewportHost = this.#deps.getViewportHost();
91362
+ if (typeof viewportHost.hasPointerCapture === "function" && typeof viewportHost.releasePointerCapture === "function" && viewportHost.hasPointerCapture(event.pointerId)) {
91363
+ viewportHost.releasePointerCapture(event.pointerId);
91364
+ }
91365
+ const pendingMarginClick = this.#pendingMarginClick;
91366
+ this.#pendingMarginClick = null;
91367
+ const dragAnchor = this.#dragAnchor;
91368
+ const dragMode = this.#dragExtensionMode;
91369
+ const dragUsedFallback = this.#dragUsedPageNotMountedFallback;
91370
+ const dragPointer = this.#dragLastPointer;
91371
+ this.#isDragging = false;
91372
+ if (this.#cellDragMode !== "none") {
91373
+ this.#cellDragMode = "none";
91374
+ }
91375
+ if (!pendingMarginClick || pendingMarginClick.pointerId !== event.pointerId) {
91376
+ this.#callbacks.updateSelectionVirtualizationPins?.({ includeDragBuffer: false });
91377
+ if (dragUsedFallback && dragAnchor != null) {
91378
+ const pointer = dragPointer ?? { clientX: event.clientX, clientY: event.clientY };
91379
+ this.#callbacks.finalizeDragSelectionWithDom?.(pointer, dragAnchor, dragMode);
91380
+ }
91381
+ this.#callbacks.scheduleA11ySelectionAnnouncement?.({ immediate: true });
91382
+ this.#dragLastPointer = null;
91383
+ this.#dragLastRawHit = null;
91384
+ this.#dragUsedPageNotMountedFallback = false;
91385
+ return;
91386
+ }
91387
+ this.#handleMarginClickEnd(event, pendingMarginClick);
91388
+ }
91389
+ #handlePointerLeave() {
91390
+ this.#callbacks.clearHoverRegion?.();
91391
+ }
91392
+ #handleDoubleClick(event) {
91393
+ if (!this.#deps) return;
91394
+ if (event.button !== 0) return;
91395
+ const layoutState = this.#deps.getLayoutState();
91396
+ if (!layoutState.layout) return;
91397
+ const viewportHost = this.#deps.getViewportHost();
91398
+ const visibleHost = this.#deps.getVisibleHost();
91399
+ const zoom = this.#deps.getZoom();
91400
+ const rect = viewportHost.getBoundingClientRect();
91401
+ const scrollLeft = visibleHost.scrollLeft ?? 0;
91402
+ const scrollTop = visibleHost.scrollTop ?? 0;
91403
+ const x2 = (event.clientX - rect.left + scrollLeft) / zoom;
91404
+ const y2 = (event.clientY - rect.top + scrollTop) / zoom;
91405
+ const region = this.#callbacks.hitTestHeaderFooterRegion?.(x2, y2);
91406
+ if (region) {
91407
+ event.preventDefault();
91408
+ event.stopPropagation();
91409
+ const descriptor = this.#callbacks.resolveDescriptorForRegion?.(region);
91410
+ const hfManager = this.#deps.getHeaderFooterSession()?.manager;
91411
+ if (!descriptor && hfManager) {
91412
+ this.#callbacks.createDefaultHeaderFooter?.(region);
91413
+ hfManager.refresh();
91414
+ }
91415
+ this.#callbacks.activateHeaderFooterRegion?.(region);
91416
+ } else if ((this.#deps.getHeaderFooterSession()?.session?.mode ?? "body") !== "body") {
91417
+ this.#callbacks.exitHeaderFooterMode?.();
91418
+ }
91419
+ }
91420
+ #handleKeyDown(event) {
91421
+ if (!this.#deps) return;
91422
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
91423
+ if (event.key === "Escape" && sessionMode !== "body") {
91424
+ event.preventDefault();
91425
+ this.#callbacks.exitHeaderFooterMode?.();
91426
+ return;
91427
+ }
91428
+ if (event.ctrlKey && event.altKey && !event.shiftKey) {
91429
+ if (event.code === "KeyH") {
91430
+ event.preventDefault();
91431
+ this.#focusHeaderFooterShortcut("header");
91432
+ } else if (event.code === "KeyF") {
91433
+ event.preventDefault();
91434
+ this.#focusHeaderFooterShortcut("footer");
91435
+ }
91436
+ }
91437
+ }
91438
+ #handleFocusIn(event) {
91439
+ if (!this.#deps) return;
91440
+ if (this.#suppressFocusInFromDraggable) {
91441
+ this.#suppressFocusInFromDraggable = false;
91442
+ return;
91443
+ }
91444
+ try {
91445
+ this.#deps.getActiveEditor().view?.focus();
91446
+ } catch {
91447
+ }
91448
+ }
91449
+ // ==========================================================================
91450
+ // Handler Helpers
91451
+ // ==========================================================================
91452
+ #handleLinkClick(event, linkEl) {
91453
+ const href = linkEl.getAttribute("href") ?? "";
91454
+ const isAnchorLink = href.startsWith("#") && href.length > 1;
91455
+ const isTocLink = linkEl.closest(".superdoc-toc-entry") !== null;
91456
+ if (isAnchorLink && isTocLink) {
91457
+ event.preventDefault();
91458
+ event.stopPropagation();
91459
+ this.#callbacks.goToAnchor?.(href);
91460
+ return;
91461
+ }
91462
+ event.preventDefault();
91463
+ event.stopPropagation();
91464
+ const linkClickEvent = new CustomEvent("superdoc-link-click", {
91465
+ bubbles: true,
91466
+ composed: true,
91467
+ detail: {
91468
+ href,
91469
+ target: linkEl.getAttribute("target"),
91470
+ rel: linkEl.getAttribute("rel"),
91471
+ tooltip: linkEl.getAttribute("title"),
91472
+ element: linkEl,
91473
+ clientX: event.clientX,
91474
+ clientY: event.clientY
91475
+ }
91476
+ });
91477
+ linkEl.dispatchEvent(linkClickEvent);
91478
+ }
91479
+ #handleAnnotationClick(event, annotationEl) {
91480
+ const editor = this.#deps?.getEditor();
91481
+ if (!editor?.isEditable) return;
91482
+ const resolved = this.#callbacks.resolveFieldAnnotationSelectionFromElement?.(annotationEl);
91483
+ if (resolved) {
91484
+ try {
91485
+ const tr = editor.state.tr.setSelection(NodeSelection.create(editor.state.doc, resolved.pos));
91486
+ editor.view?.dispatch(tr);
91487
+ } catch {
91488
+ }
91489
+ editor.emit("fieldAnnotationClicked", {
91490
+ editor,
91491
+ node: resolved.node,
91492
+ nodePos: resolved.pos,
91493
+ event,
91494
+ currentTarget: annotationEl
91495
+ });
91496
+ }
91497
+ }
91498
+ #handleClickWithoutLayout(event, isDraggableAnnotation) {
91499
+ if (!isDraggableAnnotation) {
91500
+ event.preventDefault();
91501
+ }
91502
+ if (document.activeElement instanceof HTMLElement) {
91503
+ document.activeElement.blur();
91504
+ }
91505
+ this.#focusEditorAtFirstPosition();
91506
+ }
91507
+ #handleClickInHeaderFooterMode(event, x2, y2) {
91508
+ const session = this.#deps?.getHeaderFooterSession();
91509
+ const activeEditorHost = session?.overlayManager?.getActiveEditorHost?.();
91510
+ const clickedInsideEditorHost = activeEditorHost && (activeEditorHost.contains(event.target) || activeEditorHost === event.target);
91511
+ if (clickedInsideEditorHost) {
91512
+ return true;
91513
+ }
91514
+ const headerFooterRegion = this.#callbacks.hitTestHeaderFooterRegion?.(x2, y2);
91515
+ if (!headerFooterRegion) {
91516
+ this.#callbacks.exitHeaderFooterMode?.();
91517
+ return false;
91518
+ }
91519
+ return true;
91520
+ }
91521
+ #handleInlineImageClick(event, targetImg, rawHit, doc2, epochMapper) {
91522
+ if (!targetImg) return false;
91523
+ const imgPmStart = targetImg.dataset?.pmStart ? Number(targetImg.dataset.pmStart) : null;
91524
+ if (Number.isNaN(imgPmStart) || imgPmStart == null) return false;
91525
+ const imgLayoutEpochRaw = targetImg.dataset?.layoutEpoch;
91526
+ const imgLayoutEpoch = imgLayoutEpochRaw != null ? Number(imgLayoutEpochRaw) : NaN;
91527
+ const rawLayoutEpoch = Number.isFinite(rawHit.layoutEpoch) ? rawHit.layoutEpoch : NaN;
91528
+ const effectiveEpoch = Number.isFinite(imgLayoutEpoch) && Number.isFinite(rawLayoutEpoch) ? Math.max(imgLayoutEpoch, rawLayoutEpoch) : Number.isFinite(imgLayoutEpoch) ? imgLayoutEpoch : rawHit.layoutEpoch;
91529
+ const mappedImg = epochMapper.mapPosFromLayoutToCurrentDetailed(imgPmStart, effectiveEpoch, 1);
91530
+ if (!mappedImg.ok) {
91531
+ debugLog("warn", "inline image mapping failed", mappedImg);
91532
+ this.#callbacks.setPendingDocChange?.();
91533
+ this.#callbacks.scheduleRerender?.();
91534
+ return true;
91535
+ }
91536
+ const clampedImgPos = Math.max(0, Math.min(mappedImg.pos, doc2.content.size));
91537
+ if (clampedImgPos < 0 || clampedImgPos >= doc2.content.size) return true;
91538
+ const newSelectionId = `inline-${clampedImgPos}`;
91539
+ if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== newSelectionId) {
91540
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
91541
+ }
91542
+ const editor = this.#deps?.getEditor();
91543
+ try {
91544
+ const tr = editor.state.tr.setSelection(NodeSelection.create(doc2, clampedImgPos));
91545
+ editor.view?.dispatch(tr);
91546
+ const selector = `.superdoc-inline-image[data-pm-start="${imgPmStart}"]`;
91547
+ const viewportHost = this.#deps?.getViewportHost();
91548
+ const targetElement = viewportHost?.querySelector(selector);
91549
+ this.#callbacks.emit?.("imageSelected", {
91550
+ element: targetElement ?? targetImg,
91551
+ blockId: null,
91552
+ pmStart: clampedImgPos
91553
+ });
91554
+ this.#lastSelectedImageBlockId = newSelectionId;
91555
+ } catch (error) {
91556
+ }
91557
+ this.#callbacks.focusEditorAfterImageSelection?.();
91558
+ return true;
91559
+ }
91560
+ #handleFragmentClick(event, fragmentHit, hit, doc2) {
91561
+ if (!fragmentHit) return false;
91562
+ if (fragmentHit.fragment.kind !== "image" && fragmentHit.fragment.kind !== "drawing") return false;
91563
+ const editor = this.#deps?.getEditor();
91564
+ try {
91565
+ const tr = editor.state.tr.setSelection(NodeSelection.create(doc2, hit.pos));
91566
+ editor.view?.dispatch(tr);
91567
+ if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== fragmentHit.fragment.blockId) {
91568
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
91569
+ }
91570
+ if (fragmentHit.fragment.kind === "image") {
91571
+ const viewportHost = this.#deps?.getViewportHost();
91572
+ const targetElement = viewportHost?.querySelector(
91573
+ `.superdoc-image-fragment[data-pm-start="${fragmentHit.fragment.pmStart}"]`
91574
+ );
91575
+ if (targetElement) {
91576
+ this.#callbacks.emit?.("imageSelected", {
91577
+ element: targetElement,
91578
+ blockId: fragmentHit.fragment.blockId,
91579
+ pmStart: fragmentHit.fragment.pmStart
91580
+ });
91581
+ this.#lastSelectedImageBlockId = fragmentHit.fragment.blockId;
91582
+ }
91583
+ }
91584
+ } catch (error) {
91585
+ }
91586
+ this.#callbacks.focusEditorAfterImageSelection?.();
91587
+ return true;
91588
+ }
91589
+ #handleShiftClick(event, headPos) {
91590
+ const editor = this.#deps?.getEditor();
91591
+ if (!editor) return;
91592
+ const anchor = editor.state.selection.anchor;
91593
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, headPos, this.#dragExtensionMode);
91594
+ try {
91595
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(editor.state.doc, selAnchor, selHead));
91596
+ editor.view?.dispatch(tr);
91597
+ this.#callbacks.scheduleSelectionUpdate?.();
91598
+ } catch (error) {
91599
+ console.warn("[SELECTION] Failed to extend selection on shift+click:", error);
91600
+ }
91601
+ this.#focusEditor();
91602
+ }
91603
+ #handleDragSelection(event, normalized) {
91604
+ if (!this.#deps) return;
91605
+ this.#pendingMarginClick = null;
91606
+ this.#dragLastPointer;
91607
+ this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y };
91608
+ const layoutState = this.#deps.getLayoutState();
91609
+ const viewportHost = this.#deps.getViewportHost();
91610
+ const pageGeometryHelper = this.#deps.getPageGeometryHelper();
91611
+ const rawHit = clickToPosition(
91612
+ layoutState.layout,
91613
+ layoutState.blocks,
91614
+ layoutState.measures,
91615
+ { x: normalized.x, y: normalized.y },
91616
+ viewportHost,
91617
+ event.clientX,
91618
+ event.clientY,
91619
+ pageGeometryHelper ?? void 0
91620
+ );
91621
+ if (!rawHit) return;
91622
+ const editor = this.#deps.getEditor();
91623
+ const doc2 = editor.state?.doc;
91624
+ if (!doc2) return;
91625
+ this.#dragLastRawHit = rawHit;
91626
+ const pageMounted = this.#deps.getPageElement(rawHit.pageIndex) != null;
91627
+ if (!pageMounted && this.#deps.isSelectionAwareVirtualizationEnabled()) {
91628
+ this.#dragUsedPageNotMountedFallback = true;
91629
+ }
91630
+ this.#callbacks.updateSelectionVirtualizationPins?.({ includeDragBuffer: true, extraPages: [rawHit.pageIndex] });
91631
+ const epochMapper = this.#deps.getEpochMapper();
91632
+ const mappedHead = epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1);
91633
+ if (!mappedHead.ok) {
91634
+ debugLog("warn", "drag mapping failed", mappedHead);
91635
+ return;
91636
+ }
91637
+ const hit = {
91638
+ ...rawHit,
91639
+ pos: Math.max(0, Math.min(mappedHead.pos, doc2.content.size)),
91640
+ layoutEpoch: mappedHead.toEpoch
91641
+ };
91642
+ this.#debugLastHit = {
91643
+ source: pageMounted ? "dom" : "geometry",
91644
+ pos: rawHit.pos,
91645
+ layoutEpoch: rawHit.layoutEpoch,
91646
+ mappedPos: hit.pos
91647
+ };
91648
+ this.#callbacks.updateSelectionDebugHud?.();
91649
+ const currentTableHit = this.#hitTestTable(normalized.x, normalized.y);
91650
+ const shouldUseCellSel = this.#shouldUseCellSelection(currentTableHit);
91651
+ if (shouldUseCellSel && this.#cellAnchor) {
91652
+ this.#handleCellDragSelection(currentTableHit, hit);
91653
+ return;
91654
+ }
91655
+ const anchor = this.#dragAnchor;
91656
+ const head = hit.pos;
91657
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
91658
+ try {
91659
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(editor.state.doc, selAnchor, selHead));
91660
+ editor.view?.dispatch(tr);
91661
+ this.#callbacks.scheduleSelectionUpdate?.();
91662
+ } catch (error) {
91663
+ console.warn("[SELECTION] Failed to extend selection during drag:", error);
91664
+ }
91665
+ }
91666
+ #handleCellDragSelection(currentTableHit, hit) {
91667
+ const headCellPos = currentTableHit ? this.#getCellPosFromTableHit(currentTableHit) : null;
91668
+ if (headCellPos === null) return;
91669
+ if (this.#cellDragMode !== "active") {
91670
+ this.#cellDragMode = "active";
91671
+ }
91672
+ const editor = this.#deps?.getEditor();
91673
+ if (!editor) return;
91674
+ try {
91675
+ const doc2 = editor.state.doc;
91676
+ const anchorCellPos = this.#cellAnchor.cellPos;
91677
+ const clampedAnchor = Math.max(0, Math.min(anchorCellPos, doc2.content.size));
91678
+ const clampedHead = Math.max(0, Math.min(headCellPos, doc2.content.size));
91679
+ const cellSelection = CellSelection.create(doc2, clampedAnchor, clampedHead);
91680
+ const tr = editor.state.tr.setSelection(cellSelection);
91681
+ editor.view?.dispatch(tr);
91682
+ this.#callbacks.scheduleSelectionUpdate?.();
91683
+ } catch (error) {
91684
+ console.warn("[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:", error);
91685
+ const anchor = this.#dragAnchor;
91686
+ const head = hit.pos;
91687
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
91688
+ try {
91689
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(editor.state.doc, selAnchor, selHead));
91690
+ editor.view?.dispatch(tr);
91691
+ this.#callbacks.scheduleSelectionUpdate?.();
91692
+ } catch {
91693
+ }
91694
+ }
91695
+ }
91696
+ #handleHover(normalized) {
91697
+ if (!this.#deps) return;
91698
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
91699
+ if (sessionMode !== "body") {
91700
+ this.#callbacks.clearHoverRegion?.();
91701
+ return;
91702
+ }
91703
+ if (this.#deps.getDocumentMode() === "viewing") {
91704
+ this.#callbacks.clearHoverRegion?.();
91705
+ return;
91706
+ }
91707
+ const region = this.#callbacks.hitTestHeaderFooterRegion?.(normalized.x, normalized.y);
91708
+ if (!region) {
91709
+ this.#callbacks.clearHoverRegion?.();
91710
+ return;
91711
+ }
91712
+ const currentHover = this.#deps.getHeaderFooterSession()?.hoverRegion;
91713
+ if (currentHover && currentHover.kind === region.kind && currentHover.pageIndex === region.pageIndex && currentHover.sectionType === region.sectionType) {
91714
+ return;
91715
+ }
91716
+ this.#deps.getHeaderFooterSession()?.renderHover(region);
91717
+ this.#callbacks.renderHoverRegion?.(region);
91718
+ }
91719
+ #handleMarginClickEnd(event, pendingMarginClick) {
91720
+ const sessionMode = this.#deps?.getHeaderFooterSession()?.session?.mode ?? "body";
91721
+ if (sessionMode !== "body" || this.#deps?.isViewLocked()) {
91722
+ this.#clearDragPointerState();
91723
+ return;
91724
+ }
91725
+ const editor = this.#deps?.getEditor();
91726
+ const doc2 = editor?.state?.doc;
91727
+ if (!doc2) {
91728
+ this.#clearDragPointerState();
91729
+ return;
91730
+ }
91731
+ const epochMapper = this.#deps?.getEpochMapper();
91732
+ if (!epochMapper) {
91733
+ this.#clearDragPointerState();
91734
+ return;
91735
+ }
91736
+ if (pendingMarginClick.kind === "aboveFirstLine") {
91737
+ const pos = this.#getFirstTextPosition();
91738
+ try {
91739
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(doc2, pos));
91740
+ editor.view?.dispatch(tr);
91741
+ this.#callbacks.scheduleSelectionUpdate?.();
91742
+ } catch {
91743
+ }
91744
+ this.#debugLastHit = { source: "margin", pos: null, layoutEpoch: null, mappedPos: pos };
91745
+ this.#callbacks.updateSelectionDebugHud?.();
91746
+ this.#clearDragPointerState();
91747
+ return;
91748
+ }
91749
+ if (pendingMarginClick.kind === "right") {
91750
+ const mappedEnd2 = epochMapper.mapPosFromLayoutToCurrentDetailed(
91751
+ pendingMarginClick.pmEnd,
91752
+ pendingMarginClick.layoutEpoch,
91753
+ 1
91754
+ );
91755
+ if (!mappedEnd2.ok) {
91756
+ this.#callbacks.setPendingDocChange?.();
91757
+ this.#callbacks.scheduleRerender?.();
91758
+ this.#clearDragPointerState();
91759
+ return;
91760
+ }
91761
+ const caretPos = Math.max(0, Math.min(mappedEnd2.pos, doc2.content.size));
91762
+ try {
91763
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(doc2, caretPos));
91764
+ editor.view?.dispatch(tr);
91765
+ this.#callbacks.scheduleSelectionUpdate?.();
91766
+ } catch {
91767
+ }
91768
+ this.#debugLastHit = {
91769
+ source: "margin",
91770
+ pos: pendingMarginClick.pmEnd,
91771
+ layoutEpoch: pendingMarginClick.layoutEpoch,
91772
+ mappedPos: caretPos
91773
+ };
91774
+ this.#callbacks.updateSelectionDebugHud?.();
91775
+ this.#clearDragPointerState();
91776
+ return;
91777
+ }
91778
+ const mappedStart = epochMapper.mapPosFromLayoutToCurrentDetailed(
91779
+ pendingMarginClick.pmStart,
91780
+ pendingMarginClick.layoutEpoch,
91781
+ 1
91782
+ );
91783
+ const mappedEnd = epochMapper.mapPosFromLayoutToCurrentDetailed(
91784
+ pendingMarginClick.pmEnd,
91785
+ pendingMarginClick.layoutEpoch,
91786
+ -1
91787
+ );
91788
+ if (!mappedStart.ok || !mappedEnd.ok) {
91789
+ this.#callbacks.setPendingDocChange?.();
91790
+ this.#callbacks.scheduleRerender?.();
91791
+ this.#clearDragPointerState();
91792
+ return;
91793
+ }
91794
+ const selFrom = Math.max(0, Math.min(Math.min(mappedStart.pos, mappedEnd.pos), doc2.content.size));
91795
+ const selTo = Math.max(0, Math.min(Math.max(mappedStart.pos, mappedEnd.pos), doc2.content.size));
91796
+ try {
91797
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(doc2, selFrom, selTo));
91798
+ editor.view?.dispatch(tr);
91799
+ this.#callbacks.scheduleSelectionUpdate?.();
91800
+ } catch {
91801
+ }
91802
+ this.#debugLastHit = {
91803
+ source: "margin",
91804
+ pos: pendingMarginClick.pmStart,
91805
+ layoutEpoch: pendingMarginClick.layoutEpoch,
91806
+ mappedPos: selFrom
91807
+ };
91808
+ this.#callbacks.updateSelectionDebugHud?.();
91809
+ this.#clearDragPointerState();
91810
+ }
91811
+ #clearDragPointerState() {
91812
+ this.#dragLastPointer = null;
91813
+ this.#dragLastRawHit = null;
91814
+ this.#dragUsedPageNotMountedFallback = false;
91815
+ }
91816
+ #focusHeaderFooterShortcut(kind) {
91817
+ const pageIndex = this.#callbacks.getCurrentPageIndex?.() ?? 0;
91818
+ const region = this.#callbacks.findRegionForPage?.(kind, pageIndex);
91819
+ if (!region) {
91820
+ this.#callbacks.emitHeaderFooterEditBlocked?.("missingRegion");
91821
+ return;
91822
+ }
91823
+ this.#callbacks.activateHeaderFooterRegion?.(region);
91824
+ }
91825
+ #focusEditorAtFirstPosition() {
91826
+ const editor = this.#deps?.getEditor();
91827
+ const editorDom = editor?.view?.dom;
91828
+ if (!editorDom) return;
91829
+ const validPos = this.#getFirstTextPosition();
91830
+ const doc2 = editor?.state?.doc;
91831
+ if (doc2) {
91832
+ try {
91833
+ const tr = editor.state.tr.setSelection(TextSelection$1.create(doc2, validPos));
91834
+ editor.view?.dispatch(tr);
91835
+ } catch {
91836
+ }
91837
+ }
91838
+ editorDom.focus();
91839
+ editor?.view?.focus();
91840
+ this.#callbacks.scheduleSelectionUpdate?.();
91841
+ }
91842
+ #focusEditor() {
91843
+ if (document.activeElement instanceof HTMLElement) {
91844
+ document.activeElement.blur();
91845
+ }
91846
+ const editor = this.#deps?.getEditor();
91847
+ const editorDom = editor?.view?.dom;
91848
+ if (editorDom) {
91849
+ editorDom.focus();
91850
+ editor?.view?.focus();
91851
+ }
91852
+ }
91853
+ }
91854
+ const createDefaultScheduler = () => {
91855
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
91856
+ return {
91857
+ requestAnimationFrame: (cb) => window.requestAnimationFrame(cb),
91858
+ cancelAnimationFrame: (handle2) => window.cancelAnimationFrame(handle2)
91859
+ };
91860
+ }
91861
+ const anyGlobal = globalThis;
91862
+ if (typeof anyGlobal.requestAnimationFrame === "function" && typeof anyGlobal.cancelAnimationFrame === "function") {
91863
+ return {
91864
+ requestAnimationFrame: (cb) => anyGlobal.requestAnimationFrame(cb),
91865
+ cancelAnimationFrame: (handle2) => anyGlobal.cancelAnimationFrame(handle2)
91866
+ };
91867
+ }
91868
+ return {
91869
+ requestAnimationFrame: (cb) => {
91870
+ const handle2 = anyGlobal.setTimeout?.(() => cb(Date.now()), 0);
91871
+ return handle2;
91872
+ },
91873
+ cancelAnimationFrame: (handle2) => {
91874
+ anyGlobal.clearTimeout?.(handle2);
91875
+ }
91876
+ };
91877
+ };
91878
+ class SelectionSyncCoordinator extends EventEmitter$1 {
91879
+ #docEpoch = 0;
91880
+ #layoutEpoch = 0;
91881
+ #layoutUpdating = false;
91882
+ #pending = false;
91883
+ #scheduled = false;
91884
+ #rafHandle = null;
91885
+ #scheduler;
91886
+ /**
91887
+ * Creates a new SelectionSyncCoordinator.
91888
+ *
91889
+ * @param options - Configuration options
91890
+ * @param options.scheduler - Custom scheduler for animation frames (useful for testing), defaults to platform scheduler
91891
+ */
91892
+ constructor(options) {
91893
+ super();
91894
+ this.#scheduler = options?.scheduler ?? createDefaultScheduler();
91895
+ }
91896
+ /**
91897
+ * Gets the current document epoch.
91898
+ *
91899
+ * @returns The document epoch (increments on each document-changing transaction)
91900
+ */
91901
+ getDocEpoch() {
91902
+ return this.#docEpoch;
91903
+ }
91904
+ /**
91905
+ * Gets the current layout epoch.
91906
+ *
91907
+ * @returns The epoch of the document version currently painted in the DOM
91908
+ */
91909
+ getLayoutEpoch() {
91910
+ return this.#layoutEpoch;
91911
+ }
91912
+ /**
91913
+ * Checks if a layout update is currently in progress.
91914
+ *
91915
+ * @returns True if between onLayoutStart() and onLayoutComplete(), false otherwise
91916
+ */
91917
+ isLayoutUpdating() {
91918
+ return this.#layoutUpdating;
91919
+ }
91920
+ /**
91921
+ * Updates the document epoch and triggers conditional rendering.
91922
+ *
91923
+ * @param epoch - The new document epoch (must be finite and non-negative)
91924
+ *
91925
+ * @remarks
91926
+ * When the document epoch changes:
91927
+ * 1. Any scheduled render is cancelled (layout will be out of sync)
91928
+ * 2. If layout has already caught up, rendering is rescheduled
91929
+ *
91930
+ * Calling with the same epoch as the current value is a no-op.
91931
+ * Invalid epoch values are silently ignored.
91932
+ */
91933
+ setDocEpoch(epoch) {
91934
+ if (!Number.isFinite(epoch) || epoch < 0) return;
91935
+ if (epoch === this.#docEpoch) return;
91936
+ this.#docEpoch = epoch;
91937
+ this.#cancelScheduledRender();
91938
+ this.#maybeSchedule();
91939
+ }
91940
+ /**
91941
+ * Notifies the coordinator that layout computation has started.
91942
+ *
91943
+ * @remarks
91944
+ * Marks the layout as updating and cancels any scheduled renders, since the DOM
91945
+ * is about to change and current position data will be stale.
91946
+ *
91947
+ * Safe to call multiple times (e.g., if layouts overlap) - subsequent calls are ignored
91948
+ * until onLayoutComplete() is called.
91949
+ */
91950
+ onLayoutStart() {
91951
+ if (this.#layoutUpdating) return;
91952
+ this.#layoutUpdating = true;
91953
+ this.#cancelScheduledRender();
91954
+ }
91955
+ /**
91956
+ * Notifies the coordinator that layout painting has completed.
91957
+ *
91958
+ * @param layoutEpoch - The document epoch that was just painted to the DOM
91959
+ *
91960
+ * @remarks
91961
+ * Marks the layout as no longer updating, records the new layout epoch, and attempts
91962
+ * to schedule rendering if conditions are now safe.
91963
+ *
91964
+ * If the layoutEpoch is invalid (not a finite non-negative number), it is ignored and
91965
+ * the previous layoutEpoch value is retained.
91966
+ *
91967
+ * This method is the primary trigger for selection rendering - if there's a pending
91968
+ * render request and layoutEpoch >= docEpoch, a render event will be scheduled.
91969
+ */
91970
+ onLayoutComplete(layoutEpoch) {
91971
+ this.#layoutUpdating = false;
91972
+ if (Number.isFinite(layoutEpoch) && layoutEpoch >= 0) {
91973
+ this.#layoutEpoch = layoutEpoch;
91974
+ }
91975
+ this.#maybeSchedule();
91976
+ }
91977
+ /**
91978
+ * Notifies the coordinator that layout was aborted without completing.
91979
+ *
91980
+ * @remarks
91981
+ * Marks the layout as no longer updating (without updating layoutEpoch) and attempts
91982
+ * to schedule rendering if conditions are safe.
91983
+ *
91984
+ * Use this when layout computation is cancelled or fails partway through.
91985
+ */
91986
+ onLayoutAbort() {
91987
+ this.#layoutUpdating = false;
91988
+ this.#maybeSchedule();
91989
+ }
91990
+ /**
91991
+ * Requests that selection rendering occur when conditions become safe.
91992
+ *
91993
+ * @param options - Rendering options
91994
+ * @param options.immediate - If true, attempts to render immediately (synchronously) if safe, defaults to false
91995
+ *
91996
+ * @remarks
91997
+ * Marks a render as pending and schedules it to occur on the next animation frame if
91998
+ * conditions are safe (layout not updating, layoutEpoch >= docEpoch).
91999
+ *
92000
+ * If options.immediate is true, also attempts a synchronous render before scheduling.
92001
+ * Use immediate rendering sparingly, as it can cause multiple renders per frame.
92002
+ *
92003
+ * Multiple calls are coalesced - only one render will occur per animation frame.
92004
+ */
92005
+ requestRender(options) {
92006
+ this.#pending = true;
92007
+ if (options?.immediate) {
92008
+ this.flushNow();
92009
+ }
92010
+ this.#maybeSchedule();
92011
+ }
92012
+ /**
92013
+ * Attempts to render selection immediately (synchronously) if conditions are safe.
92014
+ *
92015
+ * @remarks
92016
+ * If there's a pending render request and conditions are safe (layout not updating,
92017
+ * layoutEpoch >= docEpoch), this method:
92018
+ * 1. Cancels any scheduled asynchronous render
92019
+ * 2. Clears the pending flag
92020
+ * 3. Emits the 'render' event synchronously
92021
+ *
92022
+ * If no render is pending or conditions are not safe, this is a no-op.
92023
+ *
92024
+ * Use this for immediate selection updates in response to user actions (e.g., click
92025
+ * handlers) where waiting for the next animation frame would cause noticeable lag.
92026
+ */
92027
+ flushNow() {
92028
+ if (!this.#pending) return;
92029
+ if (!this.#isSafeToRender()) return;
92030
+ this.#cancelScheduledRender();
92031
+ this.#pending = false;
92032
+ this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
92033
+ }
92034
+ /**
92035
+ * Permanently tears down the coordinator, cancelling pending renders and removing all listeners.
92036
+ *
92037
+ * @remarks
92038
+ * After calling destroy(), this instance should not be used again. All scheduled renders
92039
+ * are cancelled and all event listeners are removed.
92040
+ *
92041
+ * Safe to call multiple times.
92042
+ */
92043
+ destroy() {
92044
+ this.#cancelScheduledRender();
92045
+ this.removeAllListeners();
92046
+ }
92047
+ #isSafeToRender() {
92048
+ return !this.#layoutUpdating && this.#layoutEpoch >= this.#docEpoch;
92049
+ }
92050
+ #maybeSchedule() {
92051
+ if (!this.#pending) return;
92052
+ if (!this.#isSafeToRender()) return;
92053
+ if (this.#scheduled) return;
92054
+ this.#scheduled = true;
92055
+ this.#rafHandle = this.#scheduler.requestAnimationFrame(() => {
92056
+ this.#scheduled = false;
92057
+ this.#rafHandle = null;
92058
+ if (!this.#pending) return;
92059
+ if (!this.#isSafeToRender()) return;
92060
+ this.#pending = false;
92061
+ this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
92062
+ });
92063
+ }
92064
+ #cancelScheduledRender() {
92065
+ if (this.#rafHandle != null) {
92066
+ try {
92067
+ this.#scheduler.cancelAnimationFrame(this.#rafHandle);
92068
+ } finally {
92069
+ this.#rafHandle = null;
92070
+ }
92071
+ }
92072
+ this.#scheduled = false;
92073
+ }
92074
+ }
92075
+ const uiSurfaces = /* @__PURE__ */ new WeakSet();
92076
+ function isInRegisteredSurface(event) {
91148
92077
  const path2 = typeof event.composedPath === "function" ? event.composedPath() : [];
91149
92078
  if (path2.length > 0) {
91150
92079
  for (const node22 of path2) {
@@ -91500,37 +92429,6 @@ ${o}
91500
92429
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
91501
92430
  }
91502
92431
  }
91503
- const WORD_CHARACTER_REGEX = /[\p{L}\p{N}'\u2018\u2019_~-]/u;
91504
- function isWordCharacter(char) {
91505
- if (!char) {
91506
- return false;
91507
- }
91508
- return WORD_CHARACTER_REGEX.test(char);
91509
- }
91510
- function calculateExtendedSelection(blocks2, anchor, head, mode) {
91511
- if (mode === "word") {
91512
- const anchorBounds = findWordBoundaries(blocks2, anchor);
91513
- const headBounds = findWordBoundaries(blocks2, head);
91514
- if (anchorBounds && headBounds) {
91515
- if (head >= anchor) {
91516
- return { selAnchor: anchorBounds.from, selHead: headBounds.to };
91517
- } else {
91518
- return { selAnchor: anchorBounds.to, selHead: headBounds.from };
91519
- }
91520
- }
91521
- } else if (mode === "para") {
91522
- const anchorBounds = findParagraphBoundaries(blocks2, anchor);
91523
- const headBounds = findParagraphBoundaries(blocks2, head);
91524
- if (anchorBounds && headBounds) {
91525
- if (head >= anchor) {
91526
- return { selAnchor: anchorBounds.from, selHead: headBounds.to };
91527
- } else {
91528
- return { selAnchor: anchorBounds.to, selHead: headBounds.from };
91529
- }
91530
- }
91531
- }
91532
- return { selAnchor: anchor, selHead: head };
91533
- }
91534
92432
  function getAtomNodeTypes(schema) {
91535
92433
  if (!schema) return [];
91536
92434
  const types2 = [];
@@ -91586,104 +92484,6 @@ ${o}
91586
92484
  if (!ok2) return null;
91587
92485
  return map2;
91588
92486
  }
91589
- function registerPointerClick(event, previous2, options) {
91590
- const time2 = event.timeStamp ?? performance.now();
91591
- const timeDelta = time2 - previous2.lastClickTime;
91592
- const withinTime = timeDelta <= options.timeThresholdMs;
91593
- const distanceX = Math.abs(event.clientX - previous2.lastClickPosition.x);
91594
- const distanceY = Math.abs(event.clientY - previous2.lastClickPosition.y);
91595
- const withinDistance = distanceX <= options.distanceThresholdPx && distanceY <= options.distanceThresholdPx;
91596
- const clickCount = withinTime && withinDistance ? Math.min(previous2.clickCount + 1, options.maxClickCount) : 1;
91597
- return {
91598
- clickCount,
91599
- lastClickTime: time2,
91600
- lastClickPosition: { x: event.clientX, y: event.clientY }
91601
- };
91602
- }
91603
- function getFirstTextPosition(doc2) {
91604
- if (!doc2 || !doc2.content) {
91605
- return 1;
91606
- }
91607
- let validPos = 1;
91608
- doc2.nodesBetween(0, doc2.content.size, (node2, pos) => {
91609
- if (node2.isTextblock) {
91610
- validPos = pos + 1;
91611
- return false;
91612
- }
91613
- return true;
91614
- });
91615
- return validPos;
91616
- }
91617
- function computeWordSelectionRangeAt(state, pos) {
91618
- if (!state?.doc) {
91619
- return null;
91620
- }
91621
- if (pos < 0 || pos > state.doc.content.size) {
91622
- return null;
91623
- }
91624
- const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
91625
- if (!textblockPos) {
91626
- return null;
91627
- }
91628
- const parentStart = textblockPos.start();
91629
- const parentEnd = textblockPos.end();
91630
- const sampleEnd = Math.min(pos + 1, parentEnd);
91631
- const charAtPos = state.doc.textBetween(pos, sampleEnd, "\0", "\0");
91632
- if (!isWordCharacter(charAtPos)) {
91633
- return null;
91634
- }
91635
- let startPos = pos;
91636
- while (startPos > parentStart) {
91637
- const prevChar = state.doc.textBetween(startPos - 1, startPos, "\0", "\0");
91638
- if (!isWordCharacter(prevChar)) {
91639
- break;
91640
- }
91641
- startPos -= 1;
91642
- }
91643
- let endPos = pos;
91644
- while (endPos < parentEnd) {
91645
- const nextChar = state.doc.textBetween(endPos, endPos + 1, "\0", "\0");
91646
- if (!isWordCharacter(nextChar)) {
91647
- break;
91648
- }
91649
- endPos += 1;
91650
- }
91651
- if (startPos === endPos) {
91652
- return null;
91653
- }
91654
- return { from: startPos, to: endPos };
91655
- }
91656
- function computeParagraphSelectionRangeAt(state, pos) {
91657
- if (!state?.doc) {
91658
- return null;
91659
- }
91660
- const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
91661
- if (!textblockPos) {
91662
- return null;
91663
- }
91664
- return { from: textblockPos.start(), to: textblockPos.end() };
91665
- }
91666
- function findNearestTextblockResolvedPos(doc2, pos) {
91667
- const $pos = doc2.resolve(pos);
91668
- let textblockPos = $pos;
91669
- while (textblockPos.depth > 0) {
91670
- if (textblockPos.parent?.isTextblock) {
91671
- break;
91672
- }
91673
- if (!textblockPos.parent || textblockPos.depth === 0) {
91674
- break;
91675
- }
91676
- const beforePos = textblockPos.before();
91677
- if (beforePos < 0 || beforePos > doc2.content.size) {
91678
- return null;
91679
- }
91680
- textblockPos = doc2.resolve(beforePos);
91681
- }
91682
- if (!textblockPos.parent?.isTextblock) {
91683
- return null;
91684
- }
91685
- return textblockPos;
91686
- }
91687
92487
  function syncHiddenEditorA11yAttributes(pmDom, documentMode) {
91688
92488
  const element2 = pmDom;
91689
92489
  if (!(element2 instanceof HTMLElement)) return;
@@ -92353,314 +93153,255 @@ ${o}
92353
93153
  }
92354
93154
  return true;
92355
93155
  }
92356
- function getCellPosFromTableHit(tableHit, doc2, blocks2) {
92357
- if (!tableHit || !tableHit.block || typeof tableHit.block.id !== "string") {
92358
- console.warn("[getCellPosFromTableHit] Invalid tableHit input:", tableHit);
92359
- return null;
92360
- }
92361
- if (typeof tableHit.cellRowIndex !== "number" || typeof tableHit.cellColIndex !== "number" || tableHit.cellRowIndex < 0 || tableHit.cellColIndex < 0) {
92362
- console.warn("[getCellPosFromTableHit] Invalid cell indices:", {
92363
- row: tableHit.cellRowIndex,
92364
- col: tableHit.cellColIndex
92365
- });
92366
- return null;
92367
- }
92368
- if (!doc2) return null;
92369
- const tableBlocks = blocks2.filter((b2) => b2.kind === "table");
92370
- const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
92371
- if (targetTableIndex === -1) return null;
92372
- let tablePos = null;
92373
- let currentTableIndex = 0;
92374
- try {
92375
- doc2.descendants((node2, pos) => {
92376
- if (node2.type.name === "table") {
92377
- if (currentTableIndex === targetTableIndex) {
92378
- tablePos = pos;
92379
- return false;
92380
- }
92381
- currentTableIndex++;
92382
- }
92383
- return true;
92384
- });
92385
- } catch (error) {
92386
- console.error("[getCellPosFromTableHit] Error during document traversal:", error);
92387
- return null;
92388
- }
92389
- if (tablePos === null) return null;
92390
- const tableNode = doc2.nodeAt(tablePos);
92391
- if (!tableNode || tableNode.type.name !== "table") return null;
92392
- const targetRowIndex = tableHit.cellRowIndex;
92393
- const targetColIndex = tableHit.cellColIndex;
92394
- if (targetRowIndex >= tableNode.childCount) {
92395
- console.warn("[getCellPosFromTableHit] Target row index out of bounds:", {
92396
- targetRowIndex,
92397
- tableChildCount: tableNode.childCount
92398
- });
92399
- return null;
92400
- }
92401
- let currentPos = tablePos + 1;
92402
- for (let r2 = 0; r2 < tableNode.childCount && r2 <= targetRowIndex; r2++) {
92403
- const row2 = tableNode.child(r2);
92404
- if (r2 === targetRowIndex) {
92405
- currentPos += 1;
92406
- let logicalCol = 0;
92407
- for (let cellIndex = 0; cellIndex < row2.childCount; cellIndex++) {
92408
- const cell2 = row2.child(cellIndex);
92409
- const rawColspan = cell2.attrs?.colspan;
92410
- const colspan = typeof rawColspan === "number" && Number.isFinite(rawColspan) && rawColspan > 0 ? rawColspan : 1;
92411
- if (targetColIndex >= logicalCol && targetColIndex < logicalCol + colspan) {
92412
- return currentPos;
92413
- }
92414
- currentPos += cell2.nodeSize;
92415
- logicalCol += colspan;
92416
- }
92417
- console.warn("[getCellPosFromTableHit] Target column not found in row:", {
92418
- targetColIndex,
92419
- logicalColReached: logicalCol,
92420
- rowCellCount: row2.childCount
92421
- });
92422
- return null;
92423
- } else {
92424
- currentPos += row2.nodeSize;
93156
+ const INTERNAL_MIME_TYPE = "application/x-field-annotation";
93157
+ const FIELD_ANNOTATION_DATA_TYPE = "fieldAnnotation";
93158
+ function isValidFieldAnnotationAttributes(attrs) {
93159
+ if (!attrs || typeof attrs !== "object") return false;
93160
+ const a2 = attrs;
93161
+ return typeof a2.fieldId === "string" && typeof a2.fieldType === "string" && typeof a2.displayLabel === "string" && typeof a2.type === "string";
93162
+ }
93163
+ function parseIntSafe$1(value) {
93164
+ if (!value) return void 0;
93165
+ const parsed = parseInt(value, 10);
93166
+ return Number.isFinite(parsed) ? parsed : void 0;
93167
+ }
93168
+ function extractFieldAnnotationData(element2) {
93169
+ const dataset = element2.dataset;
93170
+ const attributes = {};
93171
+ for (const key2 in dataset) {
93172
+ const value = dataset[key2];
93173
+ if (value !== void 0) {
93174
+ attributes[key2] = value;
92425
93175
  }
92426
93176
  }
92427
- return null;
93177
+ return {
93178
+ fieldId: dataset.fieldId,
93179
+ fieldType: dataset.fieldType,
93180
+ variant: dataset.variant ?? dataset.type,
93181
+ displayLabel: dataset.displayLabel,
93182
+ pmStart: parseIntSafe$1(dataset.pmStart),
93183
+ pmEnd: parseIntSafe$1(dataset.pmEnd),
93184
+ attributes
93185
+ };
92428
93186
  }
92429
- function getTablePosFromHit(tableHit, doc2, blocks2) {
92430
- if (!doc2) return null;
92431
- const tableBlocks = blocks2.filter((b2) => b2.kind === "table");
92432
- const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
92433
- if (targetTableIndex === -1) return null;
92434
- let tablePos = null;
92435
- let currentTableIndex = 0;
92436
- doc2.descendants((node2, pos) => {
92437
- if (node2.type.name === "table") {
92438
- if (currentTableIndex === targetTableIndex) {
92439
- tablePos = pos;
92440
- return false;
92441
- }
92442
- currentTableIndex++;
92443
- }
92444
- return true;
92445
- });
92446
- return tablePos;
93187
+ function hasFieldAnnotationData(event) {
93188
+ if (!event.dataTransfer) return false;
93189
+ const types2 = event.dataTransfer.types;
93190
+ return types2.includes(INTERNAL_MIME_TYPE) || types2.includes(FIELD_ANNOTATION_DATA_TYPE);
92447
93191
  }
92448
- function shouldUseCellSelection(currentTableHit, cellAnchor, cellDragMode) {
92449
- if (!cellAnchor) return false;
92450
- if (!currentTableHit) return cellDragMode === "active";
92451
- if (currentTableHit.block.id !== cellAnchor.tableBlockId) {
92452
- return cellDragMode === "active";
92453
- }
92454
- const sameCell = currentTableHit.cellRowIndex === cellAnchor.cellRowIndex && currentTableHit.cellColIndex === cellAnchor.cellColIndex;
92455
- if (!sameCell) {
92456
- return true;
92457
- }
92458
- return cellDragMode === "active";
93192
+ function isInternalDrag(event) {
93193
+ return event.dataTransfer?.types?.includes(INTERNAL_MIME_TYPE) ?? false;
92459
93194
  }
92460
- function hitTestTable(layout, blocks2, measures, normalizedX, normalizedY, configuredPageHeight, pageGapFallback, geometryHelper) {
92461
- if (!layout) {
93195
+ function extractDragData(event) {
93196
+ if (!event.dataTransfer) return null;
93197
+ let jsonData = event.dataTransfer.getData(INTERNAL_MIME_TYPE);
93198
+ if (!jsonData) {
93199
+ jsonData = event.dataTransfer.getData(FIELD_ANNOTATION_DATA_TYPE);
93200
+ }
93201
+ if (!jsonData) return null;
93202
+ try {
93203
+ const parsed = JSON.parse(jsonData);
93204
+ return parsed.sourceField ?? parsed.attributes ?? parsed;
93205
+ } catch {
92462
93206
  return null;
92463
93207
  }
92464
- let pageY = 0;
92465
- let pageHit = null;
92466
- if (geometryHelper) {
92467
- const idx = geometryHelper.getPageIndexAtY(normalizedY) ?? geometryHelper.getNearestPageIndex(normalizedY);
92468
- if (idx != null && layout.pages[idx]) {
92469
- pageHit = { pageIndex: idx, page: layout.pages[idx] };
92470
- pageY = geometryHelper.getPageTop(idx);
92471
- }
93208
+ }
93209
+ class DragDropManager {
93210
+ #deps = null;
93211
+ // Bound handlers for cleanup
93212
+ #boundHandleDragStart = null;
93213
+ #boundHandleDragOver = null;
93214
+ #boundHandleDrop = null;
93215
+ #boundHandleDragEnd = null;
93216
+ #boundHandleDragLeave = null;
93217
+ #boundHandleWindowDragOver = null;
93218
+ #boundHandleWindowDrop = null;
93219
+ // ==========================================================================
93220
+ // Setup
93221
+ // ==========================================================================
93222
+ setDependencies(deps) {
93223
+ this.#deps = deps;
92472
93224
  }
92473
- if (!pageHit) {
92474
- const gap = layout.pageGap ?? pageGapFallback;
92475
- for (let i2 = 0; i2 < layout.pages.length; i2++) {
92476
- const page = layout.pages[i2];
92477
- const pageHeight = page.size?.h ?? configuredPageHeight;
92478
- if (normalizedY >= pageY && normalizedY < pageY + pageHeight) {
92479
- pageHit = { pageIndex: i2, page };
92480
- break;
92481
- }
92482
- pageY += pageHeight + gap;
92483
- }
93225
+ bind() {
93226
+ if (!this.#deps) return;
93227
+ const viewportHost = this.#deps.getViewportHost();
93228
+ const painterHost = this.#deps.getPainterHost();
93229
+ this.#boundHandleDragStart = this.#handleDragStart.bind(this);
93230
+ this.#boundHandleDragOver = this.#handleDragOver.bind(this);
93231
+ this.#boundHandleDrop = this.#handleDrop.bind(this);
93232
+ this.#boundHandleDragEnd = this.#handleDragEnd.bind(this);
93233
+ this.#boundHandleDragLeave = this.#handleDragLeave.bind(this);
93234
+ this.#boundHandleWindowDragOver = this.#handleWindowDragOver.bind(this);
93235
+ this.#boundHandleWindowDrop = this.#handleWindowDrop.bind(this);
93236
+ painterHost.addEventListener("dragstart", this.#boundHandleDragStart);
93237
+ painterHost.addEventListener("dragend", this.#boundHandleDragEnd);
93238
+ painterHost.addEventListener("dragleave", this.#boundHandleDragLeave);
93239
+ viewportHost.addEventListener("dragover", this.#boundHandleDragOver);
93240
+ viewportHost.addEventListener("drop", this.#boundHandleDrop);
93241
+ window.addEventListener("dragover", this.#boundHandleWindowDragOver, false);
93242
+ window.addEventListener("drop", this.#boundHandleWindowDrop, false);
93243
+ }
93244
+ unbind() {
93245
+ if (!this.#deps) return;
93246
+ const viewportHost = this.#deps.getViewportHost();
93247
+ const painterHost = this.#deps.getPainterHost();
93248
+ if (this.#boundHandleDragStart) {
93249
+ painterHost.removeEventListener("dragstart", this.#boundHandleDragStart);
93250
+ }
93251
+ if (this.#boundHandleDragEnd) {
93252
+ painterHost.removeEventListener("dragend", this.#boundHandleDragEnd);
93253
+ }
93254
+ if (this.#boundHandleDragLeave) {
93255
+ painterHost.removeEventListener("dragleave", this.#boundHandleDragLeave);
93256
+ }
93257
+ if (this.#boundHandleDragOver) {
93258
+ viewportHost.removeEventListener("dragover", this.#boundHandleDragOver);
93259
+ }
93260
+ if (this.#boundHandleDrop) {
93261
+ viewportHost.removeEventListener("drop", this.#boundHandleDrop);
93262
+ }
93263
+ if (this.#boundHandleWindowDragOver) {
93264
+ window.removeEventListener("dragover", this.#boundHandleWindowDragOver, false);
93265
+ }
93266
+ if (this.#boundHandleWindowDrop) {
93267
+ window.removeEventListener("drop", this.#boundHandleWindowDrop, false);
93268
+ }
93269
+ this.#boundHandleDragStart = null;
93270
+ this.#boundHandleDragOver = null;
93271
+ this.#boundHandleDrop = null;
93272
+ this.#boundHandleDragEnd = null;
93273
+ this.#boundHandleDragLeave = null;
93274
+ this.#boundHandleWindowDragOver = null;
93275
+ this.#boundHandleWindowDrop = null;
92484
93276
  }
92485
- if (!pageHit) {
92486
- return null;
93277
+ destroy() {
93278
+ this.unbind();
93279
+ this.#deps = null;
92487
93280
  }
92488
- const pageRelativeY = normalizedY - pageY;
92489
- const point2 = { x: normalizedX, y: pageRelativeY };
92490
- return hitTestTableFragment(pageHit, blocks2, measures, point2);
92491
- }
92492
- function isValidFieldAnnotationAttributes(attrs) {
92493
- if (!attrs || typeof attrs !== "object") return false;
92494
- const a2 = attrs;
92495
- return typeof a2.fieldId === "string" && typeof a2.fieldType === "string" && typeof a2.displayLabel === "string" && typeof a2.type === "string";
92496
- }
92497
- const FIELD_ANNOTATION_DATA_TYPE = "fieldAnnotation";
92498
- function setupInternalFieldAnnotationDragHandlers({
92499
- painterHost,
92500
- getActiveEditor,
92501
- hitTest,
92502
- scheduleSelectionUpdate
92503
- }) {
92504
- return createDragHandler(painterHost, {
92505
- onDragOver: (event) => {
92506
- if (!event.hasFieldAnnotation || event.event.clientX === 0) {
92507
- return;
92508
- }
92509
- const activeEditor = getActiveEditor();
92510
- if (!activeEditor?.isEditable) {
92511
- return;
92512
- }
92513
- const hit = hitTest(event.clientX, event.clientY);
92514
- const doc2 = activeEditor.state?.doc;
92515
- if (!hit || !doc2) {
92516
- return;
92517
- }
92518
- const pos = Math.min(Math.max(hit.pos, 1), doc2.content.size);
92519
- const currentSelection = activeEditor.state.selection;
92520
- if (currentSelection instanceof TextSelection$1 && currentSelection.from === pos && currentSelection.to === pos) {
92521
- return;
92522
- }
92523
- try {
92524
- const tr = activeEditor.state.tr.setSelection(TextSelection$1.create(doc2, pos)).setMeta("addToHistory", false);
92525
- activeEditor.view?.dispatch(tr);
92526
- scheduleSelectionUpdate();
92527
- } catch {
92528
- }
92529
- },
92530
- onDrop: (event) => {
92531
- event.event.preventDefault();
92532
- event.event.stopPropagation();
92533
- if (event.pmPosition === null) {
92534
- return;
92535
- }
92536
- const activeEditor = getActiveEditor();
92537
- const { state, view } = activeEditor;
92538
- if (!state || !view) {
92539
- return;
92540
- }
92541
- const fieldId = event.data.fieldId;
92542
- if (fieldId) {
92543
- const targetPos = event.pmPosition;
92544
- const pmStart = event.data.pmStart;
92545
- let sourceStart = null;
92546
- let sourceEnd = null;
92547
- let sourceNode = null;
92548
- if (pmStart != null) {
92549
- const nodeAt = state.doc.nodeAt(pmStart);
92550
- if (nodeAt?.type?.name === "fieldAnnotation") {
92551
- sourceStart = pmStart;
92552
- sourceEnd = pmStart + nodeAt.nodeSize;
92553
- sourceNode = nodeAt;
92554
- }
92555
- }
92556
- if (sourceStart == null || sourceEnd == null || !sourceNode) {
92557
- state.doc.descendants((node2, pos) => {
92558
- if (node2.type.name === "fieldAnnotation" && node2.attrs.fieldId === fieldId) {
92559
- sourceStart = pos;
92560
- sourceEnd = pos + node2.nodeSize;
92561
- sourceNode = node2;
92562
- return false;
92563
- }
92564
- return true;
92565
- });
92566
- }
92567
- if (sourceStart === null || sourceEnd === null || !sourceNode) {
92568
- return;
92569
- }
92570
- if (targetPos >= sourceStart && targetPos <= sourceEnd) {
92571
- return;
92572
- }
92573
- const tr = state.tr;
92574
- tr.delete(sourceStart, sourceEnd);
92575
- const mappedTarget = tr.mapping.map(targetPos);
92576
- if (mappedTarget < 0 || mappedTarget > tr.doc.content.size) {
92577
- return;
92578
- }
92579
- tr.insert(mappedTarget, sourceNode);
92580
- tr.setMeta("uiEvent", "drop");
92581
- view.dispatch(tr);
92582
- return;
92583
- }
92584
- const attrs = event.data.attributes;
92585
- if (attrs && isValidFieldAnnotationAttributes(attrs)) {
92586
- const inserted = activeEditor.commands?.addFieldAnnotation?.(event.pmPosition, attrs, true);
92587
- if (inserted) {
92588
- scheduleSelectionUpdate();
92589
- }
92590
- return;
92591
- }
92592
- activeEditor.emit("fieldAnnotationDropped", {
92593
- sourceField: event.data,
92594
- editor: activeEditor,
92595
- coordinates: { pos: event.pmPosition }
92596
- });
92597
- }
92598
- });
92599
- }
92600
- function createExternalFieldAnnotationDragOverHandler({
92601
- getActiveEditor,
92602
- hitTest,
92603
- scheduleSelectionUpdate
92604
- }) {
92605
- return (event) => {
92606
- const activeEditor = getActiveEditor();
92607
- if (!activeEditor?.isEditable) {
93281
+ // ==========================================================================
93282
+ // Event Handlers
93283
+ // ==========================================================================
93284
+ /**
93285
+ * Handle dragstart for internal field annotations.
93286
+ */
93287
+ #handleDragStart(event) {
93288
+ const target = event.target;
93289
+ if (!target?.dataset?.draggable || target.dataset.draggable !== "true") {
92608
93290
  return;
92609
93291
  }
92610
- event.preventDefault();
93292
+ const data = extractFieldAnnotationData(target);
92611
93293
  if (event.dataTransfer) {
92612
- event.dataTransfer.dropEffect = "copy";
93294
+ const jsonData = JSON.stringify({
93295
+ attributes: data.attributes,
93296
+ sourceField: data
93297
+ });
93298
+ event.dataTransfer.setData(INTERNAL_MIME_TYPE, jsonData);
93299
+ event.dataTransfer.setData(FIELD_ANNOTATION_DATA_TYPE, jsonData);
93300
+ event.dataTransfer.setData("text/plain", data.displayLabel ?? "Field Annotation");
93301
+ event.dataTransfer.setDragImage(target, 0, 0);
93302
+ event.dataTransfer.effectAllowed = "move";
92613
93303
  }
92614
- const dt = event.dataTransfer;
92615
- const hasFieldAnnotation = dt?.types?.includes(FIELD_ANNOTATION_DATA_TYPE) || Boolean(dt?.getData?.(FIELD_ANNOTATION_DATA_TYPE));
92616
- if (!hasFieldAnnotation) {
92617
- return;
93304
+ }
93305
+ /**
93306
+ * Handle dragover - update cursor position to show drop location.
93307
+ */
93308
+ #handleDragOver(event) {
93309
+ if (!this.#deps) return;
93310
+ if (!hasFieldAnnotationData(event)) return;
93311
+ const activeEditor = this.#deps.getActiveEditor();
93312
+ if (!activeEditor?.isEditable) return;
93313
+ event.preventDefault();
93314
+ if (event.dataTransfer) {
93315
+ event.dataTransfer.dropEffect = isInternalDrag(event) ? "move" : "copy";
92618
93316
  }
92619
- const hit = hitTest(event.clientX, event.clientY);
93317
+ const hit = this.#deps.hitTest(event.clientX, event.clientY);
92620
93318
  const doc2 = activeEditor.state?.doc;
92621
- if (!hit || !doc2) {
92622
- return;
92623
- }
93319
+ if (!hit || !doc2) return;
92624
93320
  const pos = Math.min(Math.max(hit.pos, 1), doc2.content.size);
92625
93321
  const currentSelection = activeEditor.state.selection;
92626
- const isSameCursor = currentSelection instanceof TextSelection$1 && currentSelection.from === pos && currentSelection.to === pos;
92627
- if (isSameCursor) {
93322
+ if (currentSelection instanceof TextSelection$1 && currentSelection.from === pos && currentSelection.to === pos) {
92628
93323
  return;
92629
93324
  }
92630
93325
  try {
92631
93326
  const tr = activeEditor.state.tr.setSelection(TextSelection$1.create(doc2, pos)).setMeta("addToHistory", false);
92632
93327
  activeEditor.view?.dispatch(tr);
92633
- scheduleSelectionUpdate();
92634
- } catch (error) {
92635
- }
92636
- };
92637
- }
92638
- function createExternalFieldAnnotationDropHandler({
92639
- getActiveEditor,
92640
- hitTest,
92641
- scheduleSelectionUpdate
92642
- }) {
92643
- return (event) => {
92644
- const activeEditor = getActiveEditor();
92645
- if (!activeEditor?.isEditable) {
92646
- return;
92647
- }
92648
- if (event.dataTransfer?.types?.includes("application/x-field-annotation")) {
92649
- return;
93328
+ this.#deps.scheduleSelectionUpdate();
93329
+ } catch {
92650
93330
  }
93331
+ }
93332
+ /**
93333
+ * Handle drop - either move internal annotation or insert external one.
93334
+ */
93335
+ #handleDrop(event) {
93336
+ if (!this.#deps) return;
93337
+ if (!hasFieldAnnotationData(event)) return;
92651
93338
  event.preventDefault();
92652
93339
  event.stopPropagation();
92653
- const fieldAnnotationData = event.dataTransfer?.getData(FIELD_ANNOTATION_DATA_TYPE);
92654
- if (!fieldAnnotationData) {
93340
+ const activeEditor = this.#deps.getActiveEditor();
93341
+ if (!activeEditor?.isEditable) return;
93342
+ const { state, view } = activeEditor;
93343
+ if (!state || !view) return;
93344
+ const hit = this.#deps.hitTest(event.clientX, event.clientY);
93345
+ const fallbackPos = state.selection?.from ?? state.doc?.content.size ?? null;
93346
+ const dropPos = hit?.pos ?? fallbackPos;
93347
+ if (dropPos == null) return;
93348
+ if (isInternalDrag(event)) {
93349
+ this.#handleInternalDrop(event, dropPos);
92655
93350
  return;
92656
93351
  }
92657
- const hit = hitTest(event.clientX, event.clientY);
92658
- const selection = activeEditor.state?.selection;
92659
- const fallbackPos = selection?.from ?? activeEditor.state?.doc?.content.size ?? null;
92660
- const pos = hit?.pos ?? fallbackPos;
92661
- if (pos == null) {
92662
- return;
93352
+ this.#handleExternalDrop(event, dropPos);
93353
+ }
93354
+ /**
93355
+ * Handle internal drop - move field annotation within document.
93356
+ */
93357
+ #handleInternalDrop(event, targetPos) {
93358
+ if (!this.#deps) return;
93359
+ const activeEditor = this.#deps.getActiveEditor();
93360
+ const { state, view } = activeEditor;
93361
+ if (!state || !view) return;
93362
+ const data = extractDragData(event);
93363
+ if (!data?.fieldId) return;
93364
+ const pmStart = data.pmStart;
93365
+ let sourceStart = null;
93366
+ let sourceEnd = null;
93367
+ let sourceNode = null;
93368
+ if (pmStart != null) {
93369
+ const nodeAt = state.doc.nodeAt(pmStart);
93370
+ if (nodeAt?.type?.name === "fieldAnnotation") {
93371
+ sourceStart = pmStart;
93372
+ sourceEnd = pmStart + nodeAt.nodeSize;
93373
+ sourceNode = nodeAt;
93374
+ }
92663
93375
  }
93376
+ if (sourceStart == null || sourceEnd == null || !sourceNode) {
93377
+ state.doc.descendants((node2, pos) => {
93378
+ if (node2.type.name === "fieldAnnotation" && node2.attrs.fieldId === data.fieldId) {
93379
+ sourceStart = pos;
93380
+ sourceEnd = pos + node2.nodeSize;
93381
+ sourceNode = node2;
93382
+ return false;
93383
+ }
93384
+ return true;
93385
+ });
93386
+ }
93387
+ if (sourceStart === null || sourceEnd === null || !sourceNode) return;
93388
+ if (targetPos >= sourceStart && targetPos <= sourceEnd) return;
93389
+ const tr = state.tr;
93390
+ tr.delete(sourceStart, sourceEnd);
93391
+ const mappedTarget = tr.mapping.map(targetPos);
93392
+ if (mappedTarget < 0 || mappedTarget > tr.doc.content.size) return;
93393
+ tr.insert(mappedTarget, sourceNode);
93394
+ tr.setMeta("uiEvent", "drop");
93395
+ view.dispatch(tr);
93396
+ }
93397
+ /**
93398
+ * Handle external drop - insert new field annotation.
93399
+ */
93400
+ #handleExternalDrop(event, pos) {
93401
+ if (!this.#deps) return;
93402
+ const activeEditor = this.#deps.getActiveEditor();
93403
+ const fieldAnnotationData = event.dataTransfer?.getData(FIELD_ANNOTATION_DATA_TYPE);
93404
+ if (!fieldAnnotationData) return;
92664
93405
  let parsedData = null;
92665
93406
  try {
92666
93407
  parsedData = JSON.parse(fieldAnnotationData);
@@ -92671,7 +93412,7 @@ ${o}
92671
93412
  activeEditor.emit?.("fieldAnnotationDropped", {
92672
93413
  sourceField,
92673
93414
  editor: activeEditor,
92674
- coordinates: hit,
93415
+ coordinates: this.#deps.hitTest(event.clientX, event.clientY),
92675
93416
  pos
92676
93417
  });
92677
93418
  if (attributes && isValidFieldAnnotationAttributes(attributes)) {
@@ -92681,14 +93422,49 @@ ${o}
92681
93422
  if (tr) {
92682
93423
  activeEditor.view?.dispatch(tr);
92683
93424
  }
92684
- scheduleSelectionUpdate();
93425
+ this.#deps.scheduleSelectionUpdate();
92685
93426
  }
92686
93427
  const editorDom = activeEditor.view?.dom;
92687
93428
  if (editorDom) {
92688
93429
  editorDom.focus();
92689
93430
  activeEditor.view?.focus();
92690
93431
  }
92691
- };
93432
+ }
93433
+ #handleDragEnd(_event) {
93434
+ this.#deps?.getPainterHost()?.classList.remove("drag-over");
93435
+ }
93436
+ #handleDragLeave(event) {
93437
+ const painterHost = this.#deps?.getPainterHost();
93438
+ if (!painterHost) return;
93439
+ const relatedTarget = event.relatedTarget;
93440
+ if (!relatedTarget || !painterHost.contains(relatedTarget)) {
93441
+ painterHost.classList.remove("drag-over");
93442
+ }
93443
+ }
93444
+ /**
93445
+ * Window-level dragover to allow drops on overlay elements.
93446
+ */
93447
+ #handleWindowDragOver(event) {
93448
+ if (!hasFieldAnnotationData(event)) return;
93449
+ const viewportHost = this.#deps?.getViewportHost();
93450
+ const target = event.target;
93451
+ if (viewportHost?.contains(target)) return;
93452
+ event.preventDefault();
93453
+ if (event.dataTransfer) {
93454
+ event.dataTransfer.dropEffect = isInternalDrag(event) ? "move" : "copy";
93455
+ }
93456
+ this.#handleDragOver(event);
93457
+ }
93458
+ /**
93459
+ * Window-level drop to catch drops on overlay elements.
93460
+ */
93461
+ #handleWindowDrop(event) {
93462
+ if (!hasFieldAnnotationData(event)) return;
93463
+ const viewportHost = this.#deps?.getViewportHost();
93464
+ const target = event.target;
93465
+ if (viewportHost?.contains(target)) return;
93466
+ this.#handleDrop(event);
93467
+ }
92692
93468
  }
92693
93469
  var SectionType = /* @__PURE__ */ ((SectionType2) => {
92694
93470
  SectionType2["CONTINUOUS"] = "continuous";
@@ -94696,16 +95472,17 @@ ${o}
94696
95472
  const afterRaw = pickNumber(source.after);
94697
95473
  const lineRaw = pickNumber(source.line);
94698
95474
  const lineRule = normalizeLineRule(source.lineRule);
95475
+ const resolvedLineRule = lineRule ?? (lineRaw != null ? "auto" : void 0);
94699
95476
  const beforeAutospacing = toBooleanFlag(source.beforeAutospacing ?? source.beforeAutoSpacing);
94700
95477
  const afterAutospacing = toBooleanFlag(source.afterAutospacing ?? source.afterAutoSpacing);
94701
95478
  const contextualSpacing = toBooleanFlag(source.contextualSpacing);
94702
95479
  const before = beforeRaw != null ? twipsToPx$1(beforeRaw) : pickNumber(source.lineSpaceBefore);
94703
95480
  const after = afterRaw != null ? twipsToPx$1(afterRaw) : pickNumber(source.lineSpaceAfter);
94704
- const line = normalizeLineValue(lineRaw, lineRule);
95481
+ const line = normalizeLineValue(lineRaw, resolvedLineRule);
94705
95482
  if (before != null) spacing.before = before;
94706
95483
  if (after != null) spacing.after = after;
94707
95484
  if (line != null) spacing.line = line;
94708
- if (lineRule) spacing.lineRule = lineRule;
95485
+ if (resolvedLineRule) spacing.lineRule = resolvedLineRule;
94709
95486
  if (beforeAutospacing != null) spacing.beforeAutospacing = beforeAutospacing;
94710
95487
  if (afterAutospacing != null) spacing.afterAutospacing = afterAutospacing;
94711
95488
  if (contextualSpacing != null) spacing.contextualSpacing = contextualSpacing;
@@ -104521,8 +105298,6 @@ ${o}
104521
105298
  const DEFAULT_VIRTUALIZED_PAGE_GAP = 72;
104522
105299
  const DEFAULT_PAGE_GAP = 24;
104523
105300
  const DEFAULT_HORIZONTAL_PAGE_GAP = 20;
104524
- const MULTI_CLICK_TIME_THRESHOLD_MS = 400;
104525
- const MULTI_CLICK_DISTANCE_THRESHOLD_PX = 5;
104526
105301
  const HEADER_FOOTER_INIT_BUDGET_MS = 200;
104527
105302
  const MAX_ZOOM_WARNING_THRESHOLD = 10;
104528
105303
  const MAX_SELECTION_RECTS_PER_USER = 100;
@@ -104578,7 +105353,7 @@ ${o}
104578
105353
  #layoutState = { blocks: [], measures: [], layout: null, bookmarks: /* @__PURE__ */ new Map() };
104579
105354
  #domPainter = null;
104580
105355
  #pageGeometryHelper = null;
104581
- #dragHandlerCleanup = null;
105356
+ #dragDropManager = null;
104582
105357
  #layoutError = null;
104583
105358
  #layoutErrorState = "healthy";
104584
105359
  #errorBanner = null;
@@ -104595,9 +105370,6 @@ ${o}
104595
105370
  #htmlAnnotationMeasureAttempts = 0;
104596
105371
  #domPositionIndex = new DomPositionIndex();
104597
105372
  #domIndexObserverManager = null;
104598
- #debugLastPointer = null;
104599
- #debugLastHit = null;
104600
- #pendingMarginClick = null;
104601
105373
  #rafHandle = null;
104602
105374
  #editorListeners = [];
104603
105375
  #sectionMetadata = [];
@@ -104614,25 +105386,7 @@ ${o}
104614
105386
  #ariaLiveRegion = null;
104615
105387
  #a11ySelectionAnnounceTimeout = null;
104616
105388
  #a11yLastAnnouncedSelectionKey = null;
104617
- #clickCount = 0;
104618
- #lastClickTime = 0;
104619
- #lastClickPosition = { x: 0, y: 0 };
104620
- #lastSelectedImageBlockId = null;
104621
105389
  #lastSelectedFieldAnnotation = null;
104622
- // Drag selection state
104623
- #dragAnchor = null;
104624
- #dragAnchorPageIndex = null;
104625
- #isDragging = false;
104626
- #dragExtensionMode = "char";
104627
- #dragLastPointer = null;
104628
- #dragLastRawHit = null;
104629
- #dragUsedPageNotMountedFallback = false;
104630
- #suppressFocusInFromDraggable = false;
104631
- // Cell selection drag state
104632
- // Tracks cell-specific context when drag starts in a table for multi-cell selection
104633
- #cellAnchor = null;
104634
- /** Cell drag mode state machine: 'none' = not in table, 'pending' = in table but haven't crossed cell boundary, 'active' = crossed cell boundary */
104635
- #cellDragMode = "none";
104636
105390
  // Remote cursor/presence state management
104637
105391
  /** Manager for remote cursor rendering and awareness subscriptions */
104638
105392
  #remoteCursorManager = null;
@@ -104640,6 +105394,9 @@ ${o}
104640
105394
  #remoteCursorOverlay = null;
104641
105395
  /** DOM element for rendering local selection/caret (dual-layer overlay architecture) */
104642
105396
  #localSelectionLayer = null;
105397
+ // Editor input management
105398
+ /** Manager for pointer events, focus, drag selection, and click handling */
105399
+ #editorInputManager = null;
104643
105400
  constructor(options) {
104644
105401
  super();
104645
105402
  if (!options?.element) {
@@ -104848,6 +105605,7 @@ ${o}
104848
105605
  this.#setupHeaderFooterSession();
104849
105606
  this.#applyZoom();
104850
105607
  this.#setupEditorListeners();
105608
+ this.#initializeEditorInputManager();
104851
105609
  this.#setupPointerHandlers();
104852
105610
  this.#setupDragHandlers();
104853
105611
  this.#setupInputBridge();
@@ -105769,8 +106527,8 @@ ${o}
105769
106527
  docEpoch: this.#epochMapper.getCurrentEpoch(),
105770
106528
  layoutEpoch: this.#layoutEpoch,
105771
106529
  selection,
105772
- lastPointer: this.#debugLastPointer,
105773
- lastHit: this.#debugLastHit
106530
+ lastPointer: this.#editorInputManager?.debugLastPointer ?? null,
106531
+ lastHit: this.#editorInputManager?.debugLastHit ?? null
105774
106532
  });
105775
106533
  } catch {
105776
106534
  }
@@ -106163,15 +106921,12 @@ ${o}
106163
106921
  this.#editorListeners = [];
106164
106922
  this.#domIndexObserverManager?.destroy();
106165
106923
  this.#domIndexObserverManager = null;
106166
- this.#viewportHost?.removeEventListener("pointerdown", this.#handlePointerDown);
106167
- this.#viewportHost?.removeEventListener("dblclick", this.#handleDoubleClick);
106168
- this.#viewportHost?.removeEventListener("pointermove", this.#handlePointerMove);
106169
- this.#viewportHost?.removeEventListener("pointerup", this.#handlePointerUp);
106170
- this.#viewportHost?.removeEventListener("pointerleave", this.#handlePointerLeave);
106171
- this.#viewportHost?.removeEventListener("dragover", this.#handleDragOver);
106172
- this.#viewportHost?.removeEventListener("drop", this.#handleDrop);
106173
- this.#visibleHost?.removeEventListener("keydown", this.#handleKeyDown);
106174
- this.#visibleHost?.removeEventListener("focusin", this.#handleVisibleHostFocusIn);
106924
+ if (this.#editorInputManager) {
106925
+ safeCleanup(() => {
106926
+ this.#editorInputManager?.destroy();
106927
+ this.#editorInputManager = null;
106928
+ }, "Editor input manager");
106929
+ }
106175
106930
  this.#inputBridge?.notifyTargetChanged();
106176
106931
  this.#inputBridge?.destroy();
106177
106932
  this.#inputBridge = null;
@@ -106179,7 +106934,6 @@ ${o}
106179
106934
  clearTimeout(this.#a11ySelectionAnnounceTimeout);
106180
106935
  this.#a11ySelectionAnnounceTimeout = null;
106181
106936
  }
106182
- this.#clearCellAnchor();
106183
106937
  if (this.#options?.documentId) {
106184
106938
  PresentationEditor.#instances.delete(this.#options.documentId);
106185
106939
  }
@@ -106189,8 +106943,8 @@ ${o}
106189
106943
  }, "Header/footer session manager");
106190
106944
  this.#domPainter = null;
106191
106945
  this.#pageGeometryHelper = null;
106192
- this.#dragHandlerCleanup?.();
106193
- this.#dragHandlerCleanup = null;
106946
+ this.#dragDropManager?.destroy();
106947
+ this.#dragDropManager = null;
106194
106948
  this.#selectionOverlay?.remove();
106195
106949
  this.#painterHost?.remove();
106196
106950
  this.#hiddenHost?.remove();
@@ -106237,7 +106991,7 @@ ${o}
106237
106991
  }
106238
106992
  if (transaction?.docChanged) {
106239
106993
  this.#updateLocalAwarenessCursor();
106240
- this.#clearCellAnchor();
106994
+ this.#editorInputManager?.clearCellAnchor();
106241
106995
  }
106242
106996
  };
106243
106997
  const handleSelection = () => {
@@ -106333,39 +107087,83 @@ ${o}
106333
107087
  cursors: Array.from(this.#remoteCursorManager.state.values())
106334
107088
  });
106335
107089
  }
106336
- /**
106337
- * Render remote cursors from existing state without normalization.
106338
- * Delegates to RemoteCursorManager.
106339
- * @private
106340
- */
106341
- #renderRemoteCursors() {
106342
- this.#remoteCursorManager?.render(this.#getRemoteCursorRenderDeps());
106343
- }
107090
+ /**
107091
+ * Render remote cursors from existing state without normalization.
107092
+ * Delegates to RemoteCursorManager.
107093
+ * @private
107094
+ */
107095
+ #renderRemoteCursors() {
107096
+ this.#remoteCursorManager?.render(this.#getRemoteCursorRenderDeps());
107097
+ }
107098
+ /**
107099
+ * Initialize the EditorInputManager with dependencies and callbacks.
107100
+ * @private
107101
+ */
107102
+ #initializeEditorInputManager() {
107103
+ this.#editorInputManager = new EditorInputManager();
107104
+ this.#editorInputManager.setDependencies({
107105
+ getActiveEditor: () => this.getActiveEditor(),
107106
+ getEditor: () => this.#editor,
107107
+ getLayoutState: () => this.#layoutState,
107108
+ getEpochMapper: () => this.#epochMapper,
107109
+ getViewportHost: () => this.#viewportHost,
107110
+ getVisibleHost: () => this.#visibleHost,
107111
+ getHeaderFooterSession: () => this.#headerFooterSession,
107112
+ getPageGeometryHelper: () => this.#pageGeometryHelper,
107113
+ getZoom: () => this.#layoutOptions.zoom ?? 1,
107114
+ isViewLocked: () => this.#isViewLocked(),
107115
+ getDocumentMode: () => this.#documentMode,
107116
+ getPageElement: (pageIndex) => this.#getPageElement(pageIndex),
107117
+ isSelectionAwareVirtualizationEnabled: () => this.#isSelectionAwareVirtualizationEnabled()
107118
+ });
107119
+ this.#editorInputManager.setCallbacks({
107120
+ scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate(),
107121
+ scheduleRerender: () => this.#scheduleRerender(),
107122
+ setPendingDocChange: () => {
107123
+ this.#pendingDocChange = true;
107124
+ },
107125
+ updateSelectionVirtualizationPins: (options) => this.#updateSelectionVirtualizationPins(options),
107126
+ scheduleA11ySelectionAnnouncement: (options) => this.#scheduleA11ySelectionAnnouncement(options),
107127
+ goToAnchor: (href) => this.goToAnchor(href),
107128
+ emit: (event, payload) => this.emit(event, payload),
107129
+ normalizeClientPoint: (clientX, clientY) => this.#normalizeClientPoint(clientX, clientY),
107130
+ hitTestHeaderFooterRegion: (x2, y2) => this.#hitTestHeaderFooterRegion(x2, y2),
107131
+ exitHeaderFooterMode: () => this.#exitHeaderFooterMode(),
107132
+ activateHeaderFooterRegion: (region) => this.#activateHeaderFooterRegion(region),
107133
+ createDefaultHeaderFooter: (region) => this.#createDefaultHeaderFooter(region),
107134
+ emitHeaderFooterEditBlocked: (reason) => this.#emitHeaderFooterEditBlocked(reason),
107135
+ findRegionForPage: (kind, pageIndex) => this.#findRegionForPage(kind, pageIndex),
107136
+ getCurrentPageIndex: () => this.#getCurrentPageIndex(),
107137
+ resolveDescriptorForRegion: (region) => this.#resolveDescriptorForRegion(region),
107138
+ updateSelectionDebugHud: () => this.#updateSelectionDebugHud(),
107139
+ clearHoverRegion: () => this.#clearHoverRegion(),
107140
+ renderHoverRegion: (region) => this.#renderHoverRegion(region),
107141
+ focusEditorAfterImageSelection: () => this.#focusEditorAfterImageSelection(),
107142
+ resolveFieldAnnotationSelectionFromElement: (el) => this.#resolveFieldAnnotationSelectionFromElement(el),
107143
+ computePendingMarginClick: (pointerId, x2, y2) => this.#computePendingMarginClick(pointerId, x2, y2),
107144
+ selectWordAt: (pos) => this.#selectWordAt(pos),
107145
+ selectParagraphAt: (pos) => this.#selectParagraphAt(pos),
107146
+ finalizeDragSelectionWithDom: (pointer, dragAnchor, dragMode) => this.#finalizeDragSelectionWithDom(pointer, dragAnchor, dragMode),
107147
+ hitTestTable: (x2, y2) => this.#hitTestTable(x2, y2)
107148
+ });
107149
+ }
106344
107150
  #setupPointerHandlers() {
106345
- this.#viewportHost.addEventListener("pointerdown", this.#handlePointerDown);
106346
- this.#viewportHost.addEventListener("dblclick", this.#handleDoubleClick);
106347
- this.#viewportHost.addEventListener("pointermove", this.#handlePointerMove);
106348
- this.#viewportHost.addEventListener("pointerup", this.#handlePointerUp);
106349
- this.#viewportHost.addEventListener("pointerleave", this.#handlePointerLeave);
106350
- this.#viewportHost.addEventListener("dragover", this.#handleDragOver);
106351
- this.#viewportHost.addEventListener("drop", this.#handleDrop);
106352
- this.#visibleHost.addEventListener("keydown", this.#handleKeyDown);
106353
- this.#visibleHost.addEventListener("focusin", this.#handleVisibleHostFocusIn);
107151
+ this.#editorInputManager?.bind();
106354
107152
  }
106355
107153
  /**
106356
- * Sets up drag and drop handlers for field annotations in the layout engine view.
106357
- * Uses the DragHandler from layout-bridge to handle drag events and map drop
106358
- * coordinates to ProseMirror positions.
107154
+ * Sets up drag and drop handlers for field annotations.
106359
107155
  */
106360
107156
  #setupDragHandlers() {
106361
- this.#dragHandlerCleanup?.();
106362
- this.#dragHandlerCleanup = null;
106363
- this.#dragHandlerCleanup = setupInternalFieldAnnotationDragHandlers({
106364
- painterHost: this.#painterHost,
107157
+ this.#dragDropManager?.destroy();
107158
+ this.#dragDropManager = new DragDropManager();
107159
+ this.#dragDropManager.setDependencies({
106365
107160
  getActiveEditor: () => this.getActiveEditor(),
106366
107161
  hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
106367
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
107162
+ scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate(),
107163
+ getViewportHost: () => this.#viewportHost,
107164
+ getPainterHost: () => this.#painterHost
106368
107165
  });
107166
+ this.#dragDropManager.bind();
106369
107167
  }
106370
107168
  /**
106371
107169
  * Focus the editor after image selection and schedule selection update.
@@ -106483,516 +107281,6 @@ ${o}
106483
107281
  });
106484
107282
  this.#headerFooterSession.initialize();
106485
107283
  }
106486
- #handlePointerDown = (event) => {
106487
- if (event.button !== 0) {
106488
- return;
106489
- }
106490
- if (event.ctrlKey && navigator.platform.includes("Mac")) {
106491
- return;
106492
- }
106493
- this.#pendingMarginClick = null;
106494
- const target = event.target;
106495
- if (target?.closest?.(".superdoc-ruler-handle") != null) {
106496
- return;
106497
- }
106498
- const linkEl = target?.closest?.("a.superdoc-link");
106499
- if (linkEl) {
106500
- const href = linkEl.getAttribute("href") ?? "";
106501
- const isAnchorLink = href.startsWith("#") && href.length > 1;
106502
- const isTocLink = linkEl.closest(".superdoc-toc-entry") !== null;
106503
- if (isAnchorLink && isTocLink) {
106504
- event.preventDefault();
106505
- event.stopPropagation();
106506
- this.goToAnchor(href);
106507
- return;
106508
- }
106509
- event.preventDefault();
106510
- event.stopPropagation();
106511
- const linkClickEvent = new CustomEvent("superdoc-link-click", {
106512
- bubbles: true,
106513
- composed: true,
106514
- detail: {
106515
- href,
106516
- target: linkEl.getAttribute("target"),
106517
- rel: linkEl.getAttribute("rel"),
106518
- tooltip: linkEl.getAttribute("title"),
106519
- element: linkEl,
106520
- clientX: event.clientX,
106521
- clientY: event.clientY
106522
- }
106523
- });
106524
- linkEl.dispatchEvent(linkClickEvent);
106525
- return;
106526
- }
106527
- const annotationEl = target?.closest?.(".annotation[data-pm-start]");
106528
- const isDraggableAnnotation = target?.closest?.('[data-draggable="true"]') != null;
106529
- this.#suppressFocusInFromDraggable = isDraggableAnnotation;
106530
- if (annotationEl) {
106531
- if (!this.#editor.isEditable) {
106532
- return;
106533
- }
106534
- const resolved = this.#resolveFieldAnnotationSelectionFromElement(annotationEl);
106535
- if (resolved) {
106536
- try {
106537
- const tr = this.#editor.state.tr.setSelection(NodeSelection.create(this.#editor.state.doc, resolved.pos));
106538
- this.#editor.view?.dispatch(tr);
106539
- } catch {
106540
- }
106541
- this.#editor.emit("fieldAnnotationClicked", {
106542
- editor: this.#editor,
106543
- node: resolved.node,
106544
- nodePos: resolved.pos,
106545
- event,
106546
- currentTarget: annotationEl
106547
- });
106548
- }
106549
- return;
106550
- }
106551
- if (!this.#layoutState.layout) {
106552
- if (!isDraggableAnnotation) {
106553
- event.preventDefault();
106554
- }
106555
- if (document.activeElement instanceof HTMLElement) {
106556
- document.activeElement.blur();
106557
- }
106558
- const editorDom2 = this.#editor.view?.dom;
106559
- if (!editorDom2) {
106560
- return;
106561
- }
106562
- const validPos = this.#getFirstTextPosition();
106563
- const doc22 = this.#editor?.state?.doc;
106564
- if (doc22) {
106565
- try {
106566
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(doc22, validPos));
106567
- this.#editor.view?.dispatch(tr);
106568
- } catch (error) {
106569
- }
106570
- }
106571
- editorDom2.focus();
106572
- this.#editor.view?.focus();
106573
- this.#scheduleSelectionUpdate();
106574
- return;
106575
- }
106576
- const normalizedPoint = this.#normalizeClientPoint(event.clientX, event.clientY);
106577
- if (!normalizedPoint) {
106578
- return;
106579
- }
106580
- const { x: x2, y: y2 } = normalizedPoint;
106581
- this.#debugLastPointer = { clientX: event.clientX, clientY: event.clientY, x: x2, y: y2 };
106582
- const sessionMode = this.#headerFooterSession?.session?.mode ?? "body";
106583
- if (sessionMode !== "body") {
106584
- const activeEditorHost = this.#headerFooterSession?.overlayManager?.getActiveEditorHost?.();
106585
- const clickedInsideEditorHost = activeEditorHost && (activeEditorHost.contains(event.target) || activeEditorHost === event.target);
106586
- if (clickedInsideEditorHost) {
106587
- return;
106588
- }
106589
- const headerFooterRegion2 = this.#hitTestHeaderFooterRegion(x2, y2);
106590
- if (!headerFooterRegion2) {
106591
- this.#exitHeaderFooterMode();
106592
- } else {
106593
- return;
106594
- }
106595
- }
106596
- const headerFooterRegion = this.#hitTestHeaderFooterRegion(x2, y2);
106597
- if (headerFooterRegion) {
106598
- return;
106599
- }
106600
- const rawHit = clickToPosition(
106601
- this.#layoutState.layout,
106602
- this.#layoutState.blocks,
106603
- this.#layoutState.measures,
106604
- { x: x2, y: y2 },
106605
- this.#viewportHost,
106606
- event.clientX,
106607
- event.clientY,
106608
- this.#pageGeometryHelper ?? void 0
106609
- );
106610
- const doc2 = this.#editor.state?.doc;
106611
- const mapped = rawHit && doc2 ? this.#epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1) : null;
106612
- if (mapped && !mapped.ok) {
106613
- debugLog("warn", "pointerdown mapping failed", mapped);
106614
- }
106615
- const hit = rawHit && doc2 && mapped?.ok ? { ...rawHit, pos: Math.max(0, Math.min(mapped.pos, doc2.content.size)), layoutEpoch: mapped.toEpoch } : null;
106616
- this.#debugLastHit = hit ? { source: "dom", pos: rawHit?.pos ?? null, layoutEpoch: rawHit?.layoutEpoch ?? null, mappedPos: hit.pos } : { source: "none", pos: rawHit?.pos ?? null, layoutEpoch: rawHit?.layoutEpoch ?? null, mappedPos: null };
106617
- this.#updateSelectionDebugHud();
106618
- if (!isDraggableAnnotation) {
106619
- event.preventDefault();
106620
- }
106621
- if (!rawHit) {
106622
- if (document.activeElement instanceof HTMLElement) {
106623
- document.activeElement.blur();
106624
- }
106625
- const editorDom2 = this.#editor.view?.dom;
106626
- if (editorDom2) {
106627
- const validPos = this.#getFirstTextPosition();
106628
- const doc22 = this.#editor?.state?.doc;
106629
- if (doc22) {
106630
- try {
106631
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(doc22, validPos));
106632
- this.#editor.view?.dispatch(tr);
106633
- } catch (error) {
106634
- }
106635
- }
106636
- editorDom2.focus();
106637
- this.#editor.view?.focus();
106638
- this.#scheduleSelectionUpdate();
106639
- }
106640
- return;
106641
- }
106642
- if (!hit || !doc2) {
106643
- this.#pendingDocChange = true;
106644
- this.#scheduleRerender();
106645
- return;
106646
- }
106647
- const fragmentHit = getFragmentAtPosition(
106648
- this.#layoutState.layout,
106649
- this.#layoutState.blocks,
106650
- this.#layoutState.measures,
106651
- rawHit.pos
106652
- );
106653
- const targetImg = event.target?.closest?.("img");
106654
- const imgPmStart = targetImg?.dataset?.pmStart ? Number(targetImg.dataset.pmStart) : null;
106655
- if (!Number.isNaN(imgPmStart) && imgPmStart != null) {
106656
- const doc22 = this.#editor.state.doc;
106657
- const imgLayoutEpochRaw = targetImg?.dataset?.layoutEpoch;
106658
- const imgLayoutEpoch = imgLayoutEpochRaw != null ? Number(imgLayoutEpochRaw) : NaN;
106659
- const rawLayoutEpoch = Number.isFinite(rawHit.layoutEpoch) ? rawHit.layoutEpoch : NaN;
106660
- const effectiveEpoch = Number.isFinite(imgLayoutEpoch) && Number.isFinite(rawLayoutEpoch) ? Math.max(imgLayoutEpoch, rawLayoutEpoch) : Number.isFinite(imgLayoutEpoch) ? imgLayoutEpoch : rawHit.layoutEpoch;
106661
- const mappedImg = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(imgPmStart, effectiveEpoch, 1);
106662
- if (!mappedImg.ok) {
106663
- debugLog("warn", "inline image mapping failed", mappedImg);
106664
- this.#pendingDocChange = true;
106665
- this.#scheduleRerender();
106666
- return;
106667
- }
106668
- const clampedImgPos = Math.max(0, Math.min(mappedImg.pos, doc22.content.size));
106669
- if (clampedImgPos < 0 || clampedImgPos >= doc22.content.size) {
106670
- return;
106671
- }
106672
- const newSelectionId = `inline-${clampedImgPos}`;
106673
- if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== newSelectionId) {
106674
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
106675
- }
106676
- try {
106677
- const tr = this.#editor.state.tr.setSelection(NodeSelection.create(doc22, clampedImgPos));
106678
- this.#editor.view?.dispatch(tr);
106679
- const selector = `.superdoc-inline-image[data-pm-start="${imgPmStart}"]`;
106680
- const targetElement = this.#viewportHost.querySelector(selector);
106681
- this.emit("imageSelected", {
106682
- element: targetElement ?? targetImg,
106683
- blockId: null,
106684
- pmStart: clampedImgPos
106685
- });
106686
- this.#lastSelectedImageBlockId = newSelectionId;
106687
- } catch (error) {
106688
- }
106689
- this.#focusEditorAfterImageSelection();
106690
- return;
106691
- }
106692
- if (fragmentHit && (fragmentHit.fragment.kind === "image" || fragmentHit.fragment.kind === "drawing")) {
106693
- const doc22 = this.#editor.state.doc;
106694
- try {
106695
- const tr = this.#editor.state.tr.setSelection(NodeSelection.create(doc22, hit.pos));
106696
- this.#editor.view?.dispatch(tr);
106697
- if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== fragmentHit.fragment.blockId) {
106698
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
106699
- }
106700
- if (fragmentHit.fragment.kind === "image") {
106701
- const targetElement = this.#viewportHost.querySelector(
106702
- `.superdoc-image-fragment[data-pm-start="${fragmentHit.fragment.pmStart}"]`
106703
- );
106704
- if (targetElement) {
106705
- this.emit("imageSelected", {
106706
- element: targetElement,
106707
- blockId: fragmentHit.fragment.blockId,
106708
- pmStart: fragmentHit.fragment.pmStart
106709
- });
106710
- this.#lastSelectedImageBlockId = fragmentHit.fragment.blockId;
106711
- }
106712
- }
106713
- } catch (error) {
106714
- }
106715
- this.#focusEditorAfterImageSelection();
106716
- return;
106717
- }
106718
- if (this.#lastSelectedImageBlockId) {
106719
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
106720
- this.#lastSelectedImageBlockId = null;
106721
- }
106722
- if (event.shiftKey && this.#editor.state.selection.$anchor) {
106723
- const anchor = this.#editor.state.selection.anchor;
106724
- const head = hit.pos;
106725
- const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
106726
- try {
106727
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(this.#editor.state.doc, selAnchor, selHead));
106728
- this.#editor.view?.dispatch(tr);
106729
- this.#scheduleSelectionUpdate();
106730
- } catch (error) {
106731
- console.warn("[SELECTION] Failed to extend selection on shift+click:", {
106732
- error,
106733
- anchor,
106734
- head,
106735
- selAnchor,
106736
- selHead,
106737
- mode: this.#dragExtensionMode
106738
- });
106739
- }
106740
- if (document.activeElement instanceof HTMLElement) {
106741
- document.activeElement.blur();
106742
- }
106743
- const editorDom2 = this.#editor.view?.dom;
106744
- if (editorDom2) {
106745
- editorDom2.focus();
106746
- this.#editor.view?.focus();
106747
- }
106748
- return;
106749
- }
106750
- const clickDepth = this.#registerPointerClick(event);
106751
- if (clickDepth === 1) {
106752
- this.#dragAnchor = hit.pos;
106753
- this.#dragAnchorPageIndex = hit.pageIndex;
106754
- this.#pendingMarginClick = this.#computePendingMarginClick(event.pointerId, x2, y2);
106755
- const tableHit = this.#hitTestTable(x2, y2);
106756
- if (tableHit) {
106757
- const tablePos = this.#getTablePosFromHit(tableHit);
106758
- if (tablePos !== null) {
106759
- this.#setCellAnchor(tableHit, tablePos);
106760
- }
106761
- } else {
106762
- this.#clearCellAnchor();
106763
- }
106764
- } else {
106765
- this.#pendingMarginClick = null;
106766
- }
106767
- this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: x2, y: y2 };
106768
- this.#dragLastRawHit = hit;
106769
- this.#dragUsedPageNotMountedFallback = false;
106770
- this.#isDragging = true;
106771
- if (clickDepth >= 3) {
106772
- this.#dragExtensionMode = "para";
106773
- } else if (clickDepth === 2) {
106774
- this.#dragExtensionMode = "word";
106775
- } else {
106776
- this.#dragExtensionMode = "char";
106777
- }
106778
- debugLog(
106779
- "verbose",
106780
- `Drag selection start ${JSON.stringify({
106781
- pointer: { clientX: event.clientX, clientY: event.clientY, x: x2, y: y2 },
106782
- clickDepth,
106783
- extensionMode: this.#dragExtensionMode,
106784
- anchor: this.#dragAnchor,
106785
- anchorPageIndex: this.#dragAnchorPageIndex,
106786
- rawHit: rawHit ? {
106787
- pos: rawHit.pos,
106788
- pageIndex: rawHit.pageIndex,
106789
- blockId: rawHit.blockId,
106790
- lineIndex: rawHit.lineIndex,
106791
- layoutEpoch: rawHit.layoutEpoch
106792
- } : null,
106793
- mapped: mapped ? mapped.ok ? { ok: true, pos: mapped.pos, fromEpoch: mapped.fromEpoch, toEpoch: mapped.toEpoch } : {
106794
- ok: false,
106795
- reason: mapped.reason,
106796
- fromEpoch: mapped.fromEpoch,
106797
- toEpoch: mapped.toEpoch
106798
- } : null,
106799
- hit: hit ? { pos: hit.pos, pageIndex: hit.pageIndex, layoutEpoch: hit.layoutEpoch } : null
106800
- })}`
106801
- );
106802
- if (typeof this.#viewportHost.setPointerCapture === "function") {
106803
- this.#viewportHost.setPointerCapture(event.pointerId);
106804
- }
106805
- let handledByDepth = false;
106806
- const sessionModeForDepth = this.#headerFooterSession?.session?.mode ?? "body";
106807
- if (sessionModeForDepth === "body") {
106808
- const selectionPos = clickDepth >= 2 && this.#dragAnchor !== null ? this.#dragAnchor : hit.pos;
106809
- if (clickDepth >= 3) {
106810
- handledByDepth = this.#selectParagraphAt(selectionPos);
106811
- } else if (clickDepth === 2) {
106812
- handledByDepth = this.#selectWordAt(selectionPos);
106813
- }
106814
- }
106815
- if (!handledByDepth) {
106816
- try {
106817
- const doc22 = this.#editor.state.doc;
106818
- let nextSelection = TextSelection$1.create(doc22, hit.pos);
106819
- if (!nextSelection.$from.parent.inlineContent) {
106820
- nextSelection = Selection.near(doc22.resolve(hit.pos), 1);
106821
- }
106822
- const tr = this.#editor.state.tr.setSelection(nextSelection);
106823
- this.#editor.view?.dispatch(tr);
106824
- } catch {
106825
- }
106826
- }
106827
- this.#scheduleSelectionUpdate();
106828
- if (document.activeElement instanceof HTMLElement) {
106829
- document.activeElement.blur();
106830
- }
106831
- const editorDom = this.#editor.view?.dom;
106832
- if (!editorDom) {
106833
- return;
106834
- }
106835
- editorDom.focus();
106836
- this.#editor.view?.focus();
106837
- };
106838
- /**
106839
- * Finds the first valid text position in the document.
106840
- *
106841
- * Traverses the document tree to locate the first textblock node (paragraph, heading, etc.)
106842
- * and returns a position inside it. This is used when focusing the editor but no specific
106843
- * position is available (e.g., clicking outside text content or before layout is ready).
106844
- *
106845
- * @returns The position inside the first textblock, or 1 if no textblock is found
106846
- * @private
106847
- */
106848
- #getFirstTextPosition() {
106849
- return getFirstTextPosition(this.#editor?.state?.doc ?? null);
106850
- }
106851
- /**
106852
- * Registers a pointer click event and tracks multi-click sequences (double, triple).
106853
- *
106854
- * This method implements multi-click detection by tracking the timing and position
106855
- * of consecutive clicks. Clicks within 400ms and 5px of each other increment the
106856
- * click count, up to a maximum of 3 (single, double, triple).
106857
- *
106858
- * @param event - The mouse event from the pointer down handler
106859
- * @returns The current click count (1 = single, 2 = double, 3 = triple)
106860
- * @private
106861
- */
106862
- #registerPointerClick(event) {
106863
- const nextState = registerPointerClick(
106864
- event,
106865
- { clickCount: this.#clickCount, lastClickTime: this.#lastClickTime, lastClickPosition: this.#lastClickPosition },
106866
- {
106867
- timeThresholdMs: MULTI_CLICK_TIME_THRESHOLD_MS,
106868
- distanceThresholdPx: MULTI_CLICK_DISTANCE_THRESHOLD_PX,
106869
- maxClickCount: 3
106870
- }
106871
- );
106872
- this.#clickCount = nextState.clickCount;
106873
- this.#lastClickTime = nextState.lastClickTime;
106874
- this.#lastClickPosition = nextState.lastClickPosition;
106875
- return nextState.clickCount;
106876
- }
106877
- // ============================================================================
106878
- // Cell Selection Utilities
106879
- // ============================================================================
106880
- /**
106881
- * Gets the ProseMirror position at the start of a table cell from a table hit result.
106882
- *
106883
- * This method navigates the ProseMirror document structure to find the exact position where
106884
- * a table cell begins. The position returned is suitable for use with CellSelection.create().
106885
- *
106886
- * Algorithm:
106887
- * 1. Validate input (tableHit structure and cell indices)
106888
- * 2. Traverse document to find the table node matching tableHit.block.id
106889
- * 3. Navigate through table structure (table > row > cell) to target row
106890
- * 4. Track logical column position accounting for colspan (handles merged cells)
106891
- * 5. Return position when target column falls within a cell's span
106892
- *
106893
- * Merged cell handling:
106894
- * - Does NOT assume 1:1 mapping between cell index and logical column
106895
- * - Tracks cumulative logical column position by summing colspan values
106896
- * - A cell with colspan=3 occupies logical columns [n, n+1, n+2]
106897
- * - Finds the cell whose logical span contains the target column index
106898
- *
106899
- * Error handling:
106900
- * - Input validation with console warnings for debugging
106901
- * - Try-catch around document traversal (catches corrupted document errors)
106902
- * - Bounds checking for row indices
106903
- * - Null checks at each navigation step
106904
- *
106905
- * @param tableHit - The table hit result from hitTestTableFragment containing:
106906
- * - block: TableBlock with the table's block ID
106907
- * - cellRowIndex: 0-based row index of the target cell
106908
- * - cellColIndex: 0-based logical column index of the target cell
106909
- * @returns The PM position at the start of the cell, or null if:
106910
- * - Invalid input (null tableHit, negative indices)
106911
- * - Table not found in document
106912
- * - Target row out of bounds
106913
- * - Target column not found in row
106914
- * - Document traversal error
106915
- * @private
106916
- *
106917
- * @throws Never throws - all errors are caught and logged, returns null on failure
106918
- */
106919
- #getCellPosFromTableHit(tableHit) {
106920
- return getCellPosFromTableHit(tableHit, this.#editor.state?.doc ?? null, this.#layoutState.blocks);
106921
- }
106922
- /**
106923
- * Gets the table position (start of table node) from a table hit result.
106924
- *
106925
- * @param tableHit - The table hit result from hitTestTableFragment
106926
- * @returns The PM position at the start of the table, or null if not found
106927
- * @private
106928
- */
106929
- #getTablePosFromHit(tableHit) {
106930
- return getTablePosFromHit(tableHit, this.#editor.state?.doc ?? null, this.#layoutState.blocks);
106931
- }
106932
- /**
106933
- * Determines if the current drag should create a CellSelection instead of TextSelection.
106934
- *
106935
- * Implements a state machine for table cell selection:
106936
- * - 'none': Not in a table, use TextSelection
106937
- * - 'pending': Started drag in a table, but haven't crossed cell boundary yet
106938
- * - 'active': Crossed cell boundary, use CellSelection
106939
- *
106940
- * State transitions:
106941
- * - none → pending: When drag starts in a table cell (#setCellAnchor)
106942
- * - pending → active: When drag crosses into a different cell (this method returns true)
106943
- * - active → none: When drag ends (#clearCellAnchor)
106944
- * - * → none: When document changes or clicking outside table
106945
- *
106946
- * Decision logic:
106947
- * 1. No cell anchor → false (not in table drag mode)
106948
- * 2. Current position outside table → return current state (stay in 'active' if already there)
106949
- * 3. Different table → treat as outside table
106950
- * 4. Different cell in same table → true (activate cell selection)
106951
- * 5. Same cell → return current state (stay in 'active' if already there, else false)
106952
- *
106953
- * This state machine ensures:
106954
- * - Text selection works normally within a single cell
106955
- * - Cell selection activates smoothly when crossing cell boundaries
106956
- * - Once activated, cell selection persists even if dragging back to anchor cell
106957
- *
106958
- * @param currentTableHit - The table hit result for the current pointer position, or null if not in a table
106959
- * @returns true if we should create a CellSelection, false for TextSelection
106960
- * @private
106961
- */
106962
- #shouldUseCellSelection(currentTableHit) {
106963
- return shouldUseCellSelection(currentTableHit, this.#cellAnchor, this.#cellDragMode);
106964
- }
106965
- /**
106966
- * Stores the cell anchor when a drag operation starts inside a table cell.
106967
- *
106968
- * @param tableHit - The table hit result for the initial click position
106969
- * @param tablePos - The PM position of the table node
106970
- * @private
106971
- */
106972
- #setCellAnchor(tableHit, tablePos) {
106973
- const cellPos = this.#getCellPosFromTableHit(tableHit);
106974
- if (cellPos === null) {
106975
- return;
106976
- }
106977
- this.#cellAnchor = {
106978
- tablePos,
106979
- cellPos,
106980
- cellRowIndex: tableHit.cellRowIndex,
106981
- cellColIndex: tableHit.cellColIndex,
106982
- tableBlockId: tableHit.block.id
106983
- };
106984
- this.#cellDragMode = "pending";
106985
- }
106986
- /**
106987
- * Clears the cell drag state.
106988
- * Called when drag ends or when clicking outside a table.
106989
- *
106990
- * @private
106991
- */
106992
- #clearCellAnchor() {
106993
- this.#cellAnchor = null;
106994
- this.#cellDragMode = "none";
106995
- }
106996
107284
  /**
106997
107285
  * Attempts to perform a table hit test for the given normalized coordinates.
106998
107286
  *
@@ -107097,415 +107385,6 @@ ${o}
107097
107385
  #calculateExtendedSelection(anchor, head, mode) {
107098
107386
  return calculateExtendedSelection(this.#layoutState.blocks, anchor, head, mode);
107099
107387
  }
107100
- #handlePointerMove = (event) => {
107101
- if (!this.#layoutState.layout) return;
107102
- const normalized = this.#normalizeClientPoint(event.clientX, event.clientY);
107103
- if (!normalized) return;
107104
- if (this.#isDragging && this.#dragAnchor !== null && event.buttons & 1) {
107105
- this.#pendingMarginClick = null;
107106
- const prevPointer = this.#dragLastPointer;
107107
- const prevRawHit = this.#dragLastRawHit;
107108
- this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y };
107109
- const rawHit = clickToPosition(
107110
- this.#layoutState.layout,
107111
- this.#layoutState.blocks,
107112
- this.#layoutState.measures,
107113
- { x: normalized.x, y: normalized.y },
107114
- this.#viewportHost,
107115
- event.clientX,
107116
- event.clientY,
107117
- this.#pageGeometryHelper ?? void 0
107118
- );
107119
- if (!rawHit) {
107120
- debugLog(
107121
- "verbose",
107122
- `Drag selection update (no hit) ${JSON.stringify({
107123
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
107124
- prevPointer,
107125
- anchor: this.#dragAnchor
107126
- })}`
107127
- );
107128
- return;
107129
- }
107130
- const doc2 = this.#editor.state?.doc;
107131
- if (!doc2) return;
107132
- this.#dragLastRawHit = rawHit;
107133
- const pageMounted = this.#getPageElement(rawHit.pageIndex) != null;
107134
- if (!pageMounted && this.#isSelectionAwareVirtualizationEnabled()) {
107135
- this.#dragUsedPageNotMountedFallback = true;
107136
- debugLog("warn", "Geometry fallback", { reason: "page_not_mounted", pageIndex: rawHit.pageIndex });
107137
- }
107138
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: true, extraPages: [rawHit.pageIndex] });
107139
- const mappedHead = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1);
107140
- if (!mappedHead.ok) {
107141
- debugLog("warn", "drag mapping failed", mappedHead);
107142
- debugLog(
107143
- "verbose",
107144
- `Drag selection update (map failed) ${JSON.stringify({
107145
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
107146
- prevPointer,
107147
- anchor: this.#dragAnchor,
107148
- rawHit: {
107149
- pos: rawHit.pos,
107150
- pageIndex: rawHit.pageIndex,
107151
- blockId: rawHit.blockId,
107152
- lineIndex: rawHit.lineIndex,
107153
- layoutEpoch: rawHit.layoutEpoch
107154
- },
107155
- mapped: {
107156
- ok: false,
107157
- reason: mappedHead.reason,
107158
- fromEpoch: mappedHead.fromEpoch,
107159
- toEpoch: mappedHead.toEpoch
107160
- }
107161
- })}`
107162
- );
107163
- return;
107164
- }
107165
- const hit = {
107166
- ...rawHit,
107167
- pos: Math.max(0, Math.min(mappedHead.pos, doc2.content.size)),
107168
- layoutEpoch: mappedHead.toEpoch
107169
- };
107170
- this.#debugLastHit = {
107171
- source: pageMounted ? "dom" : "geometry",
107172
- pos: rawHit.pos,
107173
- layoutEpoch: rawHit.layoutEpoch,
107174
- mappedPos: hit.pos
107175
- };
107176
- this.#updateSelectionDebugHud();
107177
- const anchor = this.#dragAnchor;
107178
- const head = hit.pos;
107179
- const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
107180
- debugLog(
107181
- "verbose",
107182
- `Drag selection update ${JSON.stringify({
107183
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
107184
- prevPointer,
107185
- rawHit: {
107186
- pos: rawHit.pos,
107187
- pageIndex: rawHit.pageIndex,
107188
- blockId: rawHit.blockId,
107189
- lineIndex: rawHit.lineIndex,
107190
- layoutEpoch: rawHit.layoutEpoch
107191
- },
107192
- prevRawHit: prevRawHit ? {
107193
- pos: prevRawHit.pos,
107194
- pageIndex: prevRawHit.pageIndex,
107195
- blockId: prevRawHit.blockId,
107196
- lineIndex: prevRawHit.lineIndex,
107197
- layoutEpoch: prevRawHit.layoutEpoch
107198
- } : null,
107199
- mappedHead: { pos: mappedHead.pos, fromEpoch: mappedHead.fromEpoch, toEpoch: mappedHead.toEpoch },
107200
- hit: { pos: hit.pos, pageIndex: hit.pageIndex, layoutEpoch: hit.layoutEpoch },
107201
- anchor,
107202
- head,
107203
- selAnchor,
107204
- selHead,
107205
- direction: head >= anchor ? "down" : "up",
107206
- selectionDirection: selHead >= selAnchor ? "down" : "up",
107207
- extensionMode: this.#dragExtensionMode,
107208
- hitSource: pageMounted ? "dom" : "geometry",
107209
- pageMounted
107210
- })}`
107211
- );
107212
- const currentTableHit = this.#hitTestTable(normalized.x, normalized.y);
107213
- const shouldUseCellSel = this.#shouldUseCellSelection(currentTableHit);
107214
- if (shouldUseCellSel && this.#cellAnchor) {
107215
- const headCellPos = currentTableHit ? this.#getCellPosFromTableHit(currentTableHit) : null;
107216
- if (headCellPos !== null) {
107217
- if (this.#cellDragMode !== "active") {
107218
- this.#cellDragMode = "active";
107219
- }
107220
- try {
107221
- const doc22 = this.#editor.state.doc;
107222
- const anchorCellPos = this.#cellAnchor.cellPos;
107223
- const clampedAnchor = Math.max(0, Math.min(anchorCellPos, doc22.content.size));
107224
- const clampedHead = Math.max(0, Math.min(headCellPos, doc22.content.size));
107225
- const cellSelection = CellSelection.create(doc22, clampedAnchor, clampedHead);
107226
- const tr = this.#editor.state.tr.setSelection(cellSelection);
107227
- this.#editor.view?.dispatch(tr);
107228
- this.#scheduleSelectionUpdate();
107229
- } catch (error) {
107230
- console.warn("[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:", error);
107231
- const anchor2 = this.#dragAnchor;
107232
- const head2 = hit.pos;
107233
- const { selAnchor: selAnchor2, selHead: selHead2 } = this.#calculateExtendedSelection(anchor2, head2, this.#dragExtensionMode);
107234
- try {
107235
- const tr = this.#editor.state.tr.setSelection(
107236
- TextSelection$1.create(this.#editor.state.doc, selAnchor2, selHead2)
107237
- );
107238
- this.#editor.view?.dispatch(tr);
107239
- this.#scheduleSelectionUpdate();
107240
- } catch {
107241
- }
107242
- }
107243
- return;
107244
- }
107245
- }
107246
- try {
107247
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(this.#editor.state.doc, selAnchor, selHead));
107248
- this.#editor.view?.dispatch(tr);
107249
- this.#scheduleSelectionUpdate();
107250
- } catch (error) {
107251
- console.warn("[SELECTION] Failed to extend selection during drag:", {
107252
- error,
107253
- anchor,
107254
- head,
107255
- selAnchor,
107256
- selHead,
107257
- mode: this.#dragExtensionMode
107258
- });
107259
- }
107260
- return;
107261
- }
107262
- const sessionMode = this.#headerFooterSession?.session?.mode ?? "body";
107263
- if (sessionMode !== "body") {
107264
- this.#clearHoverRegion();
107265
- return;
107266
- }
107267
- if (this.#documentMode === "viewing") {
107268
- this.#clearHoverRegion();
107269
- return;
107270
- }
107271
- const region = this.#hitTestHeaderFooterRegion(normalized.x, normalized.y);
107272
- if (!region) {
107273
- this.#clearHoverRegion();
107274
- return;
107275
- }
107276
- const currentHover = this.#headerFooterSession?.hoverRegion;
107277
- if (currentHover && currentHover.kind === region.kind && currentHover.pageIndex === region.pageIndex && currentHover.sectionType === region.sectionType) {
107278
- return;
107279
- }
107280
- this.#headerFooterSession?.renderHover(region);
107281
- this.#renderHoverRegion(region);
107282
- };
107283
- #handlePointerLeave = () => {
107284
- this.#clearHoverRegion();
107285
- };
107286
- #handleVisibleHostFocusIn = (event) => {
107287
- if (isInRegisteredSurface(event)) {
107288
- return;
107289
- }
107290
- if (this.#suppressFocusInFromDraggable) {
107291
- this.#suppressFocusInFromDraggable = false;
107292
- return;
107293
- }
107294
- const target = event.target;
107295
- const activeTarget = this.#getActiveDomTarget();
107296
- if (!activeTarget) {
107297
- return;
107298
- }
107299
- const activeNode = activeTarget;
107300
- const containsFn = typeof activeNode.contains === "function" ? activeNode.contains : null;
107301
- if (target && (activeNode === target || containsFn && containsFn.call(activeNode, target))) {
107302
- return;
107303
- }
107304
- try {
107305
- if (activeTarget instanceof HTMLElement && typeof activeTarget.focus === "function") {
107306
- activeTarget.focus?.({
107307
- preventScroll: true
107308
- });
107309
- } else if (typeof activeTarget.focus === "function") {
107310
- activeTarget.focus();
107311
- }
107312
- } catch {
107313
- }
107314
- try {
107315
- this.getActiveEditor().view?.focus();
107316
- } catch {
107317
- }
107318
- };
107319
- #handlePointerUp = (event) => {
107320
- this.#suppressFocusInFromDraggable = false;
107321
- if (!this.#isDragging) return;
107322
- if (typeof this.#viewportHost.hasPointerCapture === "function" && typeof this.#viewportHost.releasePointerCapture === "function" && this.#viewportHost.hasPointerCapture(event.pointerId)) {
107323
- this.#viewportHost.releasePointerCapture(event.pointerId);
107324
- }
107325
- const pendingMarginClick = this.#pendingMarginClick;
107326
- this.#pendingMarginClick = null;
107327
- const dragAnchor = this.#dragAnchor;
107328
- const dragMode = this.#dragExtensionMode;
107329
- const dragUsedFallback = this.#dragUsedPageNotMountedFallback;
107330
- const dragPointer = this.#dragLastPointer;
107331
- this.#isDragging = false;
107332
- if (this.#cellDragMode !== "none") {
107333
- this.#cellDragMode = "none";
107334
- }
107335
- if (!pendingMarginClick || pendingMarginClick.pointerId !== event.pointerId) {
107336
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: false });
107337
- if (dragUsedFallback && dragAnchor != null) {
107338
- const pointer = dragPointer ?? { clientX: event.clientX, clientY: event.clientY };
107339
- this.#finalizeDragSelectionWithDom(pointer, dragAnchor, dragMode);
107340
- }
107341
- this.#scheduleA11ySelectionAnnouncement({ immediate: true });
107342
- this.#dragLastPointer = null;
107343
- this.#dragLastRawHit = null;
107344
- this.#dragUsedPageNotMountedFallback = false;
107345
- return;
107346
- }
107347
- const sessionModeForDrag = this.#headerFooterSession?.session?.mode ?? "body";
107348
- if (sessionModeForDrag !== "body" || this.#isViewLocked()) {
107349
- this.#dragLastPointer = null;
107350
- this.#dragLastRawHit = null;
107351
- this.#dragUsedPageNotMountedFallback = false;
107352
- return;
107353
- }
107354
- const doc2 = this.#editor.state?.doc;
107355
- if (!doc2) {
107356
- this.#dragLastPointer = null;
107357
- this.#dragLastRawHit = null;
107358
- this.#dragUsedPageNotMountedFallback = false;
107359
- return;
107360
- }
107361
- if (pendingMarginClick.kind === "aboveFirstLine") {
107362
- const pos = this.#getFirstTextPosition();
107363
- try {
107364
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(doc2, pos));
107365
- this.#editor.view?.dispatch(tr);
107366
- this.#scheduleSelectionUpdate();
107367
- } catch {
107368
- }
107369
- this.#debugLastHit = { source: "margin", pos: null, layoutEpoch: null, mappedPos: pos };
107370
- this.#updateSelectionDebugHud();
107371
- this.#dragLastPointer = null;
107372
- this.#dragLastRawHit = null;
107373
- this.#dragUsedPageNotMountedFallback = false;
107374
- return;
107375
- }
107376
- if (pendingMarginClick.kind === "right") {
107377
- const mappedEnd2 = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
107378
- pendingMarginClick.pmEnd,
107379
- pendingMarginClick.layoutEpoch,
107380
- 1
107381
- );
107382
- if (!mappedEnd2.ok) {
107383
- debugLog("warn", "right margin mapping failed", mappedEnd2);
107384
- this.#pendingDocChange = true;
107385
- this.#scheduleRerender();
107386
- this.#dragLastPointer = null;
107387
- this.#dragLastRawHit = null;
107388
- this.#dragUsedPageNotMountedFallback = false;
107389
- return;
107390
- }
107391
- const caretPos = Math.max(0, Math.min(mappedEnd2.pos, doc2.content.size));
107392
- try {
107393
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(doc2, caretPos));
107394
- this.#editor.view?.dispatch(tr);
107395
- this.#scheduleSelectionUpdate();
107396
- } catch {
107397
- }
107398
- this.#debugLastHit = {
107399
- source: "margin",
107400
- pos: pendingMarginClick.pmEnd,
107401
- layoutEpoch: pendingMarginClick.layoutEpoch,
107402
- mappedPos: caretPos
107403
- };
107404
- this.#updateSelectionDebugHud();
107405
- this.#dragLastPointer = null;
107406
- this.#dragLastRawHit = null;
107407
- this.#dragUsedPageNotMountedFallback = false;
107408
- return;
107409
- }
107410
- const mappedStart = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
107411
- pendingMarginClick.pmStart,
107412
- pendingMarginClick.layoutEpoch,
107413
- 1
107414
- );
107415
- const mappedEnd = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
107416
- pendingMarginClick.pmEnd,
107417
- pendingMarginClick.layoutEpoch,
107418
- -1
107419
- );
107420
- if (!mappedStart.ok || !mappedEnd.ok) {
107421
- if (!mappedStart.ok) debugLog("warn", "left margin mapping failed (start)", mappedStart);
107422
- if (!mappedEnd.ok) debugLog("warn", "left margin mapping failed (end)", mappedEnd);
107423
- this.#pendingDocChange = true;
107424
- this.#scheduleRerender();
107425
- this.#dragLastPointer = null;
107426
- this.#dragLastRawHit = null;
107427
- this.#dragUsedPageNotMountedFallback = false;
107428
- return;
107429
- }
107430
- const selFrom = Math.max(0, Math.min(Math.min(mappedStart.pos, mappedEnd.pos), doc2.content.size));
107431
- const selTo = Math.max(0, Math.min(Math.max(mappedStart.pos, mappedEnd.pos), doc2.content.size));
107432
- try {
107433
- const tr = this.#editor.state.tr.setSelection(TextSelection$1.create(doc2, selFrom, selTo));
107434
- this.#editor.view?.dispatch(tr);
107435
- this.#scheduleSelectionUpdate();
107436
- } catch {
107437
- }
107438
- this.#debugLastHit = {
107439
- source: "margin",
107440
- pos: pendingMarginClick.pmStart,
107441
- layoutEpoch: pendingMarginClick.layoutEpoch,
107442
- mappedPos: selFrom
107443
- };
107444
- this.#updateSelectionDebugHud();
107445
- this.#dragLastPointer = null;
107446
- this.#dragLastRawHit = null;
107447
- this.#dragUsedPageNotMountedFallback = false;
107448
- };
107449
- #handleDragOver = createExternalFieldAnnotationDragOverHandler({
107450
- getActiveEditor: () => this.getActiveEditor(),
107451
- hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
107452
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
107453
- });
107454
- #handleDrop = createExternalFieldAnnotationDropHandler({
107455
- getActiveEditor: () => this.getActiveEditor(),
107456
- hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
107457
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
107458
- });
107459
- #handleDoubleClick = (event) => {
107460
- if (event.button !== 0) return;
107461
- if (!this.#layoutState.layout) return;
107462
- const rect = this.#viewportHost.getBoundingClientRect();
107463
- const zoom = this.#layoutOptions.zoom ?? 1;
107464
- const scrollLeft = this.#visibleHost.scrollLeft ?? 0;
107465
- const scrollTop = this.#visibleHost.scrollTop ?? 0;
107466
- const x2 = (event.clientX - rect.left + scrollLeft) / zoom;
107467
- const y2 = (event.clientY - rect.top + scrollTop) / zoom;
107468
- const region = this.#hitTestHeaderFooterRegion(x2, y2);
107469
- if (region) {
107470
- event.preventDefault();
107471
- event.stopPropagation();
107472
- const descriptor = this.#resolveDescriptorForRegion(region);
107473
- const hfManager = this.#headerFooterSession?.manager;
107474
- if (!descriptor && hfManager) {
107475
- this.#createDefaultHeaderFooter(region);
107476
- hfManager.refresh();
107477
- }
107478
- this.#activateHeaderFooterRegion(region);
107479
- } else if ((this.#headerFooterSession?.session?.mode ?? "body") !== "body") {
107480
- this.#exitHeaderFooterMode();
107481
- }
107482
- };
107483
- #handleKeyDown = (event) => {
107484
- const sessionModeForKey = this.#headerFooterSession?.session?.mode ?? "body";
107485
- if (event.key === "Escape" && sessionModeForKey !== "body") {
107486
- event.preventDefault();
107487
- this.#exitHeaderFooterMode();
107488
- return;
107489
- }
107490
- if (event.ctrlKey && event.altKey && !event.shiftKey) {
107491
- if (event.code === "KeyH") {
107492
- event.preventDefault();
107493
- this.#focusHeaderFooterShortcut("header");
107494
- } else if (event.code === "KeyF") {
107495
- event.preventDefault();
107496
- this.#focusHeaderFooterShortcut("footer");
107497
- }
107498
- }
107499
- };
107500
- #focusHeaderFooterShortcut(kind) {
107501
- const pageIndex = this.#getCurrentPageIndex();
107502
- const region = this.#findRegionForPage(kind, pageIndex);
107503
- if (!region) {
107504
- this.#emitHeaderFooterEditBlocked("missingRegion");
107505
- return;
107506
- }
107507
- this.#activateHeaderFooterRegion(region);
107508
- }
107509
107388
  #scheduleRerender() {
107510
107389
  if (this.#renderScheduled) {
107511
107390
  return;
@@ -107970,7 +107849,7 @@ ${o}
107970
107849
  return;
107971
107850
  }
107972
107851
  this.#syncSelectedFieldAnnotationClass(selection);
107973
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: this.#isDragging });
107852
+ this.#updateSelectionVirtualizationPins({ includeDragBuffer: this.#editorInputManager?.isDragging ?? false });
107974
107853
  if (selection instanceof CellSelection) {
107975
107854
  try {
107976
107855
  this.#localSelectionLayer.innerHTML = "";
@@ -108718,7 +108597,7 @@ ${o}
108718
108597
  {
108719
108598
  ariaLiveRegion: this.#ariaLiveRegion,
108720
108599
  sessionMode,
108721
- isDragging: this.#isDragging,
108600
+ isDragging: this.#editorInputManager?.isDragging ?? false,
108722
108601
  visibleHost: this.#visibleHost,
108723
108602
  currentTimeout: this.#a11ySelectionAnnounceTimeout,
108724
108603
  announceNow: () => {
@@ -108846,9 +108725,9 @@ ${o}
108846
108725
  } : null,
108847
108726
  docSize,
108848
108727
  includeDragBuffer: Boolean(options?.includeDragBuffer),
108849
- isDragging: this.#isDragging,
108850
- dragAnchorPageIndex: this.#dragAnchorPageIndex,
108851
- dragLastHitPageIndex: this.#dragLastRawHit ? this.#dragLastRawHit.pageIndex : null,
108728
+ isDragging: this.#editorInputManager?.isDragging ?? false,
108729
+ dragAnchorPageIndex: this.#editorInputManager?.dragAnchorPageIndex ?? null,
108730
+ dragLastHitPageIndex: this.#editorInputManager?.dragLastHitPageIndex ?? null,
108852
108731
  extraPages: options?.extraPages
108853
108732
  });
108854
108733
  painter.setVirtualizationPins(pins);
@@ -108862,9 +108741,10 @@ ${o}
108862
108741
  }
108863
108742
  const normalized = this.#normalizeClientPoint(pointer.clientX, pointer.clientY);
108864
108743
  if (!normalized) return;
108744
+ const dragLastRawHit = this.#editorInputManager?.dragLastRawHit;
108865
108745
  this.#updateSelectionVirtualizationPins({
108866
108746
  includeDragBuffer: false,
108867
- extraPages: this.#dragLastRawHit ? [this.#dragLastRawHit.pageIndex] : void 0
108747
+ extraPages: dragLastRawHit ? [dragLastRawHit.pageIndex] : void 0
108868
108748
  });
108869
108749
  const refined = clickToPosition(
108870
108750
  layout,
@@ -108881,7 +108761,7 @@ ${o}
108881
108761
  debugLog("warn", "Drag finalize: endpoint page still not mounted", { pageIndex: refined.pageIndex });
108882
108762
  return;
108883
108763
  }
108884
- const prior = this.#dragLastRawHit;
108764
+ const prior = dragLastRawHit;
108885
108765
  if (prior && (prior.pos !== refined.pos || prior.pageIndex !== refined.pageIndex)) {
108886
108766
  debugLog("info", "Drag finalize refined hit", {
108887
108767
  fromPos: prior.pos,
@@ -109084,7 +108964,7 @@ ${o}
109084
108964
  localSelectionLayer,
109085
108965
  blocks: this.#layoutState.blocks,
109086
108966
  measures: this.#layoutState.measures,
109087
- cellAnchorTableBlockId: this.#cellAnchor?.tableBlockId ?? null,
108967
+ cellAnchorTableBlockId: this.#editorInputManager?.cellAnchor?.tableBlockId ?? null,
109088
108968
  convertPageLocalToOverlayCoords: (pageIndex, x2, y2) => this.#convertPageLocalToOverlayCoords(pageIndex, x2, y2)
109089
108969
  });
109090
108970
  }
@@ -153615,7 +153495,7 @@ ${reason}`);
153615
153495
  this.config.colors = shuffleArray(this.config.colors);
153616
153496
  this.userColorMap = /* @__PURE__ */ new Map();
153617
153497
  this.colorIndex = 0;
153618
- this.version = "1.8.0-next.3";
153498
+ this.version = "1.8.0-next.4";
153619
153499
  this.#log("🦋 [superdoc] Using SuperDoc version:", this.version);
153620
153500
  this.superdocId = config2.superdocId || v4();
153621
153501
  this.colors = this.config.colors;