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