@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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const jszip = require("./jszip-C8_CqJxM.cjs");
3
3
  const helpers$1 = require("./helpers-nOdwpmwb.cjs");
4
- const superEditor_converter = require("./SuperConverter-0rmmECRS.cjs");
4
+ const superEditor_converter = require("./SuperConverter-z513kcRv.cjs");
5
5
  const vue = require("./vue-De9wkgLl.cjs");
6
6
  require("./jszip.min-BPh2MMAa.cjs");
7
7
  const eventemitter3 = require("./eventemitter3-BQuRcMPI.cjs");
@@ -16102,7 +16102,7 @@ const canUseDOM = () => {
16102
16102
  return false;
16103
16103
  }
16104
16104
  };
16105
- const summaryVersion = "1.8.0-next.3";
16105
+ const summaryVersion = "1.8.0-next.4";
16106
16106
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
16107
16107
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
16108
16108
  function mapAttributes(attrs) {
@@ -18759,7 +18759,7 @@ class Editor extends EventEmitter {
18759
18759
  * Process collaboration migrations
18760
18760
  */
18761
18761
  processCollaborationMigrations() {
18762
- console.debug("[checkVersionMigrations] Current editor version", "1.8.0-next.3");
18762
+ console.debug("[checkVersionMigrations] Current editor version", "1.8.0-next.4");
18763
18763
  if (!this.options.ydoc) return;
18764
18764
  const metaMap = this.options.ydoc.getMap("meta");
18765
18765
  let docVersion = metaMap.get("version");
@@ -28801,24 +28801,6 @@ class DomPainter {
28801
28801
  }
28802
28802
  elem.setAttribute("role", "link");
28803
28803
  elem.setAttribute("tabindex", "0");
28804
- elem.addEventListener("click", (event) => {
28805
- event.preventDefault();
28806
- event.stopPropagation();
28807
- const linkClickEvent = new CustomEvent("superdoc-link-click", {
28808
- bubbles: true,
28809
- composed: true,
28810
- detail: {
28811
- href: linkData.href,
28812
- target: linkData.target,
28813
- rel: linkData.rel,
28814
- tooltip: linkData.tooltip,
28815
- element: elem,
28816
- clientX: event.clientX,
28817
- clientY: event.clientY
28818
- }
28819
- });
28820
- elem.dispatchEvent(linkClickEvent);
28821
- });
28822
28804
  }
28823
28805
  /**
28824
28806
  * Render a single run as an HTML element (span or anchor).
@@ -40156,255 +40138,6 @@ var Priority = /* @__PURE__ */ ((Priority2) => {
40156
40138
  /** P3: Heavy debounce for full document layout */
40157
40139
  [Priority.P3]: 150
40158
40140
  });
40159
- const DEFAULT_MIME_TYPE$1 = "application/x-field-annotation";
40160
- const LEGACY_MIME_TYPE = "fieldAnnotation";
40161
- function parseIntSafe$1(value) {
40162
- if (!value) return void 0;
40163
- const parsed = parseInt(value, 10);
40164
- return Number.isFinite(parsed) ? parsed : void 0;
40165
- }
40166
- function extractFieldAnnotationData(element) {
40167
- const dataset = element.dataset;
40168
- const attributes = {};
40169
- for (const key2 in dataset) {
40170
- const value = dataset[key2];
40171
- if (value !== void 0) {
40172
- attributes[key2] = value;
40173
- }
40174
- }
40175
- return {
40176
- fieldId: dataset.fieldId,
40177
- fieldType: dataset.fieldType,
40178
- variant: dataset.variant ?? dataset.type,
40179
- displayLabel: dataset.displayLabel,
40180
- pmStart: parseIntSafe$1(dataset.pmStart),
40181
- pmEnd: parseIntSafe$1(dataset.pmEnd),
40182
- attributes
40183
- };
40184
- }
40185
- class DragHandler {
40186
- /**
40187
- * Creates a new DragHandler instance.
40188
- *
40189
- * @param container - The DOM container element (typically .superdoc-layout)
40190
- * @param config - Configuration options and callbacks
40191
- */
40192
- constructor(container, config = {}) {
40193
- this.container = container;
40194
- this.config = config;
40195
- this.mimeType = config.mimeType ?? DEFAULT_MIME_TYPE$1;
40196
- this.boundHandlers = {
40197
- dragstart: this.handleDragStart.bind(this),
40198
- dragover: this.handleDragOver.bind(this),
40199
- drop: this.handleDrop.bind(this),
40200
- dragend: this.handleDragEnd.bind(this),
40201
- dragleave: this.handleDragLeave.bind(this)
40202
- };
40203
- this.windowDragoverHandler = this.handleWindowDragOver.bind(this);
40204
- this.windowDropHandler = this.handleWindowDrop.bind(this);
40205
- this.attachListeners();
40206
- }
40207
- /**
40208
- * Attaches event listeners to the container and window.
40209
- */
40210
- attachListeners() {
40211
- this.container.addEventListener("dragstart", this.boundHandlers.dragstart);
40212
- this.container.addEventListener("dragover", this.boundHandlers.dragover);
40213
- this.container.addEventListener("drop", this.boundHandlers.drop);
40214
- this.container.addEventListener("dragend", this.boundHandlers.dragend);
40215
- this.container.addEventListener("dragleave", this.boundHandlers.dragleave);
40216
- window.addEventListener("dragover", this.windowDragoverHandler, false);
40217
- window.addEventListener("drop", this.windowDropHandler, false);
40218
- }
40219
- /**
40220
- * Removes event listeners from the container and window.
40221
- */
40222
- removeListeners() {
40223
- this.container.removeEventListener("dragstart", this.boundHandlers.dragstart);
40224
- this.container.removeEventListener("dragover", this.boundHandlers.dragover);
40225
- this.container.removeEventListener("drop", this.boundHandlers.drop);
40226
- this.container.removeEventListener("dragend", this.boundHandlers.dragend);
40227
- this.container.removeEventListener("dragleave", this.boundHandlers.dragleave);
40228
- window.removeEventListener("dragover", this.windowDragoverHandler, false);
40229
- window.removeEventListener("drop", this.windowDropHandler, false);
40230
- }
40231
- /**
40232
- * Handles dragover at window level to allow drops on overlay elements.
40233
- * This ensures preventDefault is called even when dragging over selection
40234
- * highlights or other UI elements that sit on top of the layout content.
40235
- */
40236
- handleWindowDragOver(event) {
40237
- if (this.hasFieldAnnotationData(event)) {
40238
- event.preventDefault();
40239
- if (event.dataTransfer) {
40240
- event.dataTransfer.dropEffect = "move";
40241
- }
40242
- const target = event.target;
40243
- if (!this.container.contains(target)) {
40244
- this.config.onDragOver?.({
40245
- event,
40246
- clientX: event.clientX,
40247
- clientY: event.clientY,
40248
- hasFieldAnnotation: true
40249
- });
40250
- }
40251
- }
40252
- }
40253
- /**
40254
- * Handles drop at window level to catch drops on overlay elements.
40255
- * If the drop target is outside the container, we process it here.
40256
- */
40257
- handleWindowDrop(event) {
40258
- if (this.hasFieldAnnotationData(event)) {
40259
- const target = event.target;
40260
- if (!this.container.contains(target)) {
40261
- this.handleDrop(event);
40262
- }
40263
- }
40264
- }
40265
- /**
40266
- * Handles the dragstart event.
40267
- * Sets up dataTransfer with field annotation data and drag image.
40268
- */
40269
- handleDragStart(event) {
40270
- const target = event.target;
40271
- if (!target?.dataset?.draggable || target.dataset.draggable !== "true") {
40272
- return;
40273
- }
40274
- const data = extractFieldAnnotationData(target);
40275
- if (event.dataTransfer) {
40276
- const jsonData = JSON.stringify({
40277
- attributes: data.attributes,
40278
- sourceField: data
40279
- });
40280
- event.dataTransfer.setData(this.mimeType, jsonData);
40281
- event.dataTransfer.setData(LEGACY_MIME_TYPE, jsonData);
40282
- event.dataTransfer.setData("text/plain", data.displayLabel ?? "Field Annotation");
40283
- event.dataTransfer.setDragImage(target, 0, 0);
40284
- event.dataTransfer.effectAllowed = "move";
40285
- }
40286
- this.config.onDragStart?.({
40287
- event,
40288
- element: target,
40289
- data
40290
- });
40291
- }
40292
- /**
40293
- * Handles the dragover event.
40294
- * Provides visual feedback and determines if drop is allowed.
40295
- */
40296
- handleDragOver(event) {
40297
- const hasFieldAnnotation = this.hasFieldAnnotationData(event);
40298
- if (hasFieldAnnotation) {
40299
- event.preventDefault();
40300
- if (event.dataTransfer) {
40301
- event.dataTransfer.dropEffect = "move";
40302
- }
40303
- this.container.classList.add("drag-over");
40304
- }
40305
- this.config.onDragOver?.({
40306
- event,
40307
- clientX: event.clientX,
40308
- clientY: event.clientY,
40309
- hasFieldAnnotation
40310
- });
40311
- }
40312
- /**
40313
- * Handles the dragleave event.
40314
- * Removes visual feedback when drag leaves the container.
40315
- */
40316
- handleDragLeave(event) {
40317
- const relatedTarget = event.relatedTarget;
40318
- if (!relatedTarget || !this.container.contains(relatedTarget)) {
40319
- this.container.classList.remove("drag-over");
40320
- }
40321
- }
40322
- /**
40323
- * Handles the drop event.
40324
- * Maps drop coordinates to ProseMirror position and emits drop event.
40325
- */
40326
- handleDrop(event) {
40327
- this.container.classList.remove("drag-over");
40328
- if (!this.hasFieldAnnotationData(event)) {
40329
- return;
40330
- }
40331
- event.preventDefault();
40332
- const data = this.extractDragData(event);
40333
- if (!data) {
40334
- return;
40335
- }
40336
- const pmPosition = clickToPositionDom(this.container, event.clientX, event.clientY);
40337
- this.config.onDrop?.({
40338
- event,
40339
- data,
40340
- pmPosition,
40341
- clientX: event.clientX,
40342
- clientY: event.clientY
40343
- });
40344
- }
40345
- /**
40346
- * Handles the dragend event.
40347
- * Cleans up drag state.
40348
- */
40349
- handleDragEnd(event) {
40350
- this.container.classList.remove("drag-over");
40351
- this.config.onDragEnd?.(event);
40352
- }
40353
- /**
40354
- * Checks if a drag event contains field annotation data.
40355
- */
40356
- hasFieldAnnotationData(event) {
40357
- if (!event.dataTransfer) {
40358
- return false;
40359
- }
40360
- const types = event.dataTransfer.types;
40361
- return types.includes(this.mimeType) || types.includes(LEGACY_MIME_TYPE);
40362
- }
40363
- /**
40364
- * Extracts field annotation data from a drag event's dataTransfer.
40365
- */
40366
- extractDragData(event) {
40367
- if (!event.dataTransfer) {
40368
- return null;
40369
- }
40370
- let jsonData = event.dataTransfer.getData(this.mimeType);
40371
- if (!jsonData) {
40372
- jsonData = event.dataTransfer.getData(LEGACY_MIME_TYPE);
40373
- }
40374
- if (!jsonData) {
40375
- return null;
40376
- }
40377
- try {
40378
- const parsed = JSON.parse(jsonData);
40379
- return parsed.sourceField ?? parsed.attributes ?? parsed;
40380
- } catch {
40381
- return null;
40382
- }
40383
- }
40384
- /**
40385
- * Updates the configuration options.
40386
- *
40387
- * @param config - New configuration options to merge
40388
- */
40389
- updateConfig(config) {
40390
- this.config = { ...this.config, ...config };
40391
- if (config.mimeType) {
40392
- this.mimeType = config.mimeType;
40393
- }
40394
- }
40395
- /**
40396
- * Destroys the drag handler and removes all event listeners.
40397
- * Call this when the layout engine is unmounted or the container is removed.
40398
- */
40399
- destroy() {
40400
- this.removeListeners();
40401
- this.container.classList.remove("drag-over");
40402
- }
40403
- }
40404
- function createDragHandler(container, config = {}) {
40405
- const handler = new DragHandler(container, config);
40406
- return () => handler.destroy();
40407
- }
40408
40141
  const isAtomicFragment = (fragment) => {
40409
40142
  return fragment.kind === "drawing" || fragment.kind === "image";
40410
40143
  };
@@ -41968,230 +41701,1432 @@ class RemoteCursorManager {
41968
41701
  this.#isSetup = false;
41969
41702
  }
41970
41703
  }
41971
- const createDefaultScheduler = () => {
41972
- if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
41973
- return {
41974
- requestAnimationFrame: (cb) => window.requestAnimationFrame(cb),
41975
- cancelAnimationFrame: (handle) => window.cancelAnimationFrame(handle)
41976
- };
41704
+ const WORD_CHARACTER_REGEX = /[\p{L}\p{N}'\u2018\u2019_~-]/u;
41705
+ function isWordCharacter(char) {
41706
+ if (!char) {
41707
+ return false;
41977
41708
  }
41978
- const anyGlobal = globalThis;
41979
- if (typeof anyGlobal.requestAnimationFrame === "function" && typeof anyGlobal.cancelAnimationFrame === "function") {
41980
- return {
41981
- requestAnimationFrame: (cb) => anyGlobal.requestAnimationFrame(cb),
41982
- cancelAnimationFrame: (handle) => anyGlobal.cancelAnimationFrame(handle)
41983
- };
41709
+ return WORD_CHARACTER_REGEX.test(char);
41710
+ }
41711
+ function calculateExtendedSelection(blocks, anchor, head, mode) {
41712
+ if (mode === "word") {
41713
+ const anchorBounds = findWordBoundaries(blocks, anchor);
41714
+ const headBounds = findWordBoundaries(blocks, head);
41715
+ if (anchorBounds && headBounds) {
41716
+ if (head >= anchor) {
41717
+ return { selAnchor: anchorBounds.from, selHead: headBounds.to };
41718
+ } else {
41719
+ return { selAnchor: anchorBounds.to, selHead: headBounds.from };
41720
+ }
41721
+ }
41722
+ } else if (mode === "para") {
41723
+ const anchorBounds = findParagraphBoundaries(blocks, anchor);
41724
+ const headBounds = findParagraphBoundaries(blocks, head);
41725
+ if (anchorBounds && headBounds) {
41726
+ if (head >= anchor) {
41727
+ return { selAnchor: anchorBounds.from, selHead: headBounds.to };
41728
+ } else {
41729
+ return { selAnchor: anchorBounds.to, selHead: headBounds.from };
41730
+ }
41731
+ }
41984
41732
  }
41733
+ return { selAnchor: anchor, selHead: head };
41734
+ }
41735
+ function registerPointerClick(event, previous, options) {
41736
+ const time2 = event.timeStamp ?? performance.now();
41737
+ const timeDelta = time2 - previous.lastClickTime;
41738
+ const withinTime = timeDelta <= options.timeThresholdMs;
41739
+ const distanceX = Math.abs(event.clientX - previous.lastClickPosition.x);
41740
+ const distanceY = Math.abs(event.clientY - previous.lastClickPosition.y);
41741
+ const withinDistance = distanceX <= options.distanceThresholdPx && distanceY <= options.distanceThresholdPx;
41742
+ const clickCount = withinTime && withinDistance ? Math.min(previous.clickCount + 1, options.maxClickCount) : 1;
41985
41743
  return {
41986
- requestAnimationFrame: (cb) => {
41987
- const handle = anyGlobal.setTimeout?.(() => cb(Date.now()), 0);
41988
- return handle;
41989
- },
41990
- cancelAnimationFrame: (handle) => {
41991
- anyGlobal.clearTimeout?.(handle);
41992
- }
41744
+ clickCount,
41745
+ lastClickTime: time2,
41746
+ lastClickPosition: { x: event.clientX, y: event.clientY }
41993
41747
  };
41994
- };
41995
- class SelectionSyncCoordinator extends EventEmitter {
41996
- #docEpoch = 0;
41997
- #layoutEpoch = 0;
41998
- #layoutUpdating = false;
41999
- #pending = false;
42000
- #scheduled = false;
42001
- #rafHandle = null;
42002
- #scheduler;
42003
- /**
42004
- * Creates a new SelectionSyncCoordinator.
42005
- *
42006
- * @param options - Configuration options
42007
- * @param options.scheduler - Custom scheduler for animation frames (useful for testing), defaults to platform scheduler
42008
- */
42009
- constructor(options) {
42010
- super();
42011
- this.#scheduler = options?.scheduler ?? createDefaultScheduler();
41748
+ }
41749
+ function getFirstTextPosition(doc2) {
41750
+ if (!doc2 || !doc2.content) {
41751
+ return 1;
42012
41752
  }
42013
- /**
42014
- * Gets the current document epoch.
42015
- *
42016
- * @returns The document epoch (increments on each document-changing transaction)
42017
- */
42018
- getDocEpoch() {
42019
- return this.#docEpoch;
41753
+ let validPos = 1;
41754
+ doc2.nodesBetween(0, doc2.content.size, (node, pos) => {
41755
+ if (node.isTextblock) {
41756
+ validPos = pos + 1;
41757
+ return false;
41758
+ }
41759
+ return true;
41760
+ });
41761
+ return validPos;
41762
+ }
41763
+ function computeWordSelectionRangeAt(state, pos) {
41764
+ if (!state?.doc) {
41765
+ return null;
42020
41766
  }
42021
- /**
42022
- * Gets the current layout epoch.
42023
- *
42024
- * @returns The epoch of the document version currently painted in the DOM
42025
- */
42026
- getLayoutEpoch() {
42027
- return this.#layoutEpoch;
41767
+ if (pos < 0 || pos > state.doc.content.size) {
41768
+ return null;
42028
41769
  }
42029
- /**
42030
- * Checks if a layout update is currently in progress.
42031
- *
42032
- * @returns True if between onLayoutStart() and onLayoutComplete(), false otherwise
42033
- */
42034
- isLayoutUpdating() {
42035
- return this.#layoutUpdating;
41770
+ const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
41771
+ if (!textblockPos) {
41772
+ return null;
42036
41773
  }
42037
- /**
42038
- * Updates the document epoch and triggers conditional rendering.
42039
- *
42040
- * @param epoch - The new document epoch (must be finite and non-negative)
42041
- *
42042
- * @remarks
42043
- * When the document epoch changes:
42044
- * 1. Any scheduled render is cancelled (layout will be out of sync)
42045
- * 2. If layout has already caught up, rendering is rescheduled
42046
- *
42047
- * Calling with the same epoch as the current value is a no-op.
42048
- * Invalid epoch values are silently ignored.
42049
- */
42050
- setDocEpoch(epoch) {
42051
- if (!Number.isFinite(epoch) || epoch < 0) return;
42052
- if (epoch === this.#docEpoch) return;
42053
- this.#docEpoch = epoch;
42054
- this.#cancelScheduledRender();
42055
- this.#maybeSchedule();
41774
+ const parentStart = textblockPos.start();
41775
+ const parentEnd = textblockPos.end();
41776
+ const sampleEnd = Math.min(pos + 1, parentEnd);
41777
+ const charAtPos = state.doc.textBetween(pos, sampleEnd, "\0", "\0");
41778
+ if (!isWordCharacter(charAtPos)) {
41779
+ return null;
42056
41780
  }
42057
- /**
42058
- * Notifies the coordinator that layout computation has started.
42059
- *
42060
- * @remarks
42061
- * Marks the layout as updating and cancels any scheduled renders, since the DOM
42062
- * is about to change and current position data will be stale.
42063
- *
42064
- * Safe to call multiple times (e.g., if layouts overlap) - subsequent calls are ignored
42065
- * until onLayoutComplete() is called.
42066
- */
42067
- onLayoutStart() {
42068
- if (this.#layoutUpdating) return;
42069
- this.#layoutUpdating = true;
42070
- this.#cancelScheduledRender();
41781
+ let startPos = pos;
41782
+ while (startPos > parentStart) {
41783
+ const prevChar = state.doc.textBetween(startPos - 1, startPos, "\0", "\0");
41784
+ if (!isWordCharacter(prevChar)) {
41785
+ break;
41786
+ }
41787
+ startPos -= 1;
42071
41788
  }
42072
- /**
42073
- * Notifies the coordinator that layout painting has completed.
42074
- *
42075
- * @param layoutEpoch - The document epoch that was just painted to the DOM
42076
- *
42077
- * @remarks
42078
- * Marks the layout as no longer updating, records the new layout epoch, and attempts
42079
- * to schedule rendering if conditions are now safe.
42080
- *
42081
- * If the layoutEpoch is invalid (not a finite non-negative number), it is ignored and
42082
- * the previous layoutEpoch value is retained.
42083
- *
42084
- * This method is the primary trigger for selection rendering - if there's a pending
42085
- * render request and layoutEpoch >= docEpoch, a render event will be scheduled.
42086
- */
42087
- onLayoutComplete(layoutEpoch) {
42088
- this.#layoutUpdating = false;
42089
- if (Number.isFinite(layoutEpoch) && layoutEpoch >= 0) {
42090
- this.#layoutEpoch = layoutEpoch;
41789
+ let endPos = pos;
41790
+ while (endPos < parentEnd) {
41791
+ const nextChar = state.doc.textBetween(endPos, endPos + 1, "\0", "\0");
41792
+ if (!isWordCharacter(nextChar)) {
41793
+ break;
42091
41794
  }
42092
- this.#maybeSchedule();
41795
+ endPos += 1;
42093
41796
  }
42094
- /**
42095
- * Notifies the coordinator that layout was aborted without completing.
42096
- *
42097
- * @remarks
42098
- * Marks the layout as no longer updating (without updating layoutEpoch) and attempts
42099
- * to schedule rendering if conditions are safe.
42100
- *
42101
- * Use this when layout computation is cancelled or fails partway through.
42102
- */
42103
- onLayoutAbort() {
42104
- this.#layoutUpdating = false;
42105
- this.#maybeSchedule();
41797
+ if (startPos === endPos) {
41798
+ return null;
42106
41799
  }
42107
- /**
42108
- * Requests that selection rendering occur when conditions become safe.
42109
- *
42110
- * @param options - Rendering options
42111
- * @param options.immediate - If true, attempts to render immediately (synchronously) if safe, defaults to false
42112
- *
42113
- * @remarks
42114
- * Marks a render as pending and schedules it to occur on the next animation frame if
42115
- * conditions are safe (layout not updating, layoutEpoch >= docEpoch).
42116
- *
42117
- * If options.immediate is true, also attempts a synchronous render before scheduling.
42118
- * Use immediate rendering sparingly, as it can cause multiple renders per frame.
42119
- *
42120
- * Multiple calls are coalesced - only one render will occur per animation frame.
42121
- */
42122
- requestRender(options) {
42123
- this.#pending = true;
42124
- if (options?.immediate) {
42125
- this.flushNow();
41800
+ return { from: startPos, to: endPos };
41801
+ }
41802
+ function computeParagraphSelectionRangeAt(state, pos) {
41803
+ if (!state?.doc) {
41804
+ return null;
41805
+ }
41806
+ const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
41807
+ if (!textblockPos) {
41808
+ return null;
41809
+ }
41810
+ return { from: textblockPos.start(), to: textblockPos.end() };
41811
+ }
41812
+ function findNearestTextblockResolvedPos(doc2, pos) {
41813
+ const $pos = doc2.resolve(pos);
41814
+ let textblockPos = $pos;
41815
+ while (textblockPos.depth > 0) {
41816
+ if (textblockPos.parent?.isTextblock) {
41817
+ break;
42126
41818
  }
42127
- this.#maybeSchedule();
41819
+ if (!textblockPos.parent || textblockPos.depth === 0) {
41820
+ break;
41821
+ }
41822
+ const beforePos = textblockPos.before();
41823
+ if (beforePos < 0 || beforePos > doc2.content.size) {
41824
+ return null;
41825
+ }
41826
+ textblockPos = doc2.resolve(beforePos);
42128
41827
  }
42129
- /**
42130
- * Attempts to render selection immediately (synchronously) if conditions are safe.
42131
- *
42132
- * @remarks
42133
- * If there's a pending render request and conditions are safe (layout not updating,
42134
- * layoutEpoch >= docEpoch), this method:
42135
- * 1. Cancels any scheduled asynchronous render
42136
- * 2. Clears the pending flag
42137
- * 3. Emits the 'render' event synchronously
42138
- *
42139
- * If no render is pending or conditions are not safe, this is a no-op.
42140
- *
42141
- * Use this for immediate selection updates in response to user actions (e.g., click
42142
- * handlers) where waiting for the next animation frame would cause noticeable lag.
42143
- */
42144
- flushNow() {
42145
- if (!this.#pending) return;
42146
- if (!this.#isSafeToRender()) return;
42147
- this.#cancelScheduledRender();
42148
- this.#pending = false;
42149
- this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
41828
+ if (!textblockPos.parent?.isTextblock) {
41829
+ return null;
42150
41830
  }
42151
- /**
42152
- * Permanently tears down the coordinator, cancelling pending renders and removing all listeners.
42153
- *
42154
- * @remarks
42155
- * After calling destroy(), this instance should not be used again. All scheduled renders
42156
- * are cancelled and all event listeners are removed.
42157
- *
42158
- * Safe to call multiple times.
42159
- */
42160
- destroy() {
42161
- this.#cancelScheduledRender();
42162
- this.removeAllListeners();
41831
+ return textblockPos;
41832
+ }
41833
+ function getCellPosFromTableHit(tableHit, doc2, blocks) {
41834
+ if (!tableHit || !tableHit.block || typeof tableHit.block.id !== "string") {
41835
+ console.warn("[getCellPosFromTableHit] Invalid tableHit input:", tableHit);
41836
+ return null;
42163
41837
  }
42164
- #isSafeToRender() {
42165
- return !this.#layoutUpdating && this.#layoutEpoch >= this.#docEpoch;
41838
+ if (typeof tableHit.cellRowIndex !== "number" || typeof tableHit.cellColIndex !== "number" || tableHit.cellRowIndex < 0 || tableHit.cellColIndex < 0) {
41839
+ console.warn("[getCellPosFromTableHit] Invalid cell indices:", {
41840
+ row: tableHit.cellRowIndex,
41841
+ col: tableHit.cellColIndex
41842
+ });
41843
+ return null;
42166
41844
  }
42167
- #maybeSchedule() {
42168
- if (!this.#pending) return;
42169
- if (!this.#isSafeToRender()) return;
42170
- if (this.#scheduled) return;
42171
- this.#scheduled = true;
42172
- this.#rafHandle = this.#scheduler.requestAnimationFrame(() => {
42173
- this.#scheduled = false;
42174
- this.#rafHandle = null;
42175
- if (!this.#pending) return;
42176
- if (!this.#isSafeToRender()) return;
42177
- this.#pending = false;
42178
- this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
41845
+ if (!doc2) return null;
41846
+ const tableBlocks = blocks.filter((b2) => b2.kind === "table");
41847
+ const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
41848
+ if (targetTableIndex === -1) return null;
41849
+ let tablePos = null;
41850
+ let currentTableIndex = 0;
41851
+ try {
41852
+ doc2.descendants((node, pos) => {
41853
+ if (node.type.name === "table") {
41854
+ if (currentTableIndex === targetTableIndex) {
41855
+ tablePos = pos;
41856
+ return false;
41857
+ }
41858
+ currentTableIndex++;
41859
+ }
41860
+ return true;
42179
41861
  });
41862
+ } catch (error) {
41863
+ console.error("[getCellPosFromTableHit] Error during document traversal:", error);
41864
+ return null;
42180
41865
  }
42181
- #cancelScheduledRender() {
42182
- if (this.#rafHandle != null) {
42183
- try {
42184
- this.#scheduler.cancelAnimationFrame(this.#rafHandle);
42185
- } finally {
42186
- this.#rafHandle = null;
41866
+ if (tablePos === null) return null;
41867
+ const tableNode = doc2.nodeAt(tablePos);
41868
+ if (!tableNode || tableNode.type.name !== "table") return null;
41869
+ const targetRowIndex = tableHit.cellRowIndex;
41870
+ const targetColIndex = tableHit.cellColIndex;
41871
+ if (targetRowIndex >= tableNode.childCount) {
41872
+ console.warn("[getCellPosFromTableHit] Target row index out of bounds:", {
41873
+ targetRowIndex,
41874
+ tableChildCount: tableNode.childCount
41875
+ });
41876
+ return null;
41877
+ }
41878
+ let currentPos = tablePos + 1;
41879
+ for (let r2 = 0; r2 < tableNode.childCount && r2 <= targetRowIndex; r2++) {
41880
+ const row = tableNode.child(r2);
41881
+ if (r2 === targetRowIndex) {
41882
+ currentPos += 1;
41883
+ let logicalCol = 0;
41884
+ for (let cellIndex = 0; cellIndex < row.childCount; cellIndex++) {
41885
+ const cell = row.child(cellIndex);
41886
+ const rawColspan = cell.attrs?.colspan;
41887
+ const colspan = typeof rawColspan === "number" && Number.isFinite(rawColspan) && rawColspan > 0 ? rawColspan : 1;
41888
+ if (targetColIndex >= logicalCol && targetColIndex < logicalCol + colspan) {
41889
+ return currentPos;
41890
+ }
41891
+ currentPos += cell.nodeSize;
41892
+ logicalCol += colspan;
42187
41893
  }
41894
+ console.warn("[getCellPosFromTableHit] Target column not found in row:", {
41895
+ targetColIndex,
41896
+ logicalColReached: logicalCol,
41897
+ rowCellCount: row.childCount
41898
+ });
41899
+ return null;
41900
+ } else {
41901
+ currentPos += row.nodeSize;
42188
41902
  }
42189
- this.#scheduled = false;
42190
41903
  }
41904
+ return null;
42191
41905
  }
42192
- const uiSurfaces = /* @__PURE__ */ new WeakSet();
42193
- function isInRegisteredSurface(event) {
42194
- const path = typeof event.composedPath === "function" ? event.composedPath() : [];
41906
+ function getTablePosFromHit(tableHit, doc2, blocks) {
41907
+ if (!doc2) return null;
41908
+ const tableBlocks = blocks.filter((b2) => b2.kind === "table");
41909
+ const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
41910
+ if (targetTableIndex === -1) return null;
41911
+ let tablePos = null;
41912
+ let currentTableIndex = 0;
41913
+ doc2.descendants((node, pos) => {
41914
+ if (node.type.name === "table") {
41915
+ if (currentTableIndex === targetTableIndex) {
41916
+ tablePos = pos;
41917
+ return false;
41918
+ }
41919
+ currentTableIndex++;
41920
+ }
41921
+ return true;
41922
+ });
41923
+ return tablePos;
41924
+ }
41925
+ function shouldUseCellSelection(currentTableHit, cellAnchor, cellDragMode) {
41926
+ if (!cellAnchor) return false;
41927
+ if (!currentTableHit) return cellDragMode === "active";
41928
+ if (currentTableHit.block.id !== cellAnchor.tableBlockId) {
41929
+ return cellDragMode === "active";
41930
+ }
41931
+ const sameCell = currentTableHit.cellRowIndex === cellAnchor.cellRowIndex && currentTableHit.cellColIndex === cellAnchor.cellColIndex;
41932
+ if (!sameCell) {
41933
+ return true;
41934
+ }
41935
+ return cellDragMode === "active";
41936
+ }
41937
+ function hitTestTable(layout, blocks, measures, normalizedX, normalizedY, configuredPageHeight, pageGapFallback, geometryHelper) {
41938
+ if (!layout) {
41939
+ return null;
41940
+ }
41941
+ let pageY = 0;
41942
+ let pageHit = null;
41943
+ if (geometryHelper) {
41944
+ const idx = geometryHelper.getPageIndexAtY(normalizedY) ?? geometryHelper.getNearestPageIndex(normalizedY);
41945
+ if (idx != null && layout.pages[idx]) {
41946
+ pageHit = { pageIndex: idx, page: layout.pages[idx] };
41947
+ pageY = geometryHelper.getPageTop(idx);
41948
+ }
41949
+ }
41950
+ if (!pageHit) {
41951
+ const gap = layout.pageGap ?? pageGapFallback;
41952
+ for (let i = 0; i < layout.pages.length; i++) {
41953
+ const page = layout.pages[i];
41954
+ const pageHeight = page.size?.h ?? configuredPageHeight;
41955
+ if (normalizedY >= pageY && normalizedY < pageY + pageHeight) {
41956
+ pageHit = { pageIndex: i, page };
41957
+ break;
41958
+ }
41959
+ pageY += pageHeight + gap;
41960
+ }
41961
+ }
41962
+ if (!pageHit) {
41963
+ return null;
41964
+ }
41965
+ const pageRelativeY = normalizedY - pageY;
41966
+ const point = { x: normalizedX, y: pageRelativeY };
41967
+ return hitTestTableFragment(pageHit, blocks, measures, point);
41968
+ }
41969
+ const MULTI_CLICK_TIME_THRESHOLD_MS = 400;
41970
+ const MULTI_CLICK_DISTANCE_THRESHOLD_PX = 5;
41971
+ class EditorInputManager {
41972
+ // Dependencies
41973
+ #deps = null;
41974
+ #callbacks = {};
41975
+ // Drag selection state
41976
+ #isDragging = false;
41977
+ #dragAnchor = null;
41978
+ #dragAnchorPageIndex = null;
41979
+ #dragExtensionMode = "char";
41980
+ #dragLastPointer = null;
41981
+ #dragLastRawHit = null;
41982
+ #dragUsedPageNotMountedFallback = false;
41983
+ // Click tracking for multi-click detection
41984
+ #clickCount = 0;
41985
+ #lastClickTime = 0;
41986
+ #lastClickPosition = null;
41987
+ // Cell selection state
41988
+ #cellAnchor = null;
41989
+ #cellDragMode = "none";
41990
+ // Margin click state
41991
+ #pendingMarginClick = null;
41992
+ // Image selection state
41993
+ #lastSelectedImageBlockId = null;
41994
+ // Focus suppression (for draggable annotations)
41995
+ #suppressFocusInFromDraggable = false;
41996
+ // Debug state
41997
+ #debugLastPointer = null;
41998
+ #debugLastHit = null;
41999
+ // Bound handlers for event listener cleanup
42000
+ #boundHandlePointerDown = null;
42001
+ #boundHandlePointerMove = null;
42002
+ #boundHandlePointerUp = null;
42003
+ #boundHandlePointerLeave = null;
42004
+ #boundHandleDoubleClick = null;
42005
+ #boundHandleClick = null;
42006
+ #boundHandleKeyDown = null;
42007
+ #boundHandleFocusIn = null;
42008
+ // ==========================================================================
42009
+ // Constructor
42010
+ // ==========================================================================
42011
+ constructor() {
42012
+ }
42013
+ // ==========================================================================
42014
+ // Setup Methods
42015
+ // ==========================================================================
42016
+ /**
42017
+ * Set dependencies from PresentationEditor.
42018
+ */
42019
+ setDependencies(deps) {
42020
+ this.#deps = deps;
42021
+ }
42022
+ /**
42023
+ * Set callbacks for events.
42024
+ */
42025
+ setCallbacks(callbacks2) {
42026
+ this.#callbacks = callbacks2;
42027
+ }
42028
+ /**
42029
+ * Bind event listeners to DOM elements.
42030
+ */
42031
+ bind() {
42032
+ if (!this.#deps) return;
42033
+ const viewportHost = this.#deps.getViewportHost();
42034
+ const visibleHost = this.#deps.getVisibleHost();
42035
+ viewportHost.ownerDocument ?? document;
42036
+ this.#boundHandlePointerDown = this.#handlePointerDown.bind(this);
42037
+ this.#boundHandlePointerMove = this.#handlePointerMove.bind(this);
42038
+ this.#boundHandlePointerUp = this.#handlePointerUp.bind(this);
42039
+ this.#boundHandlePointerLeave = this.#handlePointerLeave.bind(this);
42040
+ this.#boundHandleDoubleClick = this.#handleDoubleClick.bind(this);
42041
+ this.#boundHandleClick = this.#handleClick.bind(this);
42042
+ this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
42043
+ this.#boundHandleFocusIn = this.#handleFocusIn.bind(this);
42044
+ viewportHost.addEventListener("pointerdown", this.#boundHandlePointerDown);
42045
+ viewportHost.addEventListener("pointermove", this.#boundHandlePointerMove);
42046
+ viewportHost.addEventListener("pointerup", this.#boundHandlePointerUp);
42047
+ viewportHost.addEventListener("pointerleave", this.#boundHandlePointerLeave);
42048
+ viewportHost.addEventListener("dblclick", this.#boundHandleDoubleClick);
42049
+ viewportHost.addEventListener("click", this.#boundHandleClick);
42050
+ const container = viewportHost.closest(".presentation-editor");
42051
+ if (container) {
42052
+ container.addEventListener("keydown", this.#boundHandleKeyDown);
42053
+ }
42054
+ visibleHost.addEventListener("focusin", this.#boundHandleFocusIn);
42055
+ }
42056
+ /**
42057
+ * Unbind event listeners.
42058
+ */
42059
+ unbind() {
42060
+ if (!this.#deps) return;
42061
+ const viewportHost = this.#deps.getViewportHost();
42062
+ const visibleHost = this.#deps.getVisibleHost();
42063
+ if (this.#boundHandlePointerDown) {
42064
+ viewportHost.removeEventListener("pointerdown", this.#boundHandlePointerDown);
42065
+ }
42066
+ if (this.#boundHandlePointerMove) {
42067
+ viewportHost.removeEventListener("pointermove", this.#boundHandlePointerMove);
42068
+ }
42069
+ if (this.#boundHandlePointerUp) {
42070
+ viewportHost.removeEventListener("pointerup", this.#boundHandlePointerUp);
42071
+ }
42072
+ if (this.#boundHandlePointerLeave) {
42073
+ viewportHost.removeEventListener("pointerleave", this.#boundHandlePointerLeave);
42074
+ }
42075
+ if (this.#boundHandleDoubleClick) {
42076
+ viewportHost.removeEventListener("dblclick", this.#boundHandleDoubleClick);
42077
+ }
42078
+ if (this.#boundHandleClick) {
42079
+ viewportHost.removeEventListener("click", this.#boundHandleClick);
42080
+ }
42081
+ if (this.#boundHandleKeyDown) {
42082
+ const container = viewportHost.closest(".presentation-editor");
42083
+ if (container) {
42084
+ container.removeEventListener("keydown", this.#boundHandleKeyDown);
42085
+ }
42086
+ }
42087
+ if (this.#boundHandleFocusIn) {
42088
+ visibleHost.removeEventListener("focusin", this.#boundHandleFocusIn);
42089
+ }
42090
+ this.#boundHandlePointerDown = null;
42091
+ this.#boundHandlePointerMove = null;
42092
+ this.#boundHandlePointerUp = null;
42093
+ this.#boundHandlePointerLeave = null;
42094
+ this.#boundHandleDoubleClick = null;
42095
+ this.#boundHandleClick = null;
42096
+ this.#boundHandleKeyDown = null;
42097
+ this.#boundHandleFocusIn = null;
42098
+ }
42099
+ /**
42100
+ * Destroy the manager and clean up.
42101
+ */
42102
+ destroy() {
42103
+ this.unbind();
42104
+ this.#deps = null;
42105
+ this.#callbacks = {};
42106
+ this.#clearDragState();
42107
+ this.#clearCellAnchor();
42108
+ }
42109
+ // ==========================================================================
42110
+ // Public Getters
42111
+ // ==========================================================================
42112
+ /** Whether currently dragging */
42113
+ get isDragging() {
42114
+ return this.#isDragging;
42115
+ }
42116
+ /** Current drag anchor position */
42117
+ get dragAnchor() {
42118
+ return this.#dragAnchor;
42119
+ }
42120
+ /** Cell anchor state for table selection */
42121
+ get cellAnchor() {
42122
+ return this.#cellAnchor;
42123
+ }
42124
+ /** Debug last pointer position */
42125
+ get debugLastPointer() {
42126
+ return this.#debugLastPointer;
42127
+ }
42128
+ /** Debug last hit */
42129
+ get debugLastHit() {
42130
+ return this.#debugLastHit;
42131
+ }
42132
+ /** Last selected image block ID */
42133
+ get lastSelectedImageBlockId() {
42134
+ return this.#lastSelectedImageBlockId;
42135
+ }
42136
+ /** Drag anchor page index */
42137
+ get dragAnchorPageIndex() {
42138
+ return this.#dragAnchorPageIndex;
42139
+ }
42140
+ /** Get the page index from the last raw hit during drag */
42141
+ get dragLastHitPageIndex() {
42142
+ return this.#dragLastRawHit?.pageIndex ?? null;
42143
+ }
42144
+ /** Get the last raw hit during drag (for finalization) */
42145
+ get dragLastRawHit() {
42146
+ return this.#dragLastRawHit;
42147
+ }
42148
+ // ==========================================================================
42149
+ // Public Methods
42150
+ // ==========================================================================
42151
+ /**
42152
+ * Clear cell anchor (used when document changes).
42153
+ */
42154
+ clearCellAnchor() {
42155
+ this.#clearCellAnchor();
42156
+ }
42157
+ /**
42158
+ * Set suppress focus in flag (for draggable annotations).
42159
+ */
42160
+ setSuppressFocusInFromDraggable(value) {
42161
+ this.#suppressFocusInFromDraggable = value;
42162
+ }
42163
+ // ==========================================================================
42164
+ // Private Helper Methods
42165
+ // ==========================================================================
42166
+ #clearDragState() {
42167
+ this.#isDragging = false;
42168
+ this.#dragAnchor = null;
42169
+ this.#dragAnchorPageIndex = null;
42170
+ this.#dragExtensionMode = "char";
42171
+ this.#dragLastPointer = null;
42172
+ this.#dragLastRawHit = null;
42173
+ this.#dragUsedPageNotMountedFallback = false;
42174
+ }
42175
+ #clearCellAnchor() {
42176
+ this.#cellAnchor = null;
42177
+ this.#cellDragMode = "none";
42178
+ }
42179
+ #registerPointerClick(event) {
42180
+ const nextState = registerPointerClick(
42181
+ event,
42182
+ {
42183
+ clickCount: this.#clickCount,
42184
+ lastClickTime: this.#lastClickTime,
42185
+ lastClickPosition: this.#lastClickPosition ?? { x: 0, y: 0 }
42186
+ },
42187
+ {
42188
+ timeThresholdMs: MULTI_CLICK_TIME_THRESHOLD_MS,
42189
+ distanceThresholdPx: MULTI_CLICK_DISTANCE_THRESHOLD_PX,
42190
+ maxClickCount: 3
42191
+ }
42192
+ );
42193
+ this.#clickCount = nextState.clickCount;
42194
+ this.#lastClickTime = nextState.lastClickTime;
42195
+ this.#lastClickPosition = nextState.lastClickPosition;
42196
+ return nextState.clickCount;
42197
+ }
42198
+ #getFirstTextPosition() {
42199
+ const editor = this.#deps?.getEditor();
42200
+ return getFirstTextPosition(editor?.state?.doc ?? null);
42201
+ }
42202
+ #calculateExtendedSelection(anchor, head, mode) {
42203
+ const layoutState = this.#deps?.getLayoutState();
42204
+ return calculateExtendedSelection(layoutState?.blocks ?? [], anchor, head, mode);
42205
+ }
42206
+ #shouldUseCellSelection(currentTableHit) {
42207
+ return shouldUseCellSelection(currentTableHit, this.#cellAnchor, this.#cellDragMode);
42208
+ }
42209
+ #getCellPosFromTableHit(tableHit) {
42210
+ const editor = this.#deps?.getEditor();
42211
+ const layoutState = this.#deps?.getLayoutState();
42212
+ return getCellPosFromTableHit(tableHit, editor?.state?.doc ?? null, layoutState?.blocks ?? []);
42213
+ }
42214
+ #getTablePosFromHit(tableHit) {
42215
+ const editor = this.#deps?.getEditor();
42216
+ const layoutState = this.#deps?.getLayoutState();
42217
+ return getTablePosFromHit(tableHit, editor?.state?.doc ?? null, layoutState?.blocks ?? []);
42218
+ }
42219
+ #setCellAnchor(tableHit, tablePos) {
42220
+ const cellPos = this.#getCellPosFromTableHit(tableHit);
42221
+ if (cellPos === null) return;
42222
+ this.#cellAnchor = {
42223
+ tablePos,
42224
+ cellPos,
42225
+ cellRowIndex: tableHit.cellRowIndex,
42226
+ cellColIndex: tableHit.cellColIndex,
42227
+ tableBlockId: tableHit.block.id
42228
+ };
42229
+ this.#cellDragMode = "pending";
42230
+ }
42231
+ #hitTestTable(x, y2) {
42232
+ return this.#callbacks.hitTestTable?.(x, y2) ?? null;
42233
+ }
42234
+ // ==========================================================================
42235
+ // Event Handlers
42236
+ // ==========================================================================
42237
+ /**
42238
+ * Handle click events - specifically for link navigation prevention.
42239
+ *
42240
+ * Link handling is split between pointerdown and click:
42241
+ * - pointerdown: dispatches superdoc-link-click event (for popover/UI response)
42242
+ * - click: prevents default navigation (preventDefault only works on click, not pointerdown)
42243
+ *
42244
+ * This also handles keyboard activation (Enter/Space) which triggers click but not pointerdown.
42245
+ */
42246
+ #handleClick(event) {
42247
+ const target = event.target;
42248
+ const linkEl = target?.closest?.("a.superdoc-link");
42249
+ if (linkEl) {
42250
+ event.preventDefault();
42251
+ if (!event.pointerId && event.detail === 0) {
42252
+ this.#handleLinkClick(event, linkEl);
42253
+ }
42254
+ }
42255
+ }
42256
+ #handlePointerDown(event) {
42257
+ if (!this.#deps) return;
42258
+ if (event.button !== 0) return;
42259
+ if (event.ctrlKey && navigator.platform.includes("Mac")) return;
42260
+ this.#pendingMarginClick = null;
42261
+ const target = event.target;
42262
+ if (target?.closest?.(".superdoc-ruler-handle") != null) return;
42263
+ const linkEl = target?.closest?.("a.superdoc-link");
42264
+ if (linkEl) {
42265
+ this.#handleLinkClick(event, linkEl);
42266
+ return;
42267
+ }
42268
+ const annotationEl = target?.closest?.(".annotation[data-pm-start]");
42269
+ const isDraggableAnnotation = target?.closest?.('[data-draggable="true"]') != null;
42270
+ this.#suppressFocusInFromDraggable = isDraggableAnnotation;
42271
+ if (annotationEl) {
42272
+ this.#handleAnnotationClick(event, annotationEl);
42273
+ return;
42274
+ }
42275
+ const layoutState = this.#deps.getLayoutState();
42276
+ if (!layoutState.layout) {
42277
+ this.#handleClickWithoutLayout(event, isDraggableAnnotation);
42278
+ return;
42279
+ }
42280
+ const normalizedPoint = this.#callbacks.normalizeClientPoint?.(event.clientX, event.clientY);
42281
+ if (!normalizedPoint) return;
42282
+ const { x, y: y2 } = normalizedPoint;
42283
+ this.#debugLastPointer = { clientX: event.clientX, clientY: event.clientY, x, y: y2 };
42284
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
42285
+ if (sessionMode !== "body") {
42286
+ if (this.#handleClickInHeaderFooterMode(event, x, y2)) return;
42287
+ }
42288
+ const headerFooterRegion = this.#callbacks.hitTestHeaderFooterRegion?.(x, y2);
42289
+ if (headerFooterRegion) return;
42290
+ const viewportHost = this.#deps.getViewportHost();
42291
+ const pageGeometryHelper = this.#deps.getPageGeometryHelper();
42292
+ const rawHit = clickToPosition(
42293
+ layoutState.layout,
42294
+ layoutState.blocks,
42295
+ layoutState.measures,
42296
+ { x, y: y2 },
42297
+ viewportHost,
42298
+ event.clientX,
42299
+ event.clientY,
42300
+ pageGeometryHelper ?? void 0
42301
+ );
42302
+ const editor = this.#deps.getEditor();
42303
+ const doc2 = editor.state?.doc;
42304
+ const epochMapper = this.#deps.getEpochMapper();
42305
+ const mapped = rawHit && doc2 ? epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1) : null;
42306
+ if (mapped && !mapped.ok) {
42307
+ debugLog("warn", "pointerdown mapping failed", mapped);
42308
+ }
42309
+ const hit = rawHit && doc2 && mapped?.ok ? { ...rawHit, pos: Math.max(0, Math.min(mapped.pos, doc2.content.size)), layoutEpoch: mapped.toEpoch } : null;
42310
+ 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 };
42311
+ this.#callbacks.updateSelectionDebugHud?.();
42312
+ if (!isDraggableAnnotation) {
42313
+ event.preventDefault();
42314
+ }
42315
+ if (!rawHit) {
42316
+ this.#focusEditorAtFirstPosition();
42317
+ return;
42318
+ }
42319
+ if (!hit || !doc2) {
42320
+ this.#callbacks.setPendingDocChange?.();
42321
+ this.#callbacks.scheduleRerender?.();
42322
+ return;
42323
+ }
42324
+ const fragmentHit = getFragmentAtPosition(layoutState.layout, layoutState.blocks, layoutState.measures, rawHit.pos);
42325
+ const targetImg = event.target?.closest?.("img");
42326
+ if (this.#handleInlineImageClick(event, targetImg, rawHit, doc2, epochMapper)) return;
42327
+ if (this.#handleFragmentClick(event, fragmentHit, hit, doc2)) return;
42328
+ if (this.#lastSelectedImageBlockId) {
42329
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
42330
+ this.#lastSelectedImageBlockId = null;
42331
+ }
42332
+ if (event.shiftKey && editor.state.selection.$anchor) {
42333
+ this.#handleShiftClick(event, hit.pos);
42334
+ return;
42335
+ }
42336
+ const clickDepth = this.#registerPointerClick(event);
42337
+ if (clickDepth === 1) {
42338
+ this.#dragAnchor = hit.pos;
42339
+ this.#dragAnchorPageIndex = hit.pageIndex;
42340
+ this.#pendingMarginClick = this.#callbacks.computePendingMarginClick?.(event.pointerId, x, y2) ?? null;
42341
+ const tableHit = this.#hitTestTable(x, y2);
42342
+ if (tableHit) {
42343
+ const tablePos = this.#getTablePosFromHit(tableHit);
42344
+ if (tablePos !== null) {
42345
+ this.#setCellAnchor(tableHit, tablePos);
42346
+ }
42347
+ } else {
42348
+ this.#clearCellAnchor();
42349
+ }
42350
+ } else {
42351
+ this.#pendingMarginClick = null;
42352
+ }
42353
+ this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x, y: y2 };
42354
+ this.#dragLastRawHit = hit;
42355
+ this.#dragUsedPageNotMountedFallback = false;
42356
+ this.#isDragging = true;
42357
+ if (clickDepth >= 3) {
42358
+ this.#dragExtensionMode = "para";
42359
+ } else if (clickDepth === 2) {
42360
+ this.#dragExtensionMode = "word";
42361
+ } else {
42362
+ this.#dragExtensionMode = "char";
42363
+ }
42364
+ if (typeof viewportHost.setPointerCapture === "function") {
42365
+ viewportHost.setPointerCapture(event.pointerId);
42366
+ }
42367
+ let handledByDepth = false;
42368
+ const sessionModeForDepth = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
42369
+ if (sessionModeForDepth === "body") {
42370
+ const selectionPos = clickDepth >= 2 && this.#dragAnchor !== null ? this.#dragAnchor : hit.pos;
42371
+ if (clickDepth >= 3) {
42372
+ handledByDepth = this.#callbacks.selectParagraphAt?.(selectionPos) ?? false;
42373
+ } else if (clickDepth === 2) {
42374
+ handledByDepth = this.#callbacks.selectWordAt?.(selectionPos) ?? false;
42375
+ }
42376
+ }
42377
+ if (!handledByDepth) {
42378
+ try {
42379
+ let nextSelection = superEditor_converter.TextSelection.create(doc2, hit.pos);
42380
+ if (!nextSelection.$from.parent.inlineContent) {
42381
+ nextSelection = superEditor_converter.Selection.near(doc2.resolve(hit.pos), 1);
42382
+ }
42383
+ const tr = editor.state.tr.setSelection(nextSelection);
42384
+ editor.view?.dispatch(tr);
42385
+ } catch {
42386
+ }
42387
+ }
42388
+ this.#callbacks.scheduleSelectionUpdate?.();
42389
+ this.#focusEditor();
42390
+ }
42391
+ #handlePointerMove(event) {
42392
+ if (!this.#deps) return;
42393
+ const layoutState = this.#deps.getLayoutState();
42394
+ if (!layoutState.layout) return;
42395
+ const normalized = this.#callbacks.normalizeClientPoint?.(event.clientX, event.clientY);
42396
+ if (!normalized) return;
42397
+ if (this.#isDragging && this.#dragAnchor !== null && event.buttons & 1) {
42398
+ this.#handleDragSelection(event, normalized);
42399
+ return;
42400
+ }
42401
+ this.#handleHover(normalized);
42402
+ }
42403
+ #handlePointerUp(event) {
42404
+ if (!this.#deps) return;
42405
+ this.#suppressFocusInFromDraggable = false;
42406
+ if (!this.#isDragging) return;
42407
+ const viewportHost = this.#deps.getViewportHost();
42408
+ if (typeof viewportHost.hasPointerCapture === "function" && typeof viewportHost.releasePointerCapture === "function" && viewportHost.hasPointerCapture(event.pointerId)) {
42409
+ viewportHost.releasePointerCapture(event.pointerId);
42410
+ }
42411
+ const pendingMarginClick = this.#pendingMarginClick;
42412
+ this.#pendingMarginClick = null;
42413
+ const dragAnchor = this.#dragAnchor;
42414
+ const dragMode = this.#dragExtensionMode;
42415
+ const dragUsedFallback = this.#dragUsedPageNotMountedFallback;
42416
+ const dragPointer = this.#dragLastPointer;
42417
+ this.#isDragging = false;
42418
+ if (this.#cellDragMode !== "none") {
42419
+ this.#cellDragMode = "none";
42420
+ }
42421
+ if (!pendingMarginClick || pendingMarginClick.pointerId !== event.pointerId) {
42422
+ this.#callbacks.updateSelectionVirtualizationPins?.({ includeDragBuffer: false });
42423
+ if (dragUsedFallback && dragAnchor != null) {
42424
+ const pointer = dragPointer ?? { clientX: event.clientX, clientY: event.clientY };
42425
+ this.#callbacks.finalizeDragSelectionWithDom?.(pointer, dragAnchor, dragMode);
42426
+ }
42427
+ this.#callbacks.scheduleA11ySelectionAnnouncement?.({ immediate: true });
42428
+ this.#dragLastPointer = null;
42429
+ this.#dragLastRawHit = null;
42430
+ this.#dragUsedPageNotMountedFallback = false;
42431
+ return;
42432
+ }
42433
+ this.#handleMarginClickEnd(event, pendingMarginClick);
42434
+ }
42435
+ #handlePointerLeave() {
42436
+ this.#callbacks.clearHoverRegion?.();
42437
+ }
42438
+ #handleDoubleClick(event) {
42439
+ if (!this.#deps) return;
42440
+ if (event.button !== 0) return;
42441
+ const layoutState = this.#deps.getLayoutState();
42442
+ if (!layoutState.layout) return;
42443
+ const viewportHost = this.#deps.getViewportHost();
42444
+ const visibleHost = this.#deps.getVisibleHost();
42445
+ const zoom = this.#deps.getZoom();
42446
+ const rect = viewportHost.getBoundingClientRect();
42447
+ const scrollLeft = visibleHost.scrollLeft ?? 0;
42448
+ const scrollTop = visibleHost.scrollTop ?? 0;
42449
+ const x = (event.clientX - rect.left + scrollLeft) / zoom;
42450
+ const y2 = (event.clientY - rect.top + scrollTop) / zoom;
42451
+ const region = this.#callbacks.hitTestHeaderFooterRegion?.(x, y2);
42452
+ if (region) {
42453
+ event.preventDefault();
42454
+ event.stopPropagation();
42455
+ const descriptor = this.#callbacks.resolveDescriptorForRegion?.(region);
42456
+ const hfManager = this.#deps.getHeaderFooterSession()?.manager;
42457
+ if (!descriptor && hfManager) {
42458
+ this.#callbacks.createDefaultHeaderFooter?.(region);
42459
+ hfManager.refresh();
42460
+ }
42461
+ this.#callbacks.activateHeaderFooterRegion?.(region);
42462
+ } else if ((this.#deps.getHeaderFooterSession()?.session?.mode ?? "body") !== "body") {
42463
+ this.#callbacks.exitHeaderFooterMode?.();
42464
+ }
42465
+ }
42466
+ #handleKeyDown(event) {
42467
+ if (!this.#deps) return;
42468
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
42469
+ if (event.key === "Escape" && sessionMode !== "body") {
42470
+ event.preventDefault();
42471
+ this.#callbacks.exitHeaderFooterMode?.();
42472
+ return;
42473
+ }
42474
+ if (event.ctrlKey && event.altKey && !event.shiftKey) {
42475
+ if (event.code === "KeyH") {
42476
+ event.preventDefault();
42477
+ this.#focusHeaderFooterShortcut("header");
42478
+ } else if (event.code === "KeyF") {
42479
+ event.preventDefault();
42480
+ this.#focusHeaderFooterShortcut("footer");
42481
+ }
42482
+ }
42483
+ }
42484
+ #handleFocusIn(event) {
42485
+ if (!this.#deps) return;
42486
+ if (this.#suppressFocusInFromDraggable) {
42487
+ this.#suppressFocusInFromDraggable = false;
42488
+ return;
42489
+ }
42490
+ try {
42491
+ this.#deps.getActiveEditor().view?.focus();
42492
+ } catch {
42493
+ }
42494
+ }
42495
+ // ==========================================================================
42496
+ // Handler Helpers
42497
+ // ==========================================================================
42498
+ #handleLinkClick(event, linkEl) {
42499
+ const href = linkEl.getAttribute("href") ?? "";
42500
+ const isAnchorLink = href.startsWith("#") && href.length > 1;
42501
+ const isTocLink = linkEl.closest(".superdoc-toc-entry") !== null;
42502
+ if (isAnchorLink && isTocLink) {
42503
+ event.preventDefault();
42504
+ event.stopPropagation();
42505
+ this.#callbacks.goToAnchor?.(href);
42506
+ return;
42507
+ }
42508
+ event.preventDefault();
42509
+ event.stopPropagation();
42510
+ const linkClickEvent = new CustomEvent("superdoc-link-click", {
42511
+ bubbles: true,
42512
+ composed: true,
42513
+ detail: {
42514
+ href,
42515
+ target: linkEl.getAttribute("target"),
42516
+ rel: linkEl.getAttribute("rel"),
42517
+ tooltip: linkEl.getAttribute("title"),
42518
+ element: linkEl,
42519
+ clientX: event.clientX,
42520
+ clientY: event.clientY
42521
+ }
42522
+ });
42523
+ linkEl.dispatchEvent(linkClickEvent);
42524
+ }
42525
+ #handleAnnotationClick(event, annotationEl) {
42526
+ const editor = this.#deps?.getEditor();
42527
+ if (!editor?.isEditable) return;
42528
+ const resolved = this.#callbacks.resolveFieldAnnotationSelectionFromElement?.(annotationEl);
42529
+ if (resolved) {
42530
+ try {
42531
+ const tr = editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(editor.state.doc, resolved.pos));
42532
+ editor.view?.dispatch(tr);
42533
+ } catch {
42534
+ }
42535
+ editor.emit("fieldAnnotationClicked", {
42536
+ editor,
42537
+ node: resolved.node,
42538
+ nodePos: resolved.pos,
42539
+ event,
42540
+ currentTarget: annotationEl
42541
+ });
42542
+ }
42543
+ }
42544
+ #handleClickWithoutLayout(event, isDraggableAnnotation) {
42545
+ if (!isDraggableAnnotation) {
42546
+ event.preventDefault();
42547
+ }
42548
+ if (document.activeElement instanceof HTMLElement) {
42549
+ document.activeElement.blur();
42550
+ }
42551
+ this.#focusEditorAtFirstPosition();
42552
+ }
42553
+ #handleClickInHeaderFooterMode(event, x, y2) {
42554
+ const session = this.#deps?.getHeaderFooterSession();
42555
+ const activeEditorHost = session?.overlayManager?.getActiveEditorHost?.();
42556
+ const clickedInsideEditorHost = activeEditorHost && (activeEditorHost.contains(event.target) || activeEditorHost === event.target);
42557
+ if (clickedInsideEditorHost) {
42558
+ return true;
42559
+ }
42560
+ const headerFooterRegion = this.#callbacks.hitTestHeaderFooterRegion?.(x, y2);
42561
+ if (!headerFooterRegion) {
42562
+ this.#callbacks.exitHeaderFooterMode?.();
42563
+ return false;
42564
+ }
42565
+ return true;
42566
+ }
42567
+ #handleInlineImageClick(event, targetImg, rawHit, doc2, epochMapper) {
42568
+ if (!targetImg) return false;
42569
+ const imgPmStart = targetImg.dataset?.pmStart ? Number(targetImg.dataset.pmStart) : null;
42570
+ if (Number.isNaN(imgPmStart) || imgPmStart == null) return false;
42571
+ const imgLayoutEpochRaw = targetImg.dataset?.layoutEpoch;
42572
+ const imgLayoutEpoch = imgLayoutEpochRaw != null ? Number(imgLayoutEpochRaw) : NaN;
42573
+ const rawLayoutEpoch = Number.isFinite(rawHit.layoutEpoch) ? rawHit.layoutEpoch : NaN;
42574
+ const effectiveEpoch = Number.isFinite(imgLayoutEpoch) && Number.isFinite(rawLayoutEpoch) ? Math.max(imgLayoutEpoch, rawLayoutEpoch) : Number.isFinite(imgLayoutEpoch) ? imgLayoutEpoch : rawHit.layoutEpoch;
42575
+ const mappedImg = epochMapper.mapPosFromLayoutToCurrentDetailed(imgPmStart, effectiveEpoch, 1);
42576
+ if (!mappedImg.ok) {
42577
+ debugLog("warn", "inline image mapping failed", mappedImg);
42578
+ this.#callbacks.setPendingDocChange?.();
42579
+ this.#callbacks.scheduleRerender?.();
42580
+ return true;
42581
+ }
42582
+ const clampedImgPos = Math.max(0, Math.min(mappedImg.pos, doc2.content.size));
42583
+ if (clampedImgPos < 0 || clampedImgPos >= doc2.content.size) return true;
42584
+ const newSelectionId = `inline-${clampedImgPos}`;
42585
+ if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== newSelectionId) {
42586
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
42587
+ }
42588
+ const editor = this.#deps?.getEditor();
42589
+ try {
42590
+ const tr = editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(doc2, clampedImgPos));
42591
+ editor.view?.dispatch(tr);
42592
+ const selector = `.superdoc-inline-image[data-pm-start="${imgPmStart}"]`;
42593
+ const viewportHost = this.#deps?.getViewportHost();
42594
+ const targetElement = viewportHost?.querySelector(selector);
42595
+ this.#callbacks.emit?.("imageSelected", {
42596
+ element: targetElement ?? targetImg,
42597
+ blockId: null,
42598
+ pmStart: clampedImgPos
42599
+ });
42600
+ this.#lastSelectedImageBlockId = newSelectionId;
42601
+ } catch (error) {
42602
+ if (vue.process$1.env.NODE_ENV === "development") {
42603
+ console.warn(`[EditorInputManager] Failed to create NodeSelection for inline image:`, error);
42604
+ }
42605
+ }
42606
+ this.#callbacks.focusEditorAfterImageSelection?.();
42607
+ return true;
42608
+ }
42609
+ #handleFragmentClick(event, fragmentHit, hit, doc2) {
42610
+ if (!fragmentHit) return false;
42611
+ if (fragmentHit.fragment.kind !== "image" && fragmentHit.fragment.kind !== "drawing") return false;
42612
+ const editor = this.#deps?.getEditor();
42613
+ try {
42614
+ const tr = editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(doc2, hit.pos));
42615
+ editor.view?.dispatch(tr);
42616
+ if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== fragmentHit.fragment.blockId) {
42617
+ this.#callbacks.emit?.("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
42618
+ }
42619
+ if (fragmentHit.fragment.kind === "image") {
42620
+ const viewportHost = this.#deps?.getViewportHost();
42621
+ const targetElement = viewportHost?.querySelector(
42622
+ `.superdoc-image-fragment[data-pm-start="${fragmentHit.fragment.pmStart}"]`
42623
+ );
42624
+ if (targetElement) {
42625
+ this.#callbacks.emit?.("imageSelected", {
42626
+ element: targetElement,
42627
+ blockId: fragmentHit.fragment.blockId,
42628
+ pmStart: fragmentHit.fragment.pmStart
42629
+ });
42630
+ this.#lastSelectedImageBlockId = fragmentHit.fragment.blockId;
42631
+ }
42632
+ }
42633
+ } catch (error) {
42634
+ if (vue.process$1.env.NODE_ENV === "development") {
42635
+ console.warn("[EditorInputManager] Failed to create NodeSelection for atomic fragment:", error);
42636
+ }
42637
+ }
42638
+ this.#callbacks.focusEditorAfterImageSelection?.();
42639
+ return true;
42640
+ }
42641
+ #handleShiftClick(event, headPos) {
42642
+ const editor = this.#deps?.getEditor();
42643
+ if (!editor) return;
42644
+ const anchor = editor.state.selection.anchor;
42645
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, headPos, this.#dragExtensionMode);
42646
+ try {
42647
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(editor.state.doc, selAnchor, selHead));
42648
+ editor.view?.dispatch(tr);
42649
+ this.#callbacks.scheduleSelectionUpdate?.();
42650
+ } catch (error) {
42651
+ console.warn("[SELECTION] Failed to extend selection on shift+click:", error);
42652
+ }
42653
+ this.#focusEditor();
42654
+ }
42655
+ #handleDragSelection(event, normalized) {
42656
+ if (!this.#deps) return;
42657
+ this.#pendingMarginClick = null;
42658
+ this.#dragLastPointer;
42659
+ this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y };
42660
+ const layoutState = this.#deps.getLayoutState();
42661
+ const viewportHost = this.#deps.getViewportHost();
42662
+ const pageGeometryHelper = this.#deps.getPageGeometryHelper();
42663
+ const rawHit = clickToPosition(
42664
+ layoutState.layout,
42665
+ layoutState.blocks,
42666
+ layoutState.measures,
42667
+ { x: normalized.x, y: normalized.y },
42668
+ viewportHost,
42669
+ event.clientX,
42670
+ event.clientY,
42671
+ pageGeometryHelper ?? void 0
42672
+ );
42673
+ if (!rawHit) return;
42674
+ const editor = this.#deps.getEditor();
42675
+ const doc2 = editor.state?.doc;
42676
+ if (!doc2) return;
42677
+ this.#dragLastRawHit = rawHit;
42678
+ const pageMounted = this.#deps.getPageElement(rawHit.pageIndex) != null;
42679
+ if (!pageMounted && this.#deps.isSelectionAwareVirtualizationEnabled()) {
42680
+ this.#dragUsedPageNotMountedFallback = true;
42681
+ }
42682
+ this.#callbacks.updateSelectionVirtualizationPins?.({ includeDragBuffer: true, extraPages: [rawHit.pageIndex] });
42683
+ const epochMapper = this.#deps.getEpochMapper();
42684
+ const mappedHead = epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1);
42685
+ if (!mappedHead.ok) {
42686
+ debugLog("warn", "drag mapping failed", mappedHead);
42687
+ return;
42688
+ }
42689
+ const hit = {
42690
+ ...rawHit,
42691
+ pos: Math.max(0, Math.min(mappedHead.pos, doc2.content.size)),
42692
+ layoutEpoch: mappedHead.toEpoch
42693
+ };
42694
+ this.#debugLastHit = {
42695
+ source: pageMounted ? "dom" : "geometry",
42696
+ pos: rawHit.pos,
42697
+ layoutEpoch: rawHit.layoutEpoch,
42698
+ mappedPos: hit.pos
42699
+ };
42700
+ this.#callbacks.updateSelectionDebugHud?.();
42701
+ const currentTableHit = this.#hitTestTable(normalized.x, normalized.y);
42702
+ const shouldUseCellSel = this.#shouldUseCellSelection(currentTableHit);
42703
+ if (shouldUseCellSel && this.#cellAnchor) {
42704
+ this.#handleCellDragSelection(currentTableHit, hit);
42705
+ return;
42706
+ }
42707
+ const anchor = this.#dragAnchor;
42708
+ const head = hit.pos;
42709
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
42710
+ try {
42711
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(editor.state.doc, selAnchor, selHead));
42712
+ editor.view?.dispatch(tr);
42713
+ this.#callbacks.scheduleSelectionUpdate?.();
42714
+ } catch (error) {
42715
+ console.warn("[SELECTION] Failed to extend selection during drag:", error);
42716
+ }
42717
+ }
42718
+ #handleCellDragSelection(currentTableHit, hit) {
42719
+ const headCellPos = currentTableHit ? this.#getCellPosFromTableHit(currentTableHit) : null;
42720
+ if (headCellPos === null) return;
42721
+ if (this.#cellDragMode !== "active") {
42722
+ this.#cellDragMode = "active";
42723
+ }
42724
+ const editor = this.#deps?.getEditor();
42725
+ if (!editor) return;
42726
+ try {
42727
+ const doc2 = editor.state.doc;
42728
+ const anchorCellPos = this.#cellAnchor.cellPos;
42729
+ const clampedAnchor = Math.max(0, Math.min(anchorCellPos, doc2.content.size));
42730
+ const clampedHead = Math.max(0, Math.min(headCellPos, doc2.content.size));
42731
+ const cellSelection = CellSelection.create(doc2, clampedAnchor, clampedHead);
42732
+ const tr = editor.state.tr.setSelection(cellSelection);
42733
+ editor.view?.dispatch(tr);
42734
+ this.#callbacks.scheduleSelectionUpdate?.();
42735
+ } catch (error) {
42736
+ console.warn("[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:", error);
42737
+ const anchor = this.#dragAnchor;
42738
+ const head = hit.pos;
42739
+ const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
42740
+ try {
42741
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(editor.state.doc, selAnchor, selHead));
42742
+ editor.view?.dispatch(tr);
42743
+ this.#callbacks.scheduleSelectionUpdate?.();
42744
+ } catch {
42745
+ }
42746
+ }
42747
+ }
42748
+ #handleHover(normalized) {
42749
+ if (!this.#deps) return;
42750
+ const sessionMode = this.#deps.getHeaderFooterSession()?.session?.mode ?? "body";
42751
+ if (sessionMode !== "body") {
42752
+ this.#callbacks.clearHoverRegion?.();
42753
+ return;
42754
+ }
42755
+ if (this.#deps.getDocumentMode() === "viewing") {
42756
+ this.#callbacks.clearHoverRegion?.();
42757
+ return;
42758
+ }
42759
+ const region = this.#callbacks.hitTestHeaderFooterRegion?.(normalized.x, normalized.y);
42760
+ if (!region) {
42761
+ this.#callbacks.clearHoverRegion?.();
42762
+ return;
42763
+ }
42764
+ const currentHover = this.#deps.getHeaderFooterSession()?.hoverRegion;
42765
+ if (currentHover && currentHover.kind === region.kind && currentHover.pageIndex === region.pageIndex && currentHover.sectionType === region.sectionType) {
42766
+ return;
42767
+ }
42768
+ this.#deps.getHeaderFooterSession()?.renderHover(region);
42769
+ this.#callbacks.renderHoverRegion?.(region);
42770
+ }
42771
+ #handleMarginClickEnd(event, pendingMarginClick) {
42772
+ const sessionMode = this.#deps?.getHeaderFooterSession()?.session?.mode ?? "body";
42773
+ if (sessionMode !== "body" || this.#deps?.isViewLocked()) {
42774
+ this.#clearDragPointerState();
42775
+ return;
42776
+ }
42777
+ const editor = this.#deps?.getEditor();
42778
+ const doc2 = editor?.state?.doc;
42779
+ if (!doc2) {
42780
+ this.#clearDragPointerState();
42781
+ return;
42782
+ }
42783
+ const epochMapper = this.#deps?.getEpochMapper();
42784
+ if (!epochMapper) {
42785
+ this.#clearDragPointerState();
42786
+ return;
42787
+ }
42788
+ if (pendingMarginClick.kind === "aboveFirstLine") {
42789
+ const pos = this.#getFirstTextPosition();
42790
+ try {
42791
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, pos));
42792
+ editor.view?.dispatch(tr);
42793
+ this.#callbacks.scheduleSelectionUpdate?.();
42794
+ } catch {
42795
+ }
42796
+ this.#debugLastHit = { source: "margin", pos: null, layoutEpoch: null, mappedPos: pos };
42797
+ this.#callbacks.updateSelectionDebugHud?.();
42798
+ this.#clearDragPointerState();
42799
+ return;
42800
+ }
42801
+ if (pendingMarginClick.kind === "right") {
42802
+ const mappedEnd2 = epochMapper.mapPosFromLayoutToCurrentDetailed(
42803
+ pendingMarginClick.pmEnd,
42804
+ pendingMarginClick.layoutEpoch,
42805
+ 1
42806
+ );
42807
+ if (!mappedEnd2.ok) {
42808
+ this.#callbacks.setPendingDocChange?.();
42809
+ this.#callbacks.scheduleRerender?.();
42810
+ this.#clearDragPointerState();
42811
+ return;
42812
+ }
42813
+ const caretPos = Math.max(0, Math.min(mappedEnd2.pos, doc2.content.size));
42814
+ try {
42815
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, caretPos));
42816
+ editor.view?.dispatch(tr);
42817
+ this.#callbacks.scheduleSelectionUpdate?.();
42818
+ } catch {
42819
+ }
42820
+ this.#debugLastHit = {
42821
+ source: "margin",
42822
+ pos: pendingMarginClick.pmEnd,
42823
+ layoutEpoch: pendingMarginClick.layoutEpoch,
42824
+ mappedPos: caretPos
42825
+ };
42826
+ this.#callbacks.updateSelectionDebugHud?.();
42827
+ this.#clearDragPointerState();
42828
+ return;
42829
+ }
42830
+ const mappedStart = epochMapper.mapPosFromLayoutToCurrentDetailed(
42831
+ pendingMarginClick.pmStart,
42832
+ pendingMarginClick.layoutEpoch,
42833
+ 1
42834
+ );
42835
+ const mappedEnd = epochMapper.mapPosFromLayoutToCurrentDetailed(
42836
+ pendingMarginClick.pmEnd,
42837
+ pendingMarginClick.layoutEpoch,
42838
+ -1
42839
+ );
42840
+ if (!mappedStart.ok || !mappedEnd.ok) {
42841
+ this.#callbacks.setPendingDocChange?.();
42842
+ this.#callbacks.scheduleRerender?.();
42843
+ this.#clearDragPointerState();
42844
+ return;
42845
+ }
42846
+ const selFrom = Math.max(0, Math.min(Math.min(mappedStart.pos, mappedEnd.pos), doc2.content.size));
42847
+ const selTo = Math.max(0, Math.min(Math.max(mappedStart.pos, mappedEnd.pos), doc2.content.size));
42848
+ try {
42849
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, selFrom, selTo));
42850
+ editor.view?.dispatch(tr);
42851
+ this.#callbacks.scheduleSelectionUpdate?.();
42852
+ } catch {
42853
+ }
42854
+ this.#debugLastHit = {
42855
+ source: "margin",
42856
+ pos: pendingMarginClick.pmStart,
42857
+ layoutEpoch: pendingMarginClick.layoutEpoch,
42858
+ mappedPos: selFrom
42859
+ };
42860
+ this.#callbacks.updateSelectionDebugHud?.();
42861
+ this.#clearDragPointerState();
42862
+ }
42863
+ #clearDragPointerState() {
42864
+ this.#dragLastPointer = null;
42865
+ this.#dragLastRawHit = null;
42866
+ this.#dragUsedPageNotMountedFallback = false;
42867
+ }
42868
+ #focusHeaderFooterShortcut(kind) {
42869
+ const pageIndex = this.#callbacks.getCurrentPageIndex?.() ?? 0;
42870
+ const region = this.#callbacks.findRegionForPage?.(kind, pageIndex);
42871
+ if (!region) {
42872
+ this.#callbacks.emitHeaderFooterEditBlocked?.("missingRegion");
42873
+ return;
42874
+ }
42875
+ this.#callbacks.activateHeaderFooterRegion?.(region);
42876
+ }
42877
+ #focusEditorAtFirstPosition() {
42878
+ const editor = this.#deps?.getEditor();
42879
+ const editorDom = editor?.view?.dom;
42880
+ if (!editorDom) return;
42881
+ const validPos = this.#getFirstTextPosition();
42882
+ const doc2 = editor?.state?.doc;
42883
+ if (doc2) {
42884
+ try {
42885
+ const tr = editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, validPos));
42886
+ editor.view?.dispatch(tr);
42887
+ } catch {
42888
+ }
42889
+ }
42890
+ editorDom.focus();
42891
+ editor?.view?.focus();
42892
+ this.#callbacks.scheduleSelectionUpdate?.();
42893
+ }
42894
+ #focusEditor() {
42895
+ if (document.activeElement instanceof HTMLElement) {
42896
+ document.activeElement.blur();
42897
+ }
42898
+ const editor = this.#deps?.getEditor();
42899
+ const editorDom = editor?.view?.dom;
42900
+ if (editorDom) {
42901
+ editorDom.focus();
42902
+ editor?.view?.focus();
42903
+ }
42904
+ }
42905
+ }
42906
+ const createDefaultScheduler = () => {
42907
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
42908
+ return {
42909
+ requestAnimationFrame: (cb) => window.requestAnimationFrame(cb),
42910
+ cancelAnimationFrame: (handle) => window.cancelAnimationFrame(handle)
42911
+ };
42912
+ }
42913
+ const anyGlobal = globalThis;
42914
+ if (typeof anyGlobal.requestAnimationFrame === "function" && typeof anyGlobal.cancelAnimationFrame === "function") {
42915
+ return {
42916
+ requestAnimationFrame: (cb) => anyGlobal.requestAnimationFrame(cb),
42917
+ cancelAnimationFrame: (handle) => anyGlobal.cancelAnimationFrame(handle)
42918
+ };
42919
+ }
42920
+ return {
42921
+ requestAnimationFrame: (cb) => {
42922
+ const handle = anyGlobal.setTimeout?.(() => cb(Date.now()), 0);
42923
+ return handle;
42924
+ },
42925
+ cancelAnimationFrame: (handle) => {
42926
+ anyGlobal.clearTimeout?.(handle);
42927
+ }
42928
+ };
42929
+ };
42930
+ class SelectionSyncCoordinator extends EventEmitter {
42931
+ #docEpoch = 0;
42932
+ #layoutEpoch = 0;
42933
+ #layoutUpdating = false;
42934
+ #pending = false;
42935
+ #scheduled = false;
42936
+ #rafHandle = null;
42937
+ #scheduler;
42938
+ /**
42939
+ * Creates a new SelectionSyncCoordinator.
42940
+ *
42941
+ * @param options - Configuration options
42942
+ * @param options.scheduler - Custom scheduler for animation frames (useful for testing), defaults to platform scheduler
42943
+ */
42944
+ constructor(options) {
42945
+ super();
42946
+ this.#scheduler = options?.scheduler ?? createDefaultScheduler();
42947
+ }
42948
+ /**
42949
+ * Gets the current document epoch.
42950
+ *
42951
+ * @returns The document epoch (increments on each document-changing transaction)
42952
+ */
42953
+ getDocEpoch() {
42954
+ return this.#docEpoch;
42955
+ }
42956
+ /**
42957
+ * Gets the current layout epoch.
42958
+ *
42959
+ * @returns The epoch of the document version currently painted in the DOM
42960
+ */
42961
+ getLayoutEpoch() {
42962
+ return this.#layoutEpoch;
42963
+ }
42964
+ /**
42965
+ * Checks if a layout update is currently in progress.
42966
+ *
42967
+ * @returns True if between onLayoutStart() and onLayoutComplete(), false otherwise
42968
+ */
42969
+ isLayoutUpdating() {
42970
+ return this.#layoutUpdating;
42971
+ }
42972
+ /**
42973
+ * Updates the document epoch and triggers conditional rendering.
42974
+ *
42975
+ * @param epoch - The new document epoch (must be finite and non-negative)
42976
+ *
42977
+ * @remarks
42978
+ * When the document epoch changes:
42979
+ * 1. Any scheduled render is cancelled (layout will be out of sync)
42980
+ * 2. If layout has already caught up, rendering is rescheduled
42981
+ *
42982
+ * Calling with the same epoch as the current value is a no-op.
42983
+ * Invalid epoch values are silently ignored.
42984
+ */
42985
+ setDocEpoch(epoch) {
42986
+ if (!Number.isFinite(epoch) || epoch < 0) return;
42987
+ if (epoch === this.#docEpoch) return;
42988
+ this.#docEpoch = epoch;
42989
+ this.#cancelScheduledRender();
42990
+ this.#maybeSchedule();
42991
+ }
42992
+ /**
42993
+ * Notifies the coordinator that layout computation has started.
42994
+ *
42995
+ * @remarks
42996
+ * Marks the layout as updating and cancels any scheduled renders, since the DOM
42997
+ * is about to change and current position data will be stale.
42998
+ *
42999
+ * Safe to call multiple times (e.g., if layouts overlap) - subsequent calls are ignored
43000
+ * until onLayoutComplete() is called.
43001
+ */
43002
+ onLayoutStart() {
43003
+ if (this.#layoutUpdating) return;
43004
+ this.#layoutUpdating = true;
43005
+ this.#cancelScheduledRender();
43006
+ }
43007
+ /**
43008
+ * Notifies the coordinator that layout painting has completed.
43009
+ *
43010
+ * @param layoutEpoch - The document epoch that was just painted to the DOM
43011
+ *
43012
+ * @remarks
43013
+ * Marks the layout as no longer updating, records the new layout epoch, and attempts
43014
+ * to schedule rendering if conditions are now safe.
43015
+ *
43016
+ * If the layoutEpoch is invalid (not a finite non-negative number), it is ignored and
43017
+ * the previous layoutEpoch value is retained.
43018
+ *
43019
+ * This method is the primary trigger for selection rendering - if there's a pending
43020
+ * render request and layoutEpoch >= docEpoch, a render event will be scheduled.
43021
+ */
43022
+ onLayoutComplete(layoutEpoch) {
43023
+ this.#layoutUpdating = false;
43024
+ if (Number.isFinite(layoutEpoch) && layoutEpoch >= 0) {
43025
+ this.#layoutEpoch = layoutEpoch;
43026
+ }
43027
+ this.#maybeSchedule();
43028
+ }
43029
+ /**
43030
+ * Notifies the coordinator that layout was aborted without completing.
43031
+ *
43032
+ * @remarks
43033
+ * Marks the layout as no longer updating (without updating layoutEpoch) and attempts
43034
+ * to schedule rendering if conditions are safe.
43035
+ *
43036
+ * Use this when layout computation is cancelled or fails partway through.
43037
+ */
43038
+ onLayoutAbort() {
43039
+ this.#layoutUpdating = false;
43040
+ this.#maybeSchedule();
43041
+ }
43042
+ /**
43043
+ * Requests that selection rendering occur when conditions become safe.
43044
+ *
43045
+ * @param options - Rendering options
43046
+ * @param options.immediate - If true, attempts to render immediately (synchronously) if safe, defaults to false
43047
+ *
43048
+ * @remarks
43049
+ * Marks a render as pending and schedules it to occur on the next animation frame if
43050
+ * conditions are safe (layout not updating, layoutEpoch >= docEpoch).
43051
+ *
43052
+ * If options.immediate is true, also attempts a synchronous render before scheduling.
43053
+ * Use immediate rendering sparingly, as it can cause multiple renders per frame.
43054
+ *
43055
+ * Multiple calls are coalesced - only one render will occur per animation frame.
43056
+ */
43057
+ requestRender(options) {
43058
+ this.#pending = true;
43059
+ if (options?.immediate) {
43060
+ this.flushNow();
43061
+ }
43062
+ this.#maybeSchedule();
43063
+ }
43064
+ /**
43065
+ * Attempts to render selection immediately (synchronously) if conditions are safe.
43066
+ *
43067
+ * @remarks
43068
+ * If there's a pending render request and conditions are safe (layout not updating,
43069
+ * layoutEpoch >= docEpoch), this method:
43070
+ * 1. Cancels any scheduled asynchronous render
43071
+ * 2. Clears the pending flag
43072
+ * 3. Emits the 'render' event synchronously
43073
+ *
43074
+ * If no render is pending or conditions are not safe, this is a no-op.
43075
+ *
43076
+ * Use this for immediate selection updates in response to user actions (e.g., click
43077
+ * handlers) where waiting for the next animation frame would cause noticeable lag.
43078
+ */
43079
+ flushNow() {
43080
+ if (!this.#pending) return;
43081
+ if (!this.#isSafeToRender()) return;
43082
+ this.#cancelScheduledRender();
43083
+ this.#pending = false;
43084
+ this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
43085
+ }
43086
+ /**
43087
+ * Permanently tears down the coordinator, cancelling pending renders and removing all listeners.
43088
+ *
43089
+ * @remarks
43090
+ * After calling destroy(), this instance should not be used again. All scheduled renders
43091
+ * are cancelled and all event listeners are removed.
43092
+ *
43093
+ * Safe to call multiple times.
43094
+ */
43095
+ destroy() {
43096
+ this.#cancelScheduledRender();
43097
+ this.removeAllListeners();
43098
+ }
43099
+ #isSafeToRender() {
43100
+ return !this.#layoutUpdating && this.#layoutEpoch >= this.#docEpoch;
43101
+ }
43102
+ #maybeSchedule() {
43103
+ if (!this.#pending) return;
43104
+ if (!this.#isSafeToRender()) return;
43105
+ if (this.#scheduled) return;
43106
+ this.#scheduled = true;
43107
+ this.#rafHandle = this.#scheduler.requestAnimationFrame(() => {
43108
+ this.#scheduled = false;
43109
+ this.#rafHandle = null;
43110
+ if (!this.#pending) return;
43111
+ if (!this.#isSafeToRender()) return;
43112
+ this.#pending = false;
43113
+ this.emit("render", { docEpoch: this.#docEpoch, layoutEpoch: this.#layoutEpoch });
43114
+ });
43115
+ }
43116
+ #cancelScheduledRender() {
43117
+ if (this.#rafHandle != null) {
43118
+ try {
43119
+ this.#scheduler.cancelAnimationFrame(this.#rafHandle);
43120
+ } finally {
43121
+ this.#rafHandle = null;
43122
+ }
43123
+ }
43124
+ this.#scheduled = false;
43125
+ }
43126
+ }
43127
+ const uiSurfaces = /* @__PURE__ */ new WeakSet();
43128
+ function isInRegisteredSurface(event) {
43129
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
42195
43130
  if (path.length > 0) {
42196
43131
  for (const node2 of path) {
42197
43132
  if (uiSurfaces.has(node2)) return true;
@@ -42552,37 +43487,6 @@ class PresentationInputBridge {
42552
43487
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
42553
43488
  }
42554
43489
  }
42555
- const WORD_CHARACTER_REGEX = /[\p{L}\p{N}'\u2018\u2019_~-]/u;
42556
- function isWordCharacter(char) {
42557
- if (!char) {
42558
- return false;
42559
- }
42560
- return WORD_CHARACTER_REGEX.test(char);
42561
- }
42562
- function calculateExtendedSelection(blocks, anchor, head, mode) {
42563
- if (mode === "word") {
42564
- const anchorBounds = findWordBoundaries(blocks, anchor);
42565
- const headBounds = findWordBoundaries(blocks, head);
42566
- if (anchorBounds && headBounds) {
42567
- if (head >= anchor) {
42568
- return { selAnchor: anchorBounds.from, selHead: headBounds.to };
42569
- } else {
42570
- return { selAnchor: anchorBounds.to, selHead: headBounds.from };
42571
- }
42572
- }
42573
- } else if (mode === "para") {
42574
- const anchorBounds = findParagraphBoundaries(blocks, anchor);
42575
- const headBounds = findParagraphBoundaries(blocks, head);
42576
- if (anchorBounds && headBounds) {
42577
- if (head >= anchor) {
42578
- return { selAnchor: anchorBounds.from, selHead: headBounds.to };
42579
- } else {
42580
- return { selAnchor: anchorBounds.to, selHead: headBounds.from };
42581
- }
42582
- }
42583
- }
42584
- return { selAnchor: anchor, selHead: head };
42585
- }
42586
43490
  function getAtomNodeTypes(schema) {
42587
43491
  if (!schema) return [];
42588
43492
  const types = [];
@@ -42638,104 +43542,6 @@ function buildPositionMapFromPmDoc(pmDoc, jsonDoc) {
42638
43542
  if (!ok) return null;
42639
43543
  return map3;
42640
43544
  }
42641
- function registerPointerClick(event, previous, options) {
42642
- const time2 = event.timeStamp ?? performance.now();
42643
- const timeDelta = time2 - previous.lastClickTime;
42644
- const withinTime = timeDelta <= options.timeThresholdMs;
42645
- const distanceX = Math.abs(event.clientX - previous.lastClickPosition.x);
42646
- const distanceY = Math.abs(event.clientY - previous.lastClickPosition.y);
42647
- const withinDistance = distanceX <= options.distanceThresholdPx && distanceY <= options.distanceThresholdPx;
42648
- const clickCount = withinTime && withinDistance ? Math.min(previous.clickCount + 1, options.maxClickCount) : 1;
42649
- return {
42650
- clickCount,
42651
- lastClickTime: time2,
42652
- lastClickPosition: { x: event.clientX, y: event.clientY }
42653
- };
42654
- }
42655
- function getFirstTextPosition(doc2) {
42656
- if (!doc2 || !doc2.content) {
42657
- return 1;
42658
- }
42659
- let validPos = 1;
42660
- doc2.nodesBetween(0, doc2.content.size, (node, pos) => {
42661
- if (node.isTextblock) {
42662
- validPos = pos + 1;
42663
- return false;
42664
- }
42665
- return true;
42666
- });
42667
- return validPos;
42668
- }
42669
- function computeWordSelectionRangeAt(state, pos) {
42670
- if (!state?.doc) {
42671
- return null;
42672
- }
42673
- if (pos < 0 || pos > state.doc.content.size) {
42674
- return null;
42675
- }
42676
- const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
42677
- if (!textblockPos) {
42678
- return null;
42679
- }
42680
- const parentStart = textblockPos.start();
42681
- const parentEnd = textblockPos.end();
42682
- const sampleEnd = Math.min(pos + 1, parentEnd);
42683
- const charAtPos = state.doc.textBetween(pos, sampleEnd, "\0", "\0");
42684
- if (!isWordCharacter(charAtPos)) {
42685
- return null;
42686
- }
42687
- let startPos = pos;
42688
- while (startPos > parentStart) {
42689
- const prevChar = state.doc.textBetween(startPos - 1, startPos, "\0", "\0");
42690
- if (!isWordCharacter(prevChar)) {
42691
- break;
42692
- }
42693
- startPos -= 1;
42694
- }
42695
- let endPos = pos;
42696
- while (endPos < parentEnd) {
42697
- const nextChar = state.doc.textBetween(endPos, endPos + 1, "\0", "\0");
42698
- if (!isWordCharacter(nextChar)) {
42699
- break;
42700
- }
42701
- endPos += 1;
42702
- }
42703
- if (startPos === endPos) {
42704
- return null;
42705
- }
42706
- return { from: startPos, to: endPos };
42707
- }
42708
- function computeParagraphSelectionRangeAt(state, pos) {
42709
- if (!state?.doc) {
42710
- return null;
42711
- }
42712
- const textblockPos = findNearestTextblockResolvedPos(state.doc, pos);
42713
- if (!textblockPos) {
42714
- return null;
42715
- }
42716
- return { from: textblockPos.start(), to: textblockPos.end() };
42717
- }
42718
- function findNearestTextblockResolvedPos(doc2, pos) {
42719
- const $pos = doc2.resolve(pos);
42720
- let textblockPos = $pos;
42721
- while (textblockPos.depth > 0) {
42722
- if (textblockPos.parent?.isTextblock) {
42723
- break;
42724
- }
42725
- if (!textblockPos.parent || textblockPos.depth === 0) {
42726
- break;
42727
- }
42728
- const beforePos = textblockPos.before();
42729
- if (beforePos < 0 || beforePos > doc2.content.size) {
42730
- return null;
42731
- }
42732
- textblockPos = doc2.resolve(beforePos);
42733
- }
42734
- if (!textblockPos.parent?.isTextblock) {
42735
- return null;
42736
- }
42737
- return textblockPos;
42738
- }
42739
43545
  function syncHiddenEditorA11yAttributes(pmDom, documentMode) {
42740
43546
  const element = pmDom;
42741
43547
  if (!(element instanceof HTMLElement)) return;
@@ -43405,317 +44211,255 @@ async function goToAnchor({
43405
44211
  }
43406
44212
  return true;
43407
44213
  }
43408
- function getCellPosFromTableHit(tableHit, doc2, blocks) {
43409
- if (!tableHit || !tableHit.block || typeof tableHit.block.id !== "string") {
43410
- console.warn("[getCellPosFromTableHit] Invalid tableHit input:", tableHit);
43411
- return null;
43412
- }
43413
- if (typeof tableHit.cellRowIndex !== "number" || typeof tableHit.cellColIndex !== "number" || tableHit.cellRowIndex < 0 || tableHit.cellColIndex < 0) {
43414
- console.warn("[getCellPosFromTableHit] Invalid cell indices:", {
43415
- row: tableHit.cellRowIndex,
43416
- col: tableHit.cellColIndex
43417
- });
43418
- return null;
43419
- }
43420
- if (!doc2) return null;
43421
- const tableBlocks = blocks.filter((b2) => b2.kind === "table");
43422
- const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
43423
- if (targetTableIndex === -1) return null;
43424
- let tablePos = null;
43425
- let currentTableIndex = 0;
43426
- try {
43427
- doc2.descendants((node, pos) => {
43428
- if (node.type.name === "table") {
43429
- if (currentTableIndex === targetTableIndex) {
43430
- tablePos = pos;
43431
- return false;
43432
- }
43433
- currentTableIndex++;
43434
- }
43435
- return true;
43436
- });
43437
- } catch (error) {
43438
- console.error("[getCellPosFromTableHit] Error during document traversal:", error);
43439
- return null;
43440
- }
43441
- if (tablePos === null) return null;
43442
- const tableNode = doc2.nodeAt(tablePos);
43443
- if (!tableNode || tableNode.type.name !== "table") return null;
43444
- const targetRowIndex = tableHit.cellRowIndex;
43445
- const targetColIndex = tableHit.cellColIndex;
43446
- if (targetRowIndex >= tableNode.childCount) {
43447
- console.warn("[getCellPosFromTableHit] Target row index out of bounds:", {
43448
- targetRowIndex,
43449
- tableChildCount: tableNode.childCount
43450
- });
43451
- return null;
43452
- }
43453
- let currentPos = tablePos + 1;
43454
- for (let r2 = 0; r2 < tableNode.childCount && r2 <= targetRowIndex; r2++) {
43455
- const row = tableNode.child(r2);
43456
- if (r2 === targetRowIndex) {
43457
- currentPos += 1;
43458
- let logicalCol = 0;
43459
- for (let cellIndex = 0; cellIndex < row.childCount; cellIndex++) {
43460
- const cell = row.child(cellIndex);
43461
- const rawColspan = cell.attrs?.colspan;
43462
- const colspan = typeof rawColspan === "number" && Number.isFinite(rawColspan) && rawColspan > 0 ? rawColspan : 1;
43463
- if (targetColIndex >= logicalCol && targetColIndex < logicalCol + colspan) {
43464
- return currentPos;
43465
- }
43466
- currentPos += cell.nodeSize;
43467
- logicalCol += colspan;
43468
- }
43469
- console.warn("[getCellPosFromTableHit] Target column not found in row:", {
43470
- targetColIndex,
43471
- logicalColReached: logicalCol,
43472
- rowCellCount: row.childCount
43473
- });
43474
- return null;
43475
- } else {
43476
- currentPos += row.nodeSize;
44214
+ const INTERNAL_MIME_TYPE = "application/x-field-annotation";
44215
+ const FIELD_ANNOTATION_DATA_TYPE = "fieldAnnotation";
44216
+ function isValidFieldAnnotationAttributes(attrs) {
44217
+ if (!attrs || typeof attrs !== "object") return false;
44218
+ const a = attrs;
44219
+ return typeof a.fieldId === "string" && typeof a.fieldType === "string" && typeof a.displayLabel === "string" && typeof a.type === "string";
44220
+ }
44221
+ function parseIntSafe$1(value) {
44222
+ if (!value) return void 0;
44223
+ const parsed = parseInt(value, 10);
44224
+ return Number.isFinite(parsed) ? parsed : void 0;
44225
+ }
44226
+ function extractFieldAnnotationData(element) {
44227
+ const dataset = element.dataset;
44228
+ const attributes = {};
44229
+ for (const key2 in dataset) {
44230
+ const value = dataset[key2];
44231
+ if (value !== void 0) {
44232
+ attributes[key2] = value;
43477
44233
  }
43478
44234
  }
43479
- return null;
44235
+ return {
44236
+ fieldId: dataset.fieldId,
44237
+ fieldType: dataset.fieldType,
44238
+ variant: dataset.variant ?? dataset.type,
44239
+ displayLabel: dataset.displayLabel,
44240
+ pmStart: parseIntSafe$1(dataset.pmStart),
44241
+ pmEnd: parseIntSafe$1(dataset.pmEnd),
44242
+ attributes
44243
+ };
43480
44244
  }
43481
- function getTablePosFromHit(tableHit, doc2, blocks) {
43482
- if (!doc2) return null;
43483
- const tableBlocks = blocks.filter((b2) => b2.kind === "table");
43484
- const targetTableIndex = tableBlocks.findIndex((b2) => b2.id === tableHit.block.id);
43485
- if (targetTableIndex === -1) return null;
43486
- let tablePos = null;
43487
- let currentTableIndex = 0;
43488
- doc2.descendants((node, pos) => {
43489
- if (node.type.name === "table") {
43490
- if (currentTableIndex === targetTableIndex) {
43491
- tablePos = pos;
43492
- return false;
43493
- }
43494
- currentTableIndex++;
43495
- }
43496
- return true;
43497
- });
43498
- return tablePos;
44245
+ function hasFieldAnnotationData(event) {
44246
+ if (!event.dataTransfer) return false;
44247
+ const types = event.dataTransfer.types;
44248
+ return types.includes(INTERNAL_MIME_TYPE) || types.includes(FIELD_ANNOTATION_DATA_TYPE);
43499
44249
  }
43500
- function shouldUseCellSelection(currentTableHit, cellAnchor, cellDragMode) {
43501
- if (!cellAnchor) return false;
43502
- if (!currentTableHit) return cellDragMode === "active";
43503
- if (currentTableHit.block.id !== cellAnchor.tableBlockId) {
43504
- return cellDragMode === "active";
43505
- }
43506
- const sameCell = currentTableHit.cellRowIndex === cellAnchor.cellRowIndex && currentTableHit.cellColIndex === cellAnchor.cellColIndex;
43507
- if (!sameCell) {
43508
- return true;
43509
- }
43510
- return cellDragMode === "active";
44250
+ function isInternalDrag(event) {
44251
+ return event.dataTransfer?.types?.includes(INTERNAL_MIME_TYPE) ?? false;
43511
44252
  }
43512
- function hitTestTable(layout, blocks, measures, normalizedX, normalizedY, configuredPageHeight, pageGapFallback, geometryHelper) {
43513
- if (!layout) {
44253
+ function extractDragData(event) {
44254
+ if (!event.dataTransfer) return null;
44255
+ let jsonData = event.dataTransfer.getData(INTERNAL_MIME_TYPE);
44256
+ if (!jsonData) {
44257
+ jsonData = event.dataTransfer.getData(FIELD_ANNOTATION_DATA_TYPE);
44258
+ }
44259
+ if (!jsonData) return null;
44260
+ try {
44261
+ const parsed = JSON.parse(jsonData);
44262
+ return parsed.sourceField ?? parsed.attributes ?? parsed;
44263
+ } catch {
43514
44264
  return null;
43515
44265
  }
43516
- let pageY = 0;
43517
- let pageHit = null;
43518
- if (geometryHelper) {
43519
- const idx = geometryHelper.getPageIndexAtY(normalizedY) ?? geometryHelper.getNearestPageIndex(normalizedY);
43520
- if (idx != null && layout.pages[idx]) {
43521
- pageHit = { pageIndex: idx, page: layout.pages[idx] };
43522
- pageY = geometryHelper.getPageTop(idx);
43523
- }
44266
+ }
44267
+ class DragDropManager {
44268
+ #deps = null;
44269
+ // Bound handlers for cleanup
44270
+ #boundHandleDragStart = null;
44271
+ #boundHandleDragOver = null;
44272
+ #boundHandleDrop = null;
44273
+ #boundHandleDragEnd = null;
44274
+ #boundHandleDragLeave = null;
44275
+ #boundHandleWindowDragOver = null;
44276
+ #boundHandleWindowDrop = null;
44277
+ // ==========================================================================
44278
+ // Setup
44279
+ // ==========================================================================
44280
+ setDependencies(deps) {
44281
+ this.#deps = deps;
43524
44282
  }
43525
- if (!pageHit) {
43526
- const gap = layout.pageGap ?? pageGapFallback;
43527
- for (let i = 0; i < layout.pages.length; i++) {
43528
- const page = layout.pages[i];
43529
- const pageHeight = page.size?.h ?? configuredPageHeight;
43530
- if (normalizedY >= pageY && normalizedY < pageY + pageHeight) {
43531
- pageHit = { pageIndex: i, page };
43532
- break;
43533
- }
43534
- pageY += pageHeight + gap;
43535
- }
44283
+ bind() {
44284
+ if (!this.#deps) return;
44285
+ const viewportHost = this.#deps.getViewportHost();
44286
+ const painterHost = this.#deps.getPainterHost();
44287
+ this.#boundHandleDragStart = this.#handleDragStart.bind(this);
44288
+ this.#boundHandleDragOver = this.#handleDragOver.bind(this);
44289
+ this.#boundHandleDrop = this.#handleDrop.bind(this);
44290
+ this.#boundHandleDragEnd = this.#handleDragEnd.bind(this);
44291
+ this.#boundHandleDragLeave = this.#handleDragLeave.bind(this);
44292
+ this.#boundHandleWindowDragOver = this.#handleWindowDragOver.bind(this);
44293
+ this.#boundHandleWindowDrop = this.#handleWindowDrop.bind(this);
44294
+ painterHost.addEventListener("dragstart", this.#boundHandleDragStart);
44295
+ painterHost.addEventListener("dragend", this.#boundHandleDragEnd);
44296
+ painterHost.addEventListener("dragleave", this.#boundHandleDragLeave);
44297
+ viewportHost.addEventListener("dragover", this.#boundHandleDragOver);
44298
+ viewportHost.addEventListener("drop", this.#boundHandleDrop);
44299
+ window.addEventListener("dragover", this.#boundHandleWindowDragOver, false);
44300
+ window.addEventListener("drop", this.#boundHandleWindowDrop, false);
44301
+ }
44302
+ unbind() {
44303
+ if (!this.#deps) return;
44304
+ const viewportHost = this.#deps.getViewportHost();
44305
+ const painterHost = this.#deps.getPainterHost();
44306
+ if (this.#boundHandleDragStart) {
44307
+ painterHost.removeEventListener("dragstart", this.#boundHandleDragStart);
44308
+ }
44309
+ if (this.#boundHandleDragEnd) {
44310
+ painterHost.removeEventListener("dragend", this.#boundHandleDragEnd);
44311
+ }
44312
+ if (this.#boundHandleDragLeave) {
44313
+ painterHost.removeEventListener("dragleave", this.#boundHandleDragLeave);
44314
+ }
44315
+ if (this.#boundHandleDragOver) {
44316
+ viewportHost.removeEventListener("dragover", this.#boundHandleDragOver);
44317
+ }
44318
+ if (this.#boundHandleDrop) {
44319
+ viewportHost.removeEventListener("drop", this.#boundHandleDrop);
44320
+ }
44321
+ if (this.#boundHandleWindowDragOver) {
44322
+ window.removeEventListener("dragover", this.#boundHandleWindowDragOver, false);
44323
+ }
44324
+ if (this.#boundHandleWindowDrop) {
44325
+ window.removeEventListener("drop", this.#boundHandleWindowDrop, false);
44326
+ }
44327
+ this.#boundHandleDragStart = null;
44328
+ this.#boundHandleDragOver = null;
44329
+ this.#boundHandleDrop = null;
44330
+ this.#boundHandleDragEnd = null;
44331
+ this.#boundHandleDragLeave = null;
44332
+ this.#boundHandleWindowDragOver = null;
44333
+ this.#boundHandleWindowDrop = null;
43536
44334
  }
43537
- if (!pageHit) {
43538
- return null;
44335
+ destroy() {
44336
+ this.unbind();
44337
+ this.#deps = null;
43539
44338
  }
43540
- const pageRelativeY = normalizedY - pageY;
43541
- const point = { x: normalizedX, y: pageRelativeY };
43542
- return hitTestTableFragment(pageHit, blocks, measures, point);
43543
- }
43544
- function isValidFieldAnnotationAttributes(attrs) {
43545
- if (!attrs || typeof attrs !== "object") return false;
43546
- const a = attrs;
43547
- return typeof a.fieldId === "string" && typeof a.fieldType === "string" && typeof a.displayLabel === "string" && typeof a.type === "string";
43548
- }
43549
- const FIELD_ANNOTATION_DATA_TYPE = "fieldAnnotation";
43550
- function setupInternalFieldAnnotationDragHandlers({
43551
- painterHost,
43552
- getActiveEditor,
43553
- hitTest,
43554
- scheduleSelectionUpdate
43555
- }) {
43556
- return createDragHandler(painterHost, {
43557
- onDragOver: (event) => {
43558
- if (!event.hasFieldAnnotation || event.event.clientX === 0) {
43559
- return;
43560
- }
43561
- const activeEditor = getActiveEditor();
43562
- if (!activeEditor?.isEditable) {
43563
- return;
43564
- }
43565
- const hit = hitTest(event.clientX, event.clientY);
43566
- const doc2 = activeEditor.state?.doc;
43567
- if (!hit || !doc2) {
43568
- return;
43569
- }
43570
- const pos = Math.min(Math.max(hit.pos, 1), doc2.content.size);
43571
- const currentSelection = activeEditor.state.selection;
43572
- if (currentSelection instanceof superEditor_converter.TextSelection && currentSelection.from === pos && currentSelection.to === pos) {
43573
- return;
43574
- }
43575
- try {
43576
- const tr = activeEditor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, pos)).setMeta("addToHistory", false);
43577
- activeEditor.view?.dispatch(tr);
43578
- scheduleSelectionUpdate();
43579
- } catch {
43580
- }
43581
- },
43582
- onDrop: (event) => {
43583
- event.event.preventDefault();
43584
- event.event.stopPropagation();
43585
- if (event.pmPosition === null) {
43586
- return;
43587
- }
43588
- const activeEditor = getActiveEditor();
43589
- const { state, view } = activeEditor;
43590
- if (!state || !view) {
43591
- return;
43592
- }
43593
- const fieldId = event.data.fieldId;
43594
- if (fieldId) {
43595
- const targetPos = event.pmPosition;
43596
- const pmStart = event.data.pmStart;
43597
- let sourceStart = null;
43598
- let sourceEnd = null;
43599
- let sourceNode = null;
43600
- if (pmStart != null) {
43601
- const nodeAt = state.doc.nodeAt(pmStart);
43602
- if (nodeAt?.type?.name === "fieldAnnotation") {
43603
- sourceStart = pmStart;
43604
- sourceEnd = pmStart + nodeAt.nodeSize;
43605
- sourceNode = nodeAt;
43606
- }
43607
- }
43608
- if (sourceStart == null || sourceEnd == null || !sourceNode) {
43609
- state.doc.descendants((node, pos) => {
43610
- if (node.type.name === "fieldAnnotation" && node.attrs.fieldId === fieldId) {
43611
- sourceStart = pos;
43612
- sourceEnd = pos + node.nodeSize;
43613
- sourceNode = node;
43614
- return false;
43615
- }
43616
- return true;
43617
- });
43618
- }
43619
- if (sourceStart === null || sourceEnd === null || !sourceNode) {
43620
- return;
43621
- }
43622
- if (targetPos >= sourceStart && targetPos <= sourceEnd) {
43623
- return;
43624
- }
43625
- const tr = state.tr;
43626
- tr.delete(sourceStart, sourceEnd);
43627
- const mappedTarget = tr.mapping.map(targetPos);
43628
- if (mappedTarget < 0 || mappedTarget > tr.doc.content.size) {
43629
- return;
43630
- }
43631
- tr.insert(mappedTarget, sourceNode);
43632
- tr.setMeta("uiEvent", "drop");
43633
- view.dispatch(tr);
43634
- return;
43635
- }
43636
- const attrs = event.data.attributes;
43637
- if (attrs && isValidFieldAnnotationAttributes(attrs)) {
43638
- const inserted = activeEditor.commands?.addFieldAnnotation?.(event.pmPosition, attrs, true);
43639
- if (inserted) {
43640
- scheduleSelectionUpdate();
43641
- }
43642
- return;
43643
- }
43644
- activeEditor.emit("fieldAnnotationDropped", {
43645
- sourceField: event.data,
43646
- editor: activeEditor,
43647
- coordinates: { pos: event.pmPosition }
43648
- });
43649
- }
43650
- });
43651
- }
43652
- function createExternalFieldAnnotationDragOverHandler({
43653
- getActiveEditor,
43654
- hitTest,
43655
- scheduleSelectionUpdate
43656
- }) {
43657
- return (event) => {
43658
- const activeEditor = getActiveEditor();
43659
- if (!activeEditor?.isEditable) {
44339
+ // ==========================================================================
44340
+ // Event Handlers
44341
+ // ==========================================================================
44342
+ /**
44343
+ * Handle dragstart for internal field annotations.
44344
+ */
44345
+ #handleDragStart(event) {
44346
+ const target = event.target;
44347
+ if (!target?.dataset?.draggable || target.dataset.draggable !== "true") {
43660
44348
  return;
43661
44349
  }
43662
- event.preventDefault();
44350
+ const data = extractFieldAnnotationData(target);
43663
44351
  if (event.dataTransfer) {
43664
- event.dataTransfer.dropEffect = "copy";
44352
+ const jsonData = JSON.stringify({
44353
+ attributes: data.attributes,
44354
+ sourceField: data
44355
+ });
44356
+ event.dataTransfer.setData(INTERNAL_MIME_TYPE, jsonData);
44357
+ event.dataTransfer.setData(FIELD_ANNOTATION_DATA_TYPE, jsonData);
44358
+ event.dataTransfer.setData("text/plain", data.displayLabel ?? "Field Annotation");
44359
+ event.dataTransfer.setDragImage(target, 0, 0);
44360
+ event.dataTransfer.effectAllowed = "move";
43665
44361
  }
43666
- const dt = event.dataTransfer;
43667
- const hasFieldAnnotation = dt?.types?.includes(FIELD_ANNOTATION_DATA_TYPE) || Boolean(dt?.getData?.(FIELD_ANNOTATION_DATA_TYPE));
43668
- if (!hasFieldAnnotation) {
43669
- return;
44362
+ }
44363
+ /**
44364
+ * Handle dragover - update cursor position to show drop location.
44365
+ */
44366
+ #handleDragOver(event) {
44367
+ if (!this.#deps) return;
44368
+ if (!hasFieldAnnotationData(event)) return;
44369
+ const activeEditor = this.#deps.getActiveEditor();
44370
+ if (!activeEditor?.isEditable) return;
44371
+ event.preventDefault();
44372
+ if (event.dataTransfer) {
44373
+ event.dataTransfer.dropEffect = isInternalDrag(event) ? "move" : "copy";
43670
44374
  }
43671
- const hit = hitTest(event.clientX, event.clientY);
44375
+ const hit = this.#deps.hitTest(event.clientX, event.clientY);
43672
44376
  const doc2 = activeEditor.state?.doc;
43673
- if (!hit || !doc2) {
43674
- return;
43675
- }
44377
+ if (!hit || !doc2) return;
43676
44378
  const pos = Math.min(Math.max(hit.pos, 1), doc2.content.size);
43677
44379
  const currentSelection = activeEditor.state.selection;
43678
- const isSameCursor = currentSelection instanceof superEditor_converter.TextSelection && currentSelection.from === pos && currentSelection.to === pos;
43679
- if (isSameCursor) {
44380
+ if (currentSelection instanceof superEditor_converter.TextSelection && currentSelection.from === pos && currentSelection.to === pos) {
43680
44381
  return;
43681
44382
  }
43682
44383
  try {
43683
44384
  const tr = activeEditor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, pos)).setMeta("addToHistory", false);
43684
44385
  activeEditor.view?.dispatch(tr);
43685
- scheduleSelectionUpdate();
43686
- } catch (error) {
43687
- if (vue.process$1.env.NODE_ENV === "development") {
43688
- console.debug("[PresentationEditor] Drag position update skipped:", error);
43689
- }
43690
- }
43691
- };
43692
- }
43693
- function createExternalFieldAnnotationDropHandler({
43694
- getActiveEditor,
43695
- hitTest,
43696
- scheduleSelectionUpdate
43697
- }) {
43698
- return (event) => {
43699
- const activeEditor = getActiveEditor();
43700
- if (!activeEditor?.isEditable) {
43701
- return;
43702
- }
43703
- if (event.dataTransfer?.types?.includes("application/x-field-annotation")) {
43704
- return;
44386
+ this.#deps.scheduleSelectionUpdate();
44387
+ } catch {
43705
44388
  }
44389
+ }
44390
+ /**
44391
+ * Handle drop - either move internal annotation or insert external one.
44392
+ */
44393
+ #handleDrop(event) {
44394
+ if (!this.#deps) return;
44395
+ if (!hasFieldAnnotationData(event)) return;
43706
44396
  event.preventDefault();
43707
44397
  event.stopPropagation();
43708
- const fieldAnnotationData = event.dataTransfer?.getData(FIELD_ANNOTATION_DATA_TYPE);
43709
- if (!fieldAnnotationData) {
44398
+ const activeEditor = this.#deps.getActiveEditor();
44399
+ if (!activeEditor?.isEditable) return;
44400
+ const { state, view } = activeEditor;
44401
+ if (!state || !view) return;
44402
+ const hit = this.#deps.hitTest(event.clientX, event.clientY);
44403
+ const fallbackPos = state.selection?.from ?? state.doc?.content.size ?? null;
44404
+ const dropPos = hit?.pos ?? fallbackPos;
44405
+ if (dropPos == null) return;
44406
+ if (isInternalDrag(event)) {
44407
+ this.#handleInternalDrop(event, dropPos);
43710
44408
  return;
43711
44409
  }
43712
- const hit = hitTest(event.clientX, event.clientY);
43713
- const selection = activeEditor.state?.selection;
43714
- const fallbackPos = selection?.from ?? activeEditor.state?.doc?.content.size ?? null;
43715
- const pos = hit?.pos ?? fallbackPos;
43716
- if (pos == null) {
43717
- return;
44410
+ this.#handleExternalDrop(event, dropPos);
44411
+ }
44412
+ /**
44413
+ * Handle internal drop - move field annotation within document.
44414
+ */
44415
+ #handleInternalDrop(event, targetPos) {
44416
+ if (!this.#deps) return;
44417
+ const activeEditor = this.#deps.getActiveEditor();
44418
+ const { state, view } = activeEditor;
44419
+ if (!state || !view) return;
44420
+ const data = extractDragData(event);
44421
+ if (!data?.fieldId) return;
44422
+ const pmStart = data.pmStart;
44423
+ let sourceStart = null;
44424
+ let sourceEnd = null;
44425
+ let sourceNode = null;
44426
+ if (pmStart != null) {
44427
+ const nodeAt = state.doc.nodeAt(pmStart);
44428
+ if (nodeAt?.type?.name === "fieldAnnotation") {
44429
+ sourceStart = pmStart;
44430
+ sourceEnd = pmStart + nodeAt.nodeSize;
44431
+ sourceNode = nodeAt;
44432
+ }
44433
+ }
44434
+ if (sourceStart == null || sourceEnd == null || !sourceNode) {
44435
+ state.doc.descendants((node, pos) => {
44436
+ if (node.type.name === "fieldAnnotation" && node.attrs.fieldId === data.fieldId) {
44437
+ sourceStart = pos;
44438
+ sourceEnd = pos + node.nodeSize;
44439
+ sourceNode = node;
44440
+ return false;
44441
+ }
44442
+ return true;
44443
+ });
43718
44444
  }
44445
+ if (sourceStart === null || sourceEnd === null || !sourceNode) return;
44446
+ if (targetPos >= sourceStart && targetPos <= sourceEnd) return;
44447
+ const tr = state.tr;
44448
+ tr.delete(sourceStart, sourceEnd);
44449
+ const mappedTarget = tr.mapping.map(targetPos);
44450
+ if (mappedTarget < 0 || mappedTarget > tr.doc.content.size) return;
44451
+ tr.insert(mappedTarget, sourceNode);
44452
+ tr.setMeta("uiEvent", "drop");
44453
+ view.dispatch(tr);
44454
+ }
44455
+ /**
44456
+ * Handle external drop - insert new field annotation.
44457
+ */
44458
+ #handleExternalDrop(event, pos) {
44459
+ if (!this.#deps) return;
44460
+ const activeEditor = this.#deps.getActiveEditor();
44461
+ const fieldAnnotationData = event.dataTransfer?.getData(FIELD_ANNOTATION_DATA_TYPE);
44462
+ if (!fieldAnnotationData) return;
43719
44463
  let parsedData = null;
43720
44464
  try {
43721
44465
  parsedData = JSON.parse(fieldAnnotationData);
@@ -43726,7 +44470,7 @@ function createExternalFieldAnnotationDropHandler({
43726
44470
  activeEditor.emit?.("fieldAnnotationDropped", {
43727
44471
  sourceField,
43728
44472
  editor: activeEditor,
43729
- coordinates: hit,
44473
+ coordinates: this.#deps.hitTest(event.clientX, event.clientY),
43730
44474
  pos
43731
44475
  });
43732
44476
  if (attributes && isValidFieldAnnotationAttributes(attributes)) {
@@ -43736,14 +44480,49 @@ function createExternalFieldAnnotationDropHandler({
43736
44480
  if (tr) {
43737
44481
  activeEditor.view?.dispatch(tr);
43738
44482
  }
43739
- scheduleSelectionUpdate();
44483
+ this.#deps.scheduleSelectionUpdate();
43740
44484
  }
43741
44485
  const editorDom = activeEditor.view?.dom;
43742
44486
  if (editorDom) {
43743
44487
  editorDom.focus();
43744
44488
  activeEditor.view?.focus();
43745
44489
  }
43746
- };
44490
+ }
44491
+ #handleDragEnd(_event) {
44492
+ this.#deps?.getPainterHost()?.classList.remove("drag-over");
44493
+ }
44494
+ #handleDragLeave(event) {
44495
+ const painterHost = this.#deps?.getPainterHost();
44496
+ if (!painterHost) return;
44497
+ const relatedTarget = event.relatedTarget;
44498
+ if (!relatedTarget || !painterHost.contains(relatedTarget)) {
44499
+ painterHost.classList.remove("drag-over");
44500
+ }
44501
+ }
44502
+ /**
44503
+ * Window-level dragover to allow drops on overlay elements.
44504
+ */
44505
+ #handleWindowDragOver(event) {
44506
+ if (!hasFieldAnnotationData(event)) return;
44507
+ const viewportHost = this.#deps?.getViewportHost();
44508
+ const target = event.target;
44509
+ if (viewportHost?.contains(target)) return;
44510
+ event.preventDefault();
44511
+ if (event.dataTransfer) {
44512
+ event.dataTransfer.dropEffect = isInternalDrag(event) ? "move" : "copy";
44513
+ }
44514
+ this.#handleDragOver(event);
44515
+ }
44516
+ /**
44517
+ * Window-level drop to catch drops on overlay elements.
44518
+ */
44519
+ #handleWindowDrop(event) {
44520
+ if (!hasFieldAnnotationData(event)) return;
44521
+ const viewportHost = this.#deps?.getViewportHost();
44522
+ const target = event.target;
44523
+ if (viewportHost?.contains(target)) return;
44524
+ this.#handleDrop(event);
44525
+ }
43747
44526
  }
43748
44527
  var SectionType = /* @__PURE__ */ ((SectionType2) => {
43749
44528
  SectionType2["CONTINUOUS"] = "continuous";
@@ -45797,16 +46576,17 @@ const normalizeParagraphSpacing = (value) => {
45797
46576
  const afterRaw = pickNumber(source.after);
45798
46577
  const lineRaw = pickNumber(source.line);
45799
46578
  const lineRule = normalizeLineRule(source.lineRule);
46579
+ const resolvedLineRule = lineRule ?? (lineRaw != null ? "auto" : void 0);
45800
46580
  const beforeAutospacing = toBooleanFlag(source.beforeAutospacing ?? source.beforeAutoSpacing);
45801
46581
  const afterAutospacing = toBooleanFlag(source.afterAutospacing ?? source.afterAutoSpacing);
45802
46582
  const contextualSpacing = toBooleanFlag(source.contextualSpacing);
45803
46583
  const before = beforeRaw != null ? twipsToPx$1(beforeRaw) : pickNumber(source.lineSpaceBefore);
45804
46584
  const after = afterRaw != null ? twipsToPx$1(afterRaw) : pickNumber(source.lineSpaceAfter);
45805
- const line = normalizeLineValue(lineRaw, lineRule);
46585
+ const line = normalizeLineValue(lineRaw, resolvedLineRule);
45806
46586
  if (before != null) spacing.before = before;
45807
46587
  if (after != null) spacing.after = after;
45808
46588
  if (line != null) spacing.line = line;
45809
- if (lineRule) spacing.lineRule = lineRule;
46589
+ if (resolvedLineRule) spacing.lineRule = resolvedLineRule;
45810
46590
  if (beforeAutospacing != null) spacing.beforeAutospacing = beforeAutospacing;
45811
46591
  if (afterAutospacing != null) spacing.afterAutospacing = afterAutospacing;
45812
46592
  if (contextualSpacing != null) spacing.contextualSpacing = contextualSpacing;
@@ -55622,8 +56402,6 @@ const DEFAULT_MARGINS = { top: 72, right: 72, bottom: 72, left: 72 };
55622
56402
  const DEFAULT_VIRTUALIZED_PAGE_GAP = 72;
55623
56403
  const DEFAULT_PAGE_GAP = 24;
55624
56404
  const DEFAULT_HORIZONTAL_PAGE_GAP = 20;
55625
- const MULTI_CLICK_TIME_THRESHOLD_MS = 400;
55626
- const MULTI_CLICK_DISTANCE_THRESHOLD_PX = 5;
55627
56405
  const HEADER_FOOTER_INIT_BUDGET_MS = 200;
55628
56406
  const MAX_ZOOM_WARNING_THRESHOLD = 10;
55629
56407
  const MAX_SELECTION_RECTS_PER_USER = 100;
@@ -55679,7 +56457,7 @@ class PresentationEditor extends EventEmitter {
55679
56457
  #layoutState = { blocks: [], measures: [], layout: null, bookmarks: /* @__PURE__ */ new Map() };
55680
56458
  #domPainter = null;
55681
56459
  #pageGeometryHelper = null;
55682
- #dragHandlerCleanup = null;
56460
+ #dragDropManager = null;
55683
56461
  #layoutError = null;
55684
56462
  #layoutErrorState = "healthy";
55685
56463
  #errorBanner = null;
@@ -55696,9 +56474,6 @@ class PresentationEditor extends EventEmitter {
55696
56474
  #htmlAnnotationMeasureAttempts = 0;
55697
56475
  #domPositionIndex = new DomPositionIndex();
55698
56476
  #domIndexObserverManager = null;
55699
- #debugLastPointer = null;
55700
- #debugLastHit = null;
55701
- #pendingMarginClick = null;
55702
56477
  #rafHandle = null;
55703
56478
  #editorListeners = [];
55704
56479
  #sectionMetadata = [];
@@ -55715,25 +56490,7 @@ class PresentationEditor extends EventEmitter {
55715
56490
  #ariaLiveRegion = null;
55716
56491
  #a11ySelectionAnnounceTimeout = null;
55717
56492
  #a11yLastAnnouncedSelectionKey = null;
55718
- #clickCount = 0;
55719
- #lastClickTime = 0;
55720
- #lastClickPosition = { x: 0, y: 0 };
55721
- #lastSelectedImageBlockId = null;
55722
56493
  #lastSelectedFieldAnnotation = null;
55723
- // Drag selection state
55724
- #dragAnchor = null;
55725
- #dragAnchorPageIndex = null;
55726
- #isDragging = false;
55727
- #dragExtensionMode = "char";
55728
- #dragLastPointer = null;
55729
- #dragLastRawHit = null;
55730
- #dragUsedPageNotMountedFallback = false;
55731
- #suppressFocusInFromDraggable = false;
55732
- // Cell selection drag state
55733
- // Tracks cell-specific context when drag starts in a table for multi-cell selection
55734
- #cellAnchor = null;
55735
- /** Cell drag mode state machine: 'none' = not in table, 'pending' = in table but haven't crossed cell boundary, 'active' = crossed cell boundary */
55736
- #cellDragMode = "none";
55737
56494
  // Remote cursor/presence state management
55738
56495
  /** Manager for remote cursor rendering and awareness subscriptions */
55739
56496
  #remoteCursorManager = null;
@@ -55741,6 +56498,9 @@ class PresentationEditor extends EventEmitter {
55741
56498
  #remoteCursorOverlay = null;
55742
56499
  /** DOM element for rendering local selection/caret (dual-layer overlay architecture) */
55743
56500
  #localSelectionLayer = null;
56501
+ // Editor input management
56502
+ /** Manager for pointer events, focus, drag selection, and click handling */
56503
+ #editorInputManager = null;
55744
56504
  constructor(options) {
55745
56505
  super();
55746
56506
  if (!options?.element) {
@@ -55949,6 +56709,7 @@ class PresentationEditor extends EventEmitter {
55949
56709
  this.#setupHeaderFooterSession();
55950
56710
  this.#applyZoom();
55951
56711
  this.#setupEditorListeners();
56712
+ this.#initializeEditorInputManager();
55952
56713
  this.#setupPointerHandlers();
55953
56714
  this.#setupDragHandlers();
55954
56715
  this.#setupInputBridge();
@@ -56873,8 +57634,8 @@ class PresentationEditor extends EventEmitter {
56873
57634
  docEpoch: this.#epochMapper.getCurrentEpoch(),
56874
57635
  layoutEpoch: this.#layoutEpoch,
56875
57636
  selection,
56876
- lastPointer: this.#debugLastPointer,
56877
- lastHit: this.#debugLastHit
57637
+ lastPointer: this.#editorInputManager?.debugLastPointer ?? null,
57638
+ lastHit: this.#editorInputManager?.debugLastHit ?? null
56878
57639
  });
56879
57640
  } catch {
56880
57641
  }
@@ -57267,15 +58028,12 @@ class PresentationEditor extends EventEmitter {
57267
58028
  this.#editorListeners = [];
57268
58029
  this.#domIndexObserverManager?.destroy();
57269
58030
  this.#domIndexObserverManager = null;
57270
- this.#viewportHost?.removeEventListener("pointerdown", this.#handlePointerDown);
57271
- this.#viewportHost?.removeEventListener("dblclick", this.#handleDoubleClick);
57272
- this.#viewportHost?.removeEventListener("pointermove", this.#handlePointerMove);
57273
- this.#viewportHost?.removeEventListener("pointerup", this.#handlePointerUp);
57274
- this.#viewportHost?.removeEventListener("pointerleave", this.#handlePointerLeave);
57275
- this.#viewportHost?.removeEventListener("dragover", this.#handleDragOver);
57276
- this.#viewportHost?.removeEventListener("drop", this.#handleDrop);
57277
- this.#visibleHost?.removeEventListener("keydown", this.#handleKeyDown);
57278
- this.#visibleHost?.removeEventListener("focusin", this.#handleVisibleHostFocusIn);
58031
+ if (this.#editorInputManager) {
58032
+ safeCleanup(() => {
58033
+ this.#editorInputManager?.destroy();
58034
+ this.#editorInputManager = null;
58035
+ }, "Editor input manager");
58036
+ }
57279
58037
  this.#inputBridge?.notifyTargetChanged();
57280
58038
  this.#inputBridge?.destroy();
57281
58039
  this.#inputBridge = null;
@@ -57283,7 +58041,6 @@ class PresentationEditor extends EventEmitter {
57283
58041
  clearTimeout(this.#a11ySelectionAnnounceTimeout);
57284
58042
  this.#a11ySelectionAnnounceTimeout = null;
57285
58043
  }
57286
- this.#clearCellAnchor();
57287
58044
  if (this.#options?.documentId) {
57288
58045
  PresentationEditor.#instances.delete(this.#options.documentId);
57289
58046
  }
@@ -57293,8 +58050,8 @@ class PresentationEditor extends EventEmitter {
57293
58050
  }, "Header/footer session manager");
57294
58051
  this.#domPainter = null;
57295
58052
  this.#pageGeometryHelper = null;
57296
- this.#dragHandlerCleanup?.();
57297
- this.#dragHandlerCleanup = null;
58053
+ this.#dragDropManager?.destroy();
58054
+ this.#dragDropManager = null;
57298
58055
  this.#selectionOverlay?.remove();
57299
58056
  this.#painterHost?.remove();
57300
58057
  this.#hiddenHost?.remove();
@@ -57341,7 +58098,7 @@ class PresentationEditor extends EventEmitter {
57341
58098
  }
57342
58099
  if (transaction?.docChanged) {
57343
58100
  this.#updateLocalAwarenessCursor();
57344
- this.#clearCellAnchor();
58101
+ this.#editorInputManager?.clearCellAnchor();
57345
58102
  }
57346
58103
  };
57347
58104
  const handleSelection = () => {
@@ -57445,31 +58202,75 @@ class PresentationEditor extends EventEmitter {
57445
58202
  #renderRemoteCursors() {
57446
58203
  this.#remoteCursorManager?.render(this.#getRemoteCursorRenderDeps());
57447
58204
  }
58205
+ /**
58206
+ * Initialize the EditorInputManager with dependencies and callbacks.
58207
+ * @private
58208
+ */
58209
+ #initializeEditorInputManager() {
58210
+ this.#editorInputManager = new EditorInputManager();
58211
+ this.#editorInputManager.setDependencies({
58212
+ getActiveEditor: () => this.getActiveEditor(),
58213
+ getEditor: () => this.#editor,
58214
+ getLayoutState: () => this.#layoutState,
58215
+ getEpochMapper: () => this.#epochMapper,
58216
+ getViewportHost: () => this.#viewportHost,
58217
+ getVisibleHost: () => this.#visibleHost,
58218
+ getHeaderFooterSession: () => this.#headerFooterSession,
58219
+ getPageGeometryHelper: () => this.#pageGeometryHelper,
58220
+ getZoom: () => this.#layoutOptions.zoom ?? 1,
58221
+ isViewLocked: () => this.#isViewLocked(),
58222
+ getDocumentMode: () => this.#documentMode,
58223
+ getPageElement: (pageIndex) => this.#getPageElement(pageIndex),
58224
+ isSelectionAwareVirtualizationEnabled: () => this.#isSelectionAwareVirtualizationEnabled()
58225
+ });
58226
+ this.#editorInputManager.setCallbacks({
58227
+ scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate(),
58228
+ scheduleRerender: () => this.#scheduleRerender(),
58229
+ setPendingDocChange: () => {
58230
+ this.#pendingDocChange = true;
58231
+ },
58232
+ updateSelectionVirtualizationPins: (options) => this.#updateSelectionVirtualizationPins(options),
58233
+ scheduleA11ySelectionAnnouncement: (options) => this.#scheduleA11ySelectionAnnouncement(options),
58234
+ goToAnchor: (href) => this.goToAnchor(href),
58235
+ emit: (event, payload) => this.emit(event, payload),
58236
+ normalizeClientPoint: (clientX, clientY) => this.#normalizeClientPoint(clientX, clientY),
58237
+ hitTestHeaderFooterRegion: (x, y2) => this.#hitTestHeaderFooterRegion(x, y2),
58238
+ exitHeaderFooterMode: () => this.#exitHeaderFooterMode(),
58239
+ activateHeaderFooterRegion: (region) => this.#activateHeaderFooterRegion(region),
58240
+ createDefaultHeaderFooter: (region) => this.#createDefaultHeaderFooter(region),
58241
+ emitHeaderFooterEditBlocked: (reason) => this.#emitHeaderFooterEditBlocked(reason),
58242
+ findRegionForPage: (kind, pageIndex) => this.#findRegionForPage(kind, pageIndex),
58243
+ getCurrentPageIndex: () => this.#getCurrentPageIndex(),
58244
+ resolveDescriptorForRegion: (region) => this.#resolveDescriptorForRegion(region),
58245
+ updateSelectionDebugHud: () => this.#updateSelectionDebugHud(),
58246
+ clearHoverRegion: () => this.#clearHoverRegion(),
58247
+ renderHoverRegion: (region) => this.#renderHoverRegion(region),
58248
+ focusEditorAfterImageSelection: () => this.#focusEditorAfterImageSelection(),
58249
+ resolveFieldAnnotationSelectionFromElement: (el) => this.#resolveFieldAnnotationSelectionFromElement(el),
58250
+ computePendingMarginClick: (pointerId, x, y2) => this.#computePendingMarginClick(pointerId, x, y2),
58251
+ selectWordAt: (pos) => this.#selectWordAt(pos),
58252
+ selectParagraphAt: (pos) => this.#selectParagraphAt(pos),
58253
+ finalizeDragSelectionWithDom: (pointer, dragAnchor, dragMode) => this.#finalizeDragSelectionWithDom(pointer, dragAnchor, dragMode),
58254
+ hitTestTable: (x, y2) => this.#hitTestTable(x, y2)
58255
+ });
58256
+ }
57448
58257
  #setupPointerHandlers() {
57449
- this.#viewportHost.addEventListener("pointerdown", this.#handlePointerDown);
57450
- this.#viewportHost.addEventListener("dblclick", this.#handleDoubleClick);
57451
- this.#viewportHost.addEventListener("pointermove", this.#handlePointerMove);
57452
- this.#viewportHost.addEventListener("pointerup", this.#handlePointerUp);
57453
- this.#viewportHost.addEventListener("pointerleave", this.#handlePointerLeave);
57454
- this.#viewportHost.addEventListener("dragover", this.#handleDragOver);
57455
- this.#viewportHost.addEventListener("drop", this.#handleDrop);
57456
- this.#visibleHost.addEventListener("keydown", this.#handleKeyDown);
57457
- this.#visibleHost.addEventListener("focusin", this.#handleVisibleHostFocusIn);
58258
+ this.#editorInputManager?.bind();
57458
58259
  }
57459
58260
  /**
57460
- * Sets up drag and drop handlers for field annotations in the layout engine view.
57461
- * Uses the DragHandler from layout-bridge to handle drag events and map drop
57462
- * coordinates to ProseMirror positions.
58261
+ * Sets up drag and drop handlers for field annotations.
57463
58262
  */
57464
58263
  #setupDragHandlers() {
57465
- this.#dragHandlerCleanup?.();
57466
- this.#dragHandlerCleanup = null;
57467
- this.#dragHandlerCleanup = setupInternalFieldAnnotationDragHandlers({
57468
- painterHost: this.#painterHost,
58264
+ this.#dragDropManager?.destroy();
58265
+ this.#dragDropManager = new DragDropManager();
58266
+ this.#dragDropManager.setDependencies({
57469
58267
  getActiveEditor: () => this.getActiveEditor(),
57470
58268
  hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
57471
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
58269
+ scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate(),
58270
+ getViewportHost: () => this.#viewportHost,
58271
+ getPainterHost: () => this.#painterHost
57472
58272
  });
58273
+ this.#dragDropManager.bind();
57473
58274
  }
57474
58275
  /**
57475
58276
  * Focus the editor after image selection and schedule selection update.
@@ -57587,536 +58388,6 @@ class PresentationEditor extends EventEmitter {
57587
58388
  });
57588
58389
  this.#headerFooterSession.initialize();
57589
58390
  }
57590
- #handlePointerDown = (event) => {
57591
- if (event.button !== 0) {
57592
- return;
57593
- }
57594
- if (event.ctrlKey && navigator.platform.includes("Mac")) {
57595
- return;
57596
- }
57597
- this.#pendingMarginClick = null;
57598
- const target = event.target;
57599
- if (target?.closest?.(".superdoc-ruler-handle") != null) {
57600
- return;
57601
- }
57602
- const linkEl = target?.closest?.("a.superdoc-link");
57603
- if (linkEl) {
57604
- const href = linkEl.getAttribute("href") ?? "";
57605
- const isAnchorLink = href.startsWith("#") && href.length > 1;
57606
- const isTocLink = linkEl.closest(".superdoc-toc-entry") !== null;
57607
- if (isAnchorLink && isTocLink) {
57608
- event.preventDefault();
57609
- event.stopPropagation();
57610
- this.goToAnchor(href);
57611
- return;
57612
- }
57613
- event.preventDefault();
57614
- event.stopPropagation();
57615
- const linkClickEvent = new CustomEvent("superdoc-link-click", {
57616
- bubbles: true,
57617
- composed: true,
57618
- detail: {
57619
- href,
57620
- target: linkEl.getAttribute("target"),
57621
- rel: linkEl.getAttribute("rel"),
57622
- tooltip: linkEl.getAttribute("title"),
57623
- element: linkEl,
57624
- clientX: event.clientX,
57625
- clientY: event.clientY
57626
- }
57627
- });
57628
- linkEl.dispatchEvent(linkClickEvent);
57629
- return;
57630
- }
57631
- const annotationEl = target?.closest?.(".annotation[data-pm-start]");
57632
- const isDraggableAnnotation = target?.closest?.('[data-draggable="true"]') != null;
57633
- this.#suppressFocusInFromDraggable = isDraggableAnnotation;
57634
- if (annotationEl) {
57635
- if (!this.#editor.isEditable) {
57636
- return;
57637
- }
57638
- const resolved = this.#resolveFieldAnnotationSelectionFromElement(annotationEl);
57639
- if (resolved) {
57640
- try {
57641
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(this.#editor.state.doc, resolved.pos));
57642
- this.#editor.view?.dispatch(tr);
57643
- } catch {
57644
- }
57645
- this.#editor.emit("fieldAnnotationClicked", {
57646
- editor: this.#editor,
57647
- node: resolved.node,
57648
- nodePos: resolved.pos,
57649
- event,
57650
- currentTarget: annotationEl
57651
- });
57652
- }
57653
- return;
57654
- }
57655
- if (!this.#layoutState.layout) {
57656
- if (!isDraggableAnnotation) {
57657
- event.preventDefault();
57658
- }
57659
- if (document.activeElement instanceof HTMLElement) {
57660
- document.activeElement.blur();
57661
- }
57662
- const editorDom2 = this.#editor.view?.dom;
57663
- if (!editorDom2) {
57664
- return;
57665
- }
57666
- const validPos = this.#getFirstTextPosition();
57667
- const doc22 = this.#editor?.state?.doc;
57668
- if (doc22) {
57669
- try {
57670
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc22, validPos));
57671
- this.#editor.view?.dispatch(tr);
57672
- } catch (error) {
57673
- if (vue.process$1.env.NODE_ENV === "development") {
57674
- console.warn("[PresentationEditor] Failed to set selection to first text position:", error);
57675
- }
57676
- }
57677
- }
57678
- editorDom2.focus();
57679
- this.#editor.view?.focus();
57680
- this.#scheduleSelectionUpdate();
57681
- return;
57682
- }
57683
- const normalizedPoint = this.#normalizeClientPoint(event.clientX, event.clientY);
57684
- if (!normalizedPoint) {
57685
- return;
57686
- }
57687
- const { x, y: y2 } = normalizedPoint;
57688
- this.#debugLastPointer = { clientX: event.clientX, clientY: event.clientY, x, y: y2 };
57689
- const sessionMode = this.#headerFooterSession?.session?.mode ?? "body";
57690
- if (sessionMode !== "body") {
57691
- const activeEditorHost = this.#headerFooterSession?.overlayManager?.getActiveEditorHost?.();
57692
- const clickedInsideEditorHost = activeEditorHost && (activeEditorHost.contains(event.target) || activeEditorHost === event.target);
57693
- if (clickedInsideEditorHost) {
57694
- return;
57695
- }
57696
- const headerFooterRegion2 = this.#hitTestHeaderFooterRegion(x, y2);
57697
- if (!headerFooterRegion2) {
57698
- this.#exitHeaderFooterMode();
57699
- } else {
57700
- return;
57701
- }
57702
- }
57703
- const headerFooterRegion = this.#hitTestHeaderFooterRegion(x, y2);
57704
- if (headerFooterRegion) {
57705
- return;
57706
- }
57707
- const rawHit = clickToPosition(
57708
- this.#layoutState.layout,
57709
- this.#layoutState.blocks,
57710
- this.#layoutState.measures,
57711
- { x, y: y2 },
57712
- this.#viewportHost,
57713
- event.clientX,
57714
- event.clientY,
57715
- this.#pageGeometryHelper ?? void 0
57716
- );
57717
- const doc2 = this.#editor.state?.doc;
57718
- const mapped = rawHit && doc2 ? this.#epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1) : null;
57719
- if (mapped && !mapped.ok) {
57720
- debugLog("warn", "pointerdown mapping failed", mapped);
57721
- }
57722
- const hit = rawHit && doc2 && mapped?.ok ? { ...rawHit, pos: Math.max(0, Math.min(mapped.pos, doc2.content.size)), layoutEpoch: mapped.toEpoch } : null;
57723
- 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 };
57724
- this.#updateSelectionDebugHud();
57725
- if (!isDraggableAnnotation) {
57726
- event.preventDefault();
57727
- }
57728
- if (!rawHit) {
57729
- if (document.activeElement instanceof HTMLElement) {
57730
- document.activeElement.blur();
57731
- }
57732
- const editorDom2 = this.#editor.view?.dom;
57733
- if (editorDom2) {
57734
- const validPos = this.#getFirstTextPosition();
57735
- const doc22 = this.#editor?.state?.doc;
57736
- if (doc22) {
57737
- try {
57738
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc22, validPos));
57739
- this.#editor.view?.dispatch(tr);
57740
- } catch (error) {
57741
- if (vue.process$1.env.NODE_ENV === "development") {
57742
- console.warn("[PresentationEditor] Failed to set selection to first text position:", error);
57743
- }
57744
- }
57745
- }
57746
- editorDom2.focus();
57747
- this.#editor.view?.focus();
57748
- this.#scheduleSelectionUpdate();
57749
- }
57750
- return;
57751
- }
57752
- if (!hit || !doc2) {
57753
- this.#pendingDocChange = true;
57754
- this.#scheduleRerender();
57755
- return;
57756
- }
57757
- const fragmentHit = getFragmentAtPosition(
57758
- this.#layoutState.layout,
57759
- this.#layoutState.blocks,
57760
- this.#layoutState.measures,
57761
- rawHit.pos
57762
- );
57763
- const targetImg = event.target?.closest?.("img");
57764
- const imgPmStart = targetImg?.dataset?.pmStart ? Number(targetImg.dataset.pmStart) : null;
57765
- if (!Number.isNaN(imgPmStart) && imgPmStart != null) {
57766
- const doc22 = this.#editor.state.doc;
57767
- const imgLayoutEpochRaw = targetImg?.dataset?.layoutEpoch;
57768
- const imgLayoutEpoch = imgLayoutEpochRaw != null ? Number(imgLayoutEpochRaw) : NaN;
57769
- const rawLayoutEpoch = Number.isFinite(rawHit.layoutEpoch) ? rawHit.layoutEpoch : NaN;
57770
- const effectiveEpoch = Number.isFinite(imgLayoutEpoch) && Number.isFinite(rawLayoutEpoch) ? Math.max(imgLayoutEpoch, rawLayoutEpoch) : Number.isFinite(imgLayoutEpoch) ? imgLayoutEpoch : rawHit.layoutEpoch;
57771
- const mappedImg = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(imgPmStart, effectiveEpoch, 1);
57772
- if (!mappedImg.ok) {
57773
- debugLog("warn", "inline image mapping failed", mappedImg);
57774
- this.#pendingDocChange = true;
57775
- this.#scheduleRerender();
57776
- return;
57777
- }
57778
- const clampedImgPos = Math.max(0, Math.min(mappedImg.pos, doc22.content.size));
57779
- if (clampedImgPos < 0 || clampedImgPos >= doc22.content.size) {
57780
- if (vue.process$1.env.NODE_ENV === "development") {
57781
- console.warn(
57782
- `[PresentationEditor] Invalid position ${clampedImgPos} for inline image (document size: ${doc22.content.size})`
57783
- );
57784
- }
57785
- return;
57786
- }
57787
- const newSelectionId = `inline-${clampedImgPos}`;
57788
- if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== newSelectionId) {
57789
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
57790
- }
57791
- try {
57792
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(doc22, clampedImgPos));
57793
- this.#editor.view?.dispatch(tr);
57794
- const selector = `.superdoc-inline-image[data-pm-start="${imgPmStart}"]`;
57795
- const targetElement = this.#viewportHost.querySelector(selector);
57796
- this.emit("imageSelected", {
57797
- element: targetElement ?? targetImg,
57798
- blockId: null,
57799
- pmStart: clampedImgPos
57800
- });
57801
- this.#lastSelectedImageBlockId = newSelectionId;
57802
- } catch (error) {
57803
- if (vue.process$1.env.NODE_ENV === "development") {
57804
- console.warn(
57805
- `[PresentationEditor] Failed to create NodeSelection for inline image at position ${imgPmStart}:`,
57806
- error
57807
- );
57808
- }
57809
- }
57810
- this.#focusEditorAfterImageSelection();
57811
- return;
57812
- }
57813
- if (fragmentHit && (fragmentHit.fragment.kind === "image" || fragmentHit.fragment.kind === "drawing")) {
57814
- const doc22 = this.#editor.state.doc;
57815
- try {
57816
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.NodeSelection.create(doc22, hit.pos));
57817
- this.#editor.view?.dispatch(tr);
57818
- if (this.#lastSelectedImageBlockId && this.#lastSelectedImageBlockId !== fragmentHit.fragment.blockId) {
57819
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
57820
- }
57821
- if (fragmentHit.fragment.kind === "image") {
57822
- const targetElement = this.#viewportHost.querySelector(
57823
- `.superdoc-image-fragment[data-pm-start="${fragmentHit.fragment.pmStart}"]`
57824
- );
57825
- if (targetElement) {
57826
- this.emit("imageSelected", {
57827
- element: targetElement,
57828
- blockId: fragmentHit.fragment.blockId,
57829
- pmStart: fragmentHit.fragment.pmStart
57830
- });
57831
- this.#lastSelectedImageBlockId = fragmentHit.fragment.blockId;
57832
- }
57833
- }
57834
- } catch (error) {
57835
- if (vue.process$1.env.NODE_ENV === "development") {
57836
- console.warn("[PresentationEditor] Failed to create NodeSelection for atomic fragment:", error);
57837
- }
57838
- }
57839
- this.#focusEditorAfterImageSelection();
57840
- return;
57841
- }
57842
- if (this.#lastSelectedImageBlockId) {
57843
- this.emit("imageDeselected", { blockId: this.#lastSelectedImageBlockId });
57844
- this.#lastSelectedImageBlockId = null;
57845
- }
57846
- if (event.shiftKey && this.#editor.state.selection.$anchor) {
57847
- const anchor = this.#editor.state.selection.anchor;
57848
- const head = hit.pos;
57849
- const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
57850
- try {
57851
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(this.#editor.state.doc, selAnchor, selHead));
57852
- this.#editor.view?.dispatch(tr);
57853
- this.#scheduleSelectionUpdate();
57854
- } catch (error) {
57855
- console.warn("[SELECTION] Failed to extend selection on shift+click:", {
57856
- error,
57857
- anchor,
57858
- head,
57859
- selAnchor,
57860
- selHead,
57861
- mode: this.#dragExtensionMode
57862
- });
57863
- }
57864
- if (document.activeElement instanceof HTMLElement) {
57865
- document.activeElement.blur();
57866
- }
57867
- const editorDom2 = this.#editor.view?.dom;
57868
- if (editorDom2) {
57869
- editorDom2.focus();
57870
- this.#editor.view?.focus();
57871
- }
57872
- return;
57873
- }
57874
- const clickDepth = this.#registerPointerClick(event);
57875
- if (clickDepth === 1) {
57876
- this.#dragAnchor = hit.pos;
57877
- this.#dragAnchorPageIndex = hit.pageIndex;
57878
- this.#pendingMarginClick = this.#computePendingMarginClick(event.pointerId, x, y2);
57879
- const tableHit = this.#hitTestTable(x, y2);
57880
- if (tableHit) {
57881
- const tablePos = this.#getTablePosFromHit(tableHit);
57882
- if (tablePos !== null) {
57883
- this.#setCellAnchor(tableHit, tablePos);
57884
- }
57885
- } else {
57886
- this.#clearCellAnchor();
57887
- }
57888
- } else {
57889
- this.#pendingMarginClick = null;
57890
- }
57891
- this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x, y: y2 };
57892
- this.#dragLastRawHit = hit;
57893
- this.#dragUsedPageNotMountedFallback = false;
57894
- this.#isDragging = true;
57895
- if (clickDepth >= 3) {
57896
- this.#dragExtensionMode = "para";
57897
- } else if (clickDepth === 2) {
57898
- this.#dragExtensionMode = "word";
57899
- } else {
57900
- this.#dragExtensionMode = "char";
57901
- }
57902
- debugLog(
57903
- "verbose",
57904
- `Drag selection start ${JSON.stringify({
57905
- pointer: { clientX: event.clientX, clientY: event.clientY, x, y: y2 },
57906
- clickDepth,
57907
- extensionMode: this.#dragExtensionMode,
57908
- anchor: this.#dragAnchor,
57909
- anchorPageIndex: this.#dragAnchorPageIndex,
57910
- rawHit: rawHit ? {
57911
- pos: rawHit.pos,
57912
- pageIndex: rawHit.pageIndex,
57913
- blockId: rawHit.blockId,
57914
- lineIndex: rawHit.lineIndex,
57915
- layoutEpoch: rawHit.layoutEpoch
57916
- } : null,
57917
- mapped: mapped ? mapped.ok ? { ok: true, pos: mapped.pos, fromEpoch: mapped.fromEpoch, toEpoch: mapped.toEpoch } : {
57918
- ok: false,
57919
- reason: mapped.reason,
57920
- fromEpoch: mapped.fromEpoch,
57921
- toEpoch: mapped.toEpoch
57922
- } : null,
57923
- hit: hit ? { pos: hit.pos, pageIndex: hit.pageIndex, layoutEpoch: hit.layoutEpoch } : null
57924
- })}`
57925
- );
57926
- if (typeof this.#viewportHost.setPointerCapture === "function") {
57927
- this.#viewportHost.setPointerCapture(event.pointerId);
57928
- }
57929
- let handledByDepth = false;
57930
- const sessionModeForDepth = this.#headerFooterSession?.session?.mode ?? "body";
57931
- if (sessionModeForDepth === "body") {
57932
- const selectionPos = clickDepth >= 2 && this.#dragAnchor !== null ? this.#dragAnchor : hit.pos;
57933
- if (clickDepth >= 3) {
57934
- handledByDepth = this.#selectParagraphAt(selectionPos);
57935
- } else if (clickDepth === 2) {
57936
- handledByDepth = this.#selectWordAt(selectionPos);
57937
- }
57938
- }
57939
- if (!handledByDepth) {
57940
- try {
57941
- const doc22 = this.#editor.state.doc;
57942
- let nextSelection = superEditor_converter.TextSelection.create(doc22, hit.pos);
57943
- if (!nextSelection.$from.parent.inlineContent) {
57944
- nextSelection = superEditor_converter.Selection.near(doc22.resolve(hit.pos), 1);
57945
- }
57946
- const tr = this.#editor.state.tr.setSelection(nextSelection);
57947
- this.#editor.view?.dispatch(tr);
57948
- } catch {
57949
- }
57950
- }
57951
- this.#scheduleSelectionUpdate();
57952
- if (document.activeElement instanceof HTMLElement) {
57953
- document.activeElement.blur();
57954
- }
57955
- const editorDom = this.#editor.view?.dom;
57956
- if (!editorDom) {
57957
- return;
57958
- }
57959
- editorDom.focus();
57960
- this.#editor.view?.focus();
57961
- };
57962
- /**
57963
- * Finds the first valid text position in the document.
57964
- *
57965
- * Traverses the document tree to locate the first textblock node (paragraph, heading, etc.)
57966
- * and returns a position inside it. This is used when focusing the editor but no specific
57967
- * position is available (e.g., clicking outside text content or before layout is ready).
57968
- *
57969
- * @returns The position inside the first textblock, or 1 if no textblock is found
57970
- * @private
57971
- */
57972
- #getFirstTextPosition() {
57973
- return getFirstTextPosition(this.#editor?.state?.doc ?? null);
57974
- }
57975
- /**
57976
- * Registers a pointer click event and tracks multi-click sequences (double, triple).
57977
- *
57978
- * This method implements multi-click detection by tracking the timing and position
57979
- * of consecutive clicks. Clicks within 400ms and 5px of each other increment the
57980
- * click count, up to a maximum of 3 (single, double, triple).
57981
- *
57982
- * @param event - The mouse event from the pointer down handler
57983
- * @returns The current click count (1 = single, 2 = double, 3 = triple)
57984
- * @private
57985
- */
57986
- #registerPointerClick(event) {
57987
- const nextState = registerPointerClick(
57988
- event,
57989
- { clickCount: this.#clickCount, lastClickTime: this.#lastClickTime, lastClickPosition: this.#lastClickPosition },
57990
- {
57991
- timeThresholdMs: MULTI_CLICK_TIME_THRESHOLD_MS,
57992
- distanceThresholdPx: MULTI_CLICK_DISTANCE_THRESHOLD_PX,
57993
- maxClickCount: 3
57994
- }
57995
- );
57996
- this.#clickCount = nextState.clickCount;
57997
- this.#lastClickTime = nextState.lastClickTime;
57998
- this.#lastClickPosition = nextState.lastClickPosition;
57999
- return nextState.clickCount;
58000
- }
58001
- // ============================================================================
58002
- // Cell Selection Utilities
58003
- // ============================================================================
58004
- /**
58005
- * Gets the ProseMirror position at the start of a table cell from a table hit result.
58006
- *
58007
- * This method navigates the ProseMirror document structure to find the exact position where
58008
- * a table cell begins. The position returned is suitable for use with CellSelection.create().
58009
- *
58010
- * Algorithm:
58011
- * 1. Validate input (tableHit structure and cell indices)
58012
- * 2. Traverse document to find the table node matching tableHit.block.id
58013
- * 3. Navigate through table structure (table > row > cell) to target row
58014
- * 4. Track logical column position accounting for colspan (handles merged cells)
58015
- * 5. Return position when target column falls within a cell's span
58016
- *
58017
- * Merged cell handling:
58018
- * - Does NOT assume 1:1 mapping between cell index and logical column
58019
- * - Tracks cumulative logical column position by summing colspan values
58020
- * - A cell with colspan=3 occupies logical columns [n, n+1, n+2]
58021
- * - Finds the cell whose logical span contains the target column index
58022
- *
58023
- * Error handling:
58024
- * - Input validation with console warnings for debugging
58025
- * - Try-catch around document traversal (catches corrupted document errors)
58026
- * - Bounds checking for row indices
58027
- * - Null checks at each navigation step
58028
- *
58029
- * @param tableHit - The table hit result from hitTestTableFragment containing:
58030
- * - block: TableBlock with the table's block ID
58031
- * - cellRowIndex: 0-based row index of the target cell
58032
- * - cellColIndex: 0-based logical column index of the target cell
58033
- * @returns The PM position at the start of the cell, or null if:
58034
- * - Invalid input (null tableHit, negative indices)
58035
- * - Table not found in document
58036
- * - Target row out of bounds
58037
- * - Target column not found in row
58038
- * - Document traversal error
58039
- * @private
58040
- *
58041
- * @throws Never throws - all errors are caught and logged, returns null on failure
58042
- */
58043
- #getCellPosFromTableHit(tableHit) {
58044
- return getCellPosFromTableHit(tableHit, this.#editor.state?.doc ?? null, this.#layoutState.blocks);
58045
- }
58046
- /**
58047
- * Gets the table position (start of table node) from a table hit result.
58048
- *
58049
- * @param tableHit - The table hit result from hitTestTableFragment
58050
- * @returns The PM position at the start of the table, or null if not found
58051
- * @private
58052
- */
58053
- #getTablePosFromHit(tableHit) {
58054
- return getTablePosFromHit(tableHit, this.#editor.state?.doc ?? null, this.#layoutState.blocks);
58055
- }
58056
- /**
58057
- * Determines if the current drag should create a CellSelection instead of TextSelection.
58058
- *
58059
- * Implements a state machine for table cell selection:
58060
- * - 'none': Not in a table, use TextSelection
58061
- * - 'pending': Started drag in a table, but haven't crossed cell boundary yet
58062
- * - 'active': Crossed cell boundary, use CellSelection
58063
- *
58064
- * State transitions:
58065
- * - none → pending: When drag starts in a table cell (#setCellAnchor)
58066
- * - pending → active: When drag crosses into a different cell (this method returns true)
58067
- * - active → none: When drag ends (#clearCellAnchor)
58068
- * - * → none: When document changes or clicking outside table
58069
- *
58070
- * Decision logic:
58071
- * 1. No cell anchor → false (not in table drag mode)
58072
- * 2. Current position outside table → return current state (stay in 'active' if already there)
58073
- * 3. Different table → treat as outside table
58074
- * 4. Different cell in same table → true (activate cell selection)
58075
- * 5. Same cell → return current state (stay in 'active' if already there, else false)
58076
- *
58077
- * This state machine ensures:
58078
- * - Text selection works normally within a single cell
58079
- * - Cell selection activates smoothly when crossing cell boundaries
58080
- * - Once activated, cell selection persists even if dragging back to anchor cell
58081
- *
58082
- * @param currentTableHit - The table hit result for the current pointer position, or null if not in a table
58083
- * @returns true if we should create a CellSelection, false for TextSelection
58084
- * @private
58085
- */
58086
- #shouldUseCellSelection(currentTableHit) {
58087
- return shouldUseCellSelection(currentTableHit, this.#cellAnchor, this.#cellDragMode);
58088
- }
58089
- /**
58090
- * Stores the cell anchor when a drag operation starts inside a table cell.
58091
- *
58092
- * @param tableHit - The table hit result for the initial click position
58093
- * @param tablePos - The PM position of the table node
58094
- * @private
58095
- */
58096
- #setCellAnchor(tableHit, tablePos) {
58097
- const cellPos = this.#getCellPosFromTableHit(tableHit);
58098
- if (cellPos === null) {
58099
- return;
58100
- }
58101
- this.#cellAnchor = {
58102
- tablePos,
58103
- cellPos,
58104
- cellRowIndex: tableHit.cellRowIndex,
58105
- cellColIndex: tableHit.cellColIndex,
58106
- tableBlockId: tableHit.block.id
58107
- };
58108
- this.#cellDragMode = "pending";
58109
- }
58110
- /**
58111
- * Clears the cell drag state.
58112
- * Called when drag ends or when clicking outside a table.
58113
- *
58114
- * @private
58115
- */
58116
- #clearCellAnchor() {
58117
- this.#cellAnchor = null;
58118
- this.#cellDragMode = "none";
58119
- }
58120
58391
  /**
58121
58392
  * Attempts to perform a table hit test for the given normalized coordinates.
58122
58393
  *
@@ -58227,415 +58498,6 @@ class PresentationEditor extends EventEmitter {
58227
58498
  #calculateExtendedSelection(anchor, head, mode) {
58228
58499
  return calculateExtendedSelection(this.#layoutState.blocks, anchor, head, mode);
58229
58500
  }
58230
- #handlePointerMove = (event) => {
58231
- if (!this.#layoutState.layout) return;
58232
- const normalized = this.#normalizeClientPoint(event.clientX, event.clientY);
58233
- if (!normalized) return;
58234
- if (this.#isDragging && this.#dragAnchor !== null && event.buttons & 1) {
58235
- this.#pendingMarginClick = null;
58236
- const prevPointer = this.#dragLastPointer;
58237
- const prevRawHit = this.#dragLastRawHit;
58238
- this.#dragLastPointer = { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y };
58239
- const rawHit = clickToPosition(
58240
- this.#layoutState.layout,
58241
- this.#layoutState.blocks,
58242
- this.#layoutState.measures,
58243
- { x: normalized.x, y: normalized.y },
58244
- this.#viewportHost,
58245
- event.clientX,
58246
- event.clientY,
58247
- this.#pageGeometryHelper ?? void 0
58248
- );
58249
- if (!rawHit) {
58250
- debugLog(
58251
- "verbose",
58252
- `Drag selection update (no hit) ${JSON.stringify({
58253
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
58254
- prevPointer,
58255
- anchor: this.#dragAnchor
58256
- })}`
58257
- );
58258
- return;
58259
- }
58260
- const doc2 = this.#editor.state?.doc;
58261
- if (!doc2) return;
58262
- this.#dragLastRawHit = rawHit;
58263
- const pageMounted = this.#getPageElement(rawHit.pageIndex) != null;
58264
- if (!pageMounted && this.#isSelectionAwareVirtualizationEnabled()) {
58265
- this.#dragUsedPageNotMountedFallback = true;
58266
- debugLog("warn", "Geometry fallback", { reason: "page_not_mounted", pageIndex: rawHit.pageIndex });
58267
- }
58268
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: true, extraPages: [rawHit.pageIndex] });
58269
- const mappedHead = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(rawHit.pos, rawHit.layoutEpoch, 1);
58270
- if (!mappedHead.ok) {
58271
- debugLog("warn", "drag mapping failed", mappedHead);
58272
- debugLog(
58273
- "verbose",
58274
- `Drag selection update (map failed) ${JSON.stringify({
58275
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
58276
- prevPointer,
58277
- anchor: this.#dragAnchor,
58278
- rawHit: {
58279
- pos: rawHit.pos,
58280
- pageIndex: rawHit.pageIndex,
58281
- blockId: rawHit.blockId,
58282
- lineIndex: rawHit.lineIndex,
58283
- layoutEpoch: rawHit.layoutEpoch
58284
- },
58285
- mapped: {
58286
- ok: false,
58287
- reason: mappedHead.reason,
58288
- fromEpoch: mappedHead.fromEpoch,
58289
- toEpoch: mappedHead.toEpoch
58290
- }
58291
- })}`
58292
- );
58293
- return;
58294
- }
58295
- const hit = {
58296
- ...rawHit,
58297
- pos: Math.max(0, Math.min(mappedHead.pos, doc2.content.size)),
58298
- layoutEpoch: mappedHead.toEpoch
58299
- };
58300
- this.#debugLastHit = {
58301
- source: pageMounted ? "dom" : "geometry",
58302
- pos: rawHit.pos,
58303
- layoutEpoch: rawHit.layoutEpoch,
58304
- mappedPos: hit.pos
58305
- };
58306
- this.#updateSelectionDebugHud();
58307
- const anchor = this.#dragAnchor;
58308
- const head = hit.pos;
58309
- const { selAnchor, selHead } = this.#calculateExtendedSelection(anchor, head, this.#dragExtensionMode);
58310
- debugLog(
58311
- "verbose",
58312
- `Drag selection update ${JSON.stringify({
58313
- pointer: { clientX: event.clientX, clientY: event.clientY, x: normalized.x, y: normalized.y },
58314
- prevPointer,
58315
- rawHit: {
58316
- pos: rawHit.pos,
58317
- pageIndex: rawHit.pageIndex,
58318
- blockId: rawHit.blockId,
58319
- lineIndex: rawHit.lineIndex,
58320
- layoutEpoch: rawHit.layoutEpoch
58321
- },
58322
- prevRawHit: prevRawHit ? {
58323
- pos: prevRawHit.pos,
58324
- pageIndex: prevRawHit.pageIndex,
58325
- blockId: prevRawHit.blockId,
58326
- lineIndex: prevRawHit.lineIndex,
58327
- layoutEpoch: prevRawHit.layoutEpoch
58328
- } : null,
58329
- mappedHead: { pos: mappedHead.pos, fromEpoch: mappedHead.fromEpoch, toEpoch: mappedHead.toEpoch },
58330
- hit: { pos: hit.pos, pageIndex: hit.pageIndex, layoutEpoch: hit.layoutEpoch },
58331
- anchor,
58332
- head,
58333
- selAnchor,
58334
- selHead,
58335
- direction: head >= anchor ? "down" : "up",
58336
- selectionDirection: selHead >= selAnchor ? "down" : "up",
58337
- extensionMode: this.#dragExtensionMode,
58338
- hitSource: pageMounted ? "dom" : "geometry",
58339
- pageMounted
58340
- })}`
58341
- );
58342
- const currentTableHit = this.#hitTestTable(normalized.x, normalized.y);
58343
- const shouldUseCellSel = this.#shouldUseCellSelection(currentTableHit);
58344
- if (shouldUseCellSel && this.#cellAnchor) {
58345
- const headCellPos = currentTableHit ? this.#getCellPosFromTableHit(currentTableHit) : null;
58346
- if (headCellPos !== null) {
58347
- if (this.#cellDragMode !== "active") {
58348
- this.#cellDragMode = "active";
58349
- }
58350
- try {
58351
- const doc22 = this.#editor.state.doc;
58352
- const anchorCellPos = this.#cellAnchor.cellPos;
58353
- const clampedAnchor = Math.max(0, Math.min(anchorCellPos, doc22.content.size));
58354
- const clampedHead = Math.max(0, Math.min(headCellPos, doc22.content.size));
58355
- const cellSelection = CellSelection.create(doc22, clampedAnchor, clampedHead);
58356
- const tr = this.#editor.state.tr.setSelection(cellSelection);
58357
- this.#editor.view?.dispatch(tr);
58358
- this.#scheduleSelectionUpdate();
58359
- } catch (error) {
58360
- console.warn("[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:", error);
58361
- const anchor2 = this.#dragAnchor;
58362
- const head2 = hit.pos;
58363
- const { selAnchor: selAnchor2, selHead: selHead2 } = this.#calculateExtendedSelection(anchor2, head2, this.#dragExtensionMode);
58364
- try {
58365
- const tr = this.#editor.state.tr.setSelection(
58366
- superEditor_converter.TextSelection.create(this.#editor.state.doc, selAnchor2, selHead2)
58367
- );
58368
- this.#editor.view?.dispatch(tr);
58369
- this.#scheduleSelectionUpdate();
58370
- } catch {
58371
- }
58372
- }
58373
- return;
58374
- }
58375
- }
58376
- try {
58377
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(this.#editor.state.doc, selAnchor, selHead));
58378
- this.#editor.view?.dispatch(tr);
58379
- this.#scheduleSelectionUpdate();
58380
- } catch (error) {
58381
- console.warn("[SELECTION] Failed to extend selection during drag:", {
58382
- error,
58383
- anchor,
58384
- head,
58385
- selAnchor,
58386
- selHead,
58387
- mode: this.#dragExtensionMode
58388
- });
58389
- }
58390
- return;
58391
- }
58392
- const sessionMode = this.#headerFooterSession?.session?.mode ?? "body";
58393
- if (sessionMode !== "body") {
58394
- this.#clearHoverRegion();
58395
- return;
58396
- }
58397
- if (this.#documentMode === "viewing") {
58398
- this.#clearHoverRegion();
58399
- return;
58400
- }
58401
- const region = this.#hitTestHeaderFooterRegion(normalized.x, normalized.y);
58402
- if (!region) {
58403
- this.#clearHoverRegion();
58404
- return;
58405
- }
58406
- const currentHover = this.#headerFooterSession?.hoverRegion;
58407
- if (currentHover && currentHover.kind === region.kind && currentHover.pageIndex === region.pageIndex && currentHover.sectionType === region.sectionType) {
58408
- return;
58409
- }
58410
- this.#headerFooterSession?.renderHover(region);
58411
- this.#renderHoverRegion(region);
58412
- };
58413
- #handlePointerLeave = () => {
58414
- this.#clearHoverRegion();
58415
- };
58416
- #handleVisibleHostFocusIn = (event) => {
58417
- if (isInRegisteredSurface(event)) {
58418
- return;
58419
- }
58420
- if (this.#suppressFocusInFromDraggable) {
58421
- this.#suppressFocusInFromDraggable = false;
58422
- return;
58423
- }
58424
- const target = event.target;
58425
- const activeTarget = this.#getActiveDomTarget();
58426
- if (!activeTarget) {
58427
- return;
58428
- }
58429
- const activeNode = activeTarget;
58430
- const containsFn = typeof activeNode.contains === "function" ? activeNode.contains : null;
58431
- if (target && (activeNode === target || containsFn && containsFn.call(activeNode, target))) {
58432
- return;
58433
- }
58434
- try {
58435
- if (activeTarget instanceof HTMLElement && typeof activeTarget.focus === "function") {
58436
- activeTarget.focus?.({
58437
- preventScroll: true
58438
- });
58439
- } else if (typeof activeTarget.focus === "function") {
58440
- activeTarget.focus();
58441
- }
58442
- } catch {
58443
- }
58444
- try {
58445
- this.getActiveEditor().view?.focus();
58446
- } catch {
58447
- }
58448
- };
58449
- #handlePointerUp = (event) => {
58450
- this.#suppressFocusInFromDraggable = false;
58451
- if (!this.#isDragging) return;
58452
- if (typeof this.#viewportHost.hasPointerCapture === "function" && typeof this.#viewportHost.releasePointerCapture === "function" && this.#viewportHost.hasPointerCapture(event.pointerId)) {
58453
- this.#viewportHost.releasePointerCapture(event.pointerId);
58454
- }
58455
- const pendingMarginClick = this.#pendingMarginClick;
58456
- this.#pendingMarginClick = null;
58457
- const dragAnchor = this.#dragAnchor;
58458
- const dragMode = this.#dragExtensionMode;
58459
- const dragUsedFallback = this.#dragUsedPageNotMountedFallback;
58460
- const dragPointer = this.#dragLastPointer;
58461
- this.#isDragging = false;
58462
- if (this.#cellDragMode !== "none") {
58463
- this.#cellDragMode = "none";
58464
- }
58465
- if (!pendingMarginClick || pendingMarginClick.pointerId !== event.pointerId) {
58466
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: false });
58467
- if (dragUsedFallback && dragAnchor != null) {
58468
- const pointer = dragPointer ?? { clientX: event.clientX, clientY: event.clientY };
58469
- this.#finalizeDragSelectionWithDom(pointer, dragAnchor, dragMode);
58470
- }
58471
- this.#scheduleA11ySelectionAnnouncement({ immediate: true });
58472
- this.#dragLastPointer = null;
58473
- this.#dragLastRawHit = null;
58474
- this.#dragUsedPageNotMountedFallback = false;
58475
- return;
58476
- }
58477
- const sessionModeForDrag = this.#headerFooterSession?.session?.mode ?? "body";
58478
- if (sessionModeForDrag !== "body" || this.#isViewLocked()) {
58479
- this.#dragLastPointer = null;
58480
- this.#dragLastRawHit = null;
58481
- this.#dragUsedPageNotMountedFallback = false;
58482
- return;
58483
- }
58484
- const doc2 = this.#editor.state?.doc;
58485
- if (!doc2) {
58486
- this.#dragLastPointer = null;
58487
- this.#dragLastRawHit = null;
58488
- this.#dragUsedPageNotMountedFallback = false;
58489
- return;
58490
- }
58491
- if (pendingMarginClick.kind === "aboveFirstLine") {
58492
- const pos = this.#getFirstTextPosition();
58493
- try {
58494
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, pos));
58495
- this.#editor.view?.dispatch(tr);
58496
- this.#scheduleSelectionUpdate();
58497
- } catch {
58498
- }
58499
- this.#debugLastHit = { source: "margin", pos: null, layoutEpoch: null, mappedPos: pos };
58500
- this.#updateSelectionDebugHud();
58501
- this.#dragLastPointer = null;
58502
- this.#dragLastRawHit = null;
58503
- this.#dragUsedPageNotMountedFallback = false;
58504
- return;
58505
- }
58506
- if (pendingMarginClick.kind === "right") {
58507
- const mappedEnd2 = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
58508
- pendingMarginClick.pmEnd,
58509
- pendingMarginClick.layoutEpoch,
58510
- 1
58511
- );
58512
- if (!mappedEnd2.ok) {
58513
- debugLog("warn", "right margin mapping failed", mappedEnd2);
58514
- this.#pendingDocChange = true;
58515
- this.#scheduleRerender();
58516
- this.#dragLastPointer = null;
58517
- this.#dragLastRawHit = null;
58518
- this.#dragUsedPageNotMountedFallback = false;
58519
- return;
58520
- }
58521
- const caretPos = Math.max(0, Math.min(mappedEnd2.pos, doc2.content.size));
58522
- try {
58523
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, caretPos));
58524
- this.#editor.view?.dispatch(tr);
58525
- this.#scheduleSelectionUpdate();
58526
- } catch {
58527
- }
58528
- this.#debugLastHit = {
58529
- source: "margin",
58530
- pos: pendingMarginClick.pmEnd,
58531
- layoutEpoch: pendingMarginClick.layoutEpoch,
58532
- mappedPos: caretPos
58533
- };
58534
- this.#updateSelectionDebugHud();
58535
- this.#dragLastPointer = null;
58536
- this.#dragLastRawHit = null;
58537
- this.#dragUsedPageNotMountedFallback = false;
58538
- return;
58539
- }
58540
- const mappedStart = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
58541
- pendingMarginClick.pmStart,
58542
- pendingMarginClick.layoutEpoch,
58543
- 1
58544
- );
58545
- const mappedEnd = this.#epochMapper.mapPosFromLayoutToCurrentDetailed(
58546
- pendingMarginClick.pmEnd,
58547
- pendingMarginClick.layoutEpoch,
58548
- -1
58549
- );
58550
- if (!mappedStart.ok || !mappedEnd.ok) {
58551
- if (!mappedStart.ok) debugLog("warn", "left margin mapping failed (start)", mappedStart);
58552
- if (!mappedEnd.ok) debugLog("warn", "left margin mapping failed (end)", mappedEnd);
58553
- this.#pendingDocChange = true;
58554
- this.#scheduleRerender();
58555
- this.#dragLastPointer = null;
58556
- this.#dragLastRawHit = null;
58557
- this.#dragUsedPageNotMountedFallback = false;
58558
- return;
58559
- }
58560
- const selFrom = Math.max(0, Math.min(Math.min(mappedStart.pos, mappedEnd.pos), doc2.content.size));
58561
- const selTo = Math.max(0, Math.min(Math.max(mappedStart.pos, mappedEnd.pos), doc2.content.size));
58562
- try {
58563
- const tr = this.#editor.state.tr.setSelection(superEditor_converter.TextSelection.create(doc2, selFrom, selTo));
58564
- this.#editor.view?.dispatch(tr);
58565
- this.#scheduleSelectionUpdate();
58566
- } catch {
58567
- }
58568
- this.#debugLastHit = {
58569
- source: "margin",
58570
- pos: pendingMarginClick.pmStart,
58571
- layoutEpoch: pendingMarginClick.layoutEpoch,
58572
- mappedPos: selFrom
58573
- };
58574
- this.#updateSelectionDebugHud();
58575
- this.#dragLastPointer = null;
58576
- this.#dragLastRawHit = null;
58577
- this.#dragUsedPageNotMountedFallback = false;
58578
- };
58579
- #handleDragOver = createExternalFieldAnnotationDragOverHandler({
58580
- getActiveEditor: () => this.getActiveEditor(),
58581
- hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
58582
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
58583
- });
58584
- #handleDrop = createExternalFieldAnnotationDropHandler({
58585
- getActiveEditor: () => this.getActiveEditor(),
58586
- hitTest: (clientX, clientY) => this.hitTest(clientX, clientY),
58587
- scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate()
58588
- });
58589
- #handleDoubleClick = (event) => {
58590
- if (event.button !== 0) return;
58591
- if (!this.#layoutState.layout) return;
58592
- const rect = this.#viewportHost.getBoundingClientRect();
58593
- const zoom = this.#layoutOptions.zoom ?? 1;
58594
- const scrollLeft = this.#visibleHost.scrollLeft ?? 0;
58595
- const scrollTop = this.#visibleHost.scrollTop ?? 0;
58596
- const x = (event.clientX - rect.left + scrollLeft) / zoom;
58597
- const y2 = (event.clientY - rect.top + scrollTop) / zoom;
58598
- const region = this.#hitTestHeaderFooterRegion(x, y2);
58599
- if (region) {
58600
- event.preventDefault();
58601
- event.stopPropagation();
58602
- const descriptor = this.#resolveDescriptorForRegion(region);
58603
- const hfManager = this.#headerFooterSession?.manager;
58604
- if (!descriptor && hfManager) {
58605
- this.#createDefaultHeaderFooter(region);
58606
- hfManager.refresh();
58607
- }
58608
- this.#activateHeaderFooterRegion(region);
58609
- } else if ((this.#headerFooterSession?.session?.mode ?? "body") !== "body") {
58610
- this.#exitHeaderFooterMode();
58611
- }
58612
- };
58613
- #handleKeyDown = (event) => {
58614
- const sessionModeForKey = this.#headerFooterSession?.session?.mode ?? "body";
58615
- if (event.key === "Escape" && sessionModeForKey !== "body") {
58616
- event.preventDefault();
58617
- this.#exitHeaderFooterMode();
58618
- return;
58619
- }
58620
- if (event.ctrlKey && event.altKey && !event.shiftKey) {
58621
- if (event.code === "KeyH") {
58622
- event.preventDefault();
58623
- this.#focusHeaderFooterShortcut("header");
58624
- } else if (event.code === "KeyF") {
58625
- event.preventDefault();
58626
- this.#focusHeaderFooterShortcut("footer");
58627
- }
58628
- }
58629
- };
58630
- #focusHeaderFooterShortcut(kind) {
58631
- const pageIndex = this.#getCurrentPageIndex();
58632
- const region = this.#findRegionForPage(kind, pageIndex);
58633
- if (!region) {
58634
- this.#emitHeaderFooterEditBlocked("missingRegion");
58635
- return;
58636
- }
58637
- this.#activateHeaderFooterRegion(region);
58638
- }
58639
58501
  #scheduleRerender() {
58640
58502
  if (this.#renderScheduled) {
58641
58503
  return;
@@ -59106,7 +58968,7 @@ class PresentationEditor extends EventEmitter {
59106
58968
  return;
59107
58969
  }
59108
58970
  this.#syncSelectedFieldAnnotationClass(selection);
59109
- this.#updateSelectionVirtualizationPins({ includeDragBuffer: this.#isDragging });
58971
+ this.#updateSelectionVirtualizationPins({ includeDragBuffer: this.#editorInputManager?.isDragging ?? false });
59110
58972
  if (selection instanceof CellSelection) {
59111
58973
  try {
59112
58974
  this.#localSelectionLayer.innerHTML = "";
@@ -59860,7 +59722,7 @@ class PresentationEditor extends EventEmitter {
59860
59722
  {
59861
59723
  ariaLiveRegion: this.#ariaLiveRegion,
59862
59724
  sessionMode,
59863
- isDragging: this.#isDragging,
59725
+ isDragging: this.#editorInputManager?.isDragging ?? false,
59864
59726
  visibleHost: this.#visibleHost,
59865
59727
  currentTimeout: this.#a11ySelectionAnnounceTimeout,
59866
59728
  announceNow: () => {
@@ -59988,9 +59850,9 @@ class PresentationEditor extends EventEmitter {
59988
59850
  } : null,
59989
59851
  docSize,
59990
59852
  includeDragBuffer: Boolean(options?.includeDragBuffer),
59991
- isDragging: this.#isDragging,
59992
- dragAnchorPageIndex: this.#dragAnchorPageIndex,
59993
- dragLastHitPageIndex: this.#dragLastRawHit ? this.#dragLastRawHit.pageIndex : null,
59853
+ isDragging: this.#editorInputManager?.isDragging ?? false,
59854
+ dragAnchorPageIndex: this.#editorInputManager?.dragAnchorPageIndex ?? null,
59855
+ dragLastHitPageIndex: this.#editorInputManager?.dragLastHitPageIndex ?? null,
59994
59856
  extraPages: options?.extraPages
59995
59857
  });
59996
59858
  painter.setVirtualizationPins(pins);
@@ -60004,9 +59866,10 @@ class PresentationEditor extends EventEmitter {
60004
59866
  }
60005
59867
  const normalized = this.#normalizeClientPoint(pointer.clientX, pointer.clientY);
60006
59868
  if (!normalized) return;
59869
+ const dragLastRawHit = this.#editorInputManager?.dragLastRawHit;
60007
59870
  this.#updateSelectionVirtualizationPins({
60008
59871
  includeDragBuffer: false,
60009
- extraPages: this.#dragLastRawHit ? [this.#dragLastRawHit.pageIndex] : void 0
59872
+ extraPages: dragLastRawHit ? [dragLastRawHit.pageIndex] : void 0
60010
59873
  });
60011
59874
  const refined = clickToPosition(
60012
59875
  layout,
@@ -60023,7 +59886,7 @@ class PresentationEditor extends EventEmitter {
60023
59886
  debugLog("warn", "Drag finalize: endpoint page still not mounted", { pageIndex: refined.pageIndex });
60024
59887
  return;
60025
59888
  }
60026
- const prior = this.#dragLastRawHit;
59889
+ const prior = dragLastRawHit;
60027
59890
  if (prior && (prior.pos !== refined.pos || prior.pageIndex !== refined.pageIndex)) {
60028
59891
  debugLog("info", "Drag finalize refined hit", {
60029
59892
  fromPos: prior.pos,
@@ -60226,7 +60089,7 @@ class PresentationEditor extends EventEmitter {
60226
60089
  localSelectionLayer,
60227
60090
  blocks: this.#layoutState.blocks,
60228
60091
  measures: this.#layoutState.measures,
60229
- cellAnchorTableBlockId: this.#cellAnchor?.tableBlockId ?? null,
60092
+ cellAnchorTableBlockId: this.#editorInputManager?.cellAnchor?.tableBlockId ?? null,
60230
60093
  convertPageLocalToOverlayCoords: (pageIndex, x, y2) => this.#convertPageLocalToOverlayCoords(pageIndex, x, y2)
60231
60094
  });
60232
60095
  }