superdoc 2.24.0-next.2 → 2.24.0-next.3

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.
@@ -6074,6 +6074,18 @@ function createSuperDocUI(options) {
6074
6074
  const host = getEditor()?.host;
6075
6075
  return host && typeof host === "object" ? host : null;
6076
6076
  };
6077
+ /**
6078
+ * The host selection snapshot a selection read, slice, or capture was
6079
+ * resolved from. The host replaces its snapshot object on every selection
6080
+ * change, so an identical object means the selection has not moved since.
6081
+ */
6082
+ const selectionHostSnapshots = /* @__PURE__ */ new WeakMap();
6083
+ const readHostSelectionSnapshot = () => {
6084
+ const selectionHandle = (safeCall(() => getHost()?.getHandles?.() ?? null, null)?.editing)?.selection;
6085
+ if (typeof selectionHandle?.getSnapshot !== "function") return null;
6086
+ const snapshot = safeCall(() => selectionHandle.getSnapshot(), null);
6087
+ return snapshot && typeof snapshot === "object" ? snapshot : null;
6088
+ };
6077
6089
  const createFirstPartyCommandDoc = () => {
6078
6090
  const editor = getEditor();
6079
6091
  const doc = editor?.doc;
@@ -7281,12 +7293,21 @@ function createSuperDocUI(options) {
7281
7293
  ...EMPTY_SELECTION,
7282
7294
  status
7283
7295
  };
7296
+ const slice = buildSelectionSlice(info, status);
7297
+ const hostSelection = selectionHostSnapshots.get(info);
7298
+ if (hostSelection) selectionHostSnapshots.set(slice, hostSelection);
7299
+ return slice;
7300
+ };
7301
+ const buildSelectionSlice = (info, status) => {
7284
7302
  const target = info.target ?? null;
7285
7303
  const quotedText = typeof info.text === "string" ? info.text : "";
7286
7304
  return {
7287
7305
  status,
7288
7306
  empty: info.empty === true,
7289
7307
  target,
7308
+ trackedTarget: info.trackedTarget ?? null,
7309
+ trackedChangeRangeDecisionSupported: info.trackedChangeRangeDecisionSupported,
7310
+ trackedChangesWhollySelected: info.trackedChangesWhollySelected,
7290
7311
  selectionTarget: info.selectionTarget ?? null,
7291
7312
  tableSelection: info.tableSelection ?? null,
7292
7313
  activeMarks: Array.isArray(info.activeMarks) ? info.activeMarks : [],
@@ -7295,18 +7316,22 @@ function createSuperDocUI(options) {
7295
7316
  quotedText
7296
7317
  };
7297
7318
  };
7298
- const readSelectionInfoLive = () => {
7299
- const selectionApi = getDoc()?.selection;
7300
- return readAsync("selection", selectionReadToken(), () => selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0, normalizeSelectionInfo);
7301
- };
7319
+ const readSelectionInfoLive = () => readAsync("selection", selectionReadToken(), selectionCurrentRun, normalizeSelectionInfo);
7302
7320
  const computeSelection = () => {
7303
7321
  const { value: info, status } = readSelectionInfoLive();
7304
7322
  return selectionSliceFromInfo(info, status);
7305
7323
  };
7306
- const selectionCurrentRun = () => {
7324
+ function selectionCurrentRun() {
7307
7325
  const selectionApi = getDoc()?.selection;
7308
- return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
7309
- };
7326
+ if (!selectionApi?.current) return void 0;
7327
+ const hostSelection = readHostSelectionSnapshot();
7328
+ const tag = (info) => {
7329
+ if (hostSelection && info && typeof info === "object") selectionHostSnapshots.set(info, hostSelection);
7330
+ return info;
7331
+ };
7332
+ const result = selectionApi.current({ includeText: true });
7333
+ return isPromiseLike(result) ? Promise.resolve(result).then(tag) : tag(result);
7334
+ }
7310
7335
  /** True when a host sync snapshot is a resolved collapsed caret (not a range). */
7311
7336
  const isCollapsedCaretSnapshot = (info) => {
7312
7337
  if (info.empty !== true) return false;
@@ -8286,6 +8311,10 @@ function createSuperDocUI(options) {
8286
8311
  if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
8287
8312
  if (disabledReason) return disabledReason;
8288
8313
  if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
8314
+ if (command.scope !== "all") {
8315
+ const plan = planSelectionTrackDecision(selection, true);
8316
+ if (plan?.kind === "blocked") return plan.reason;
8317
+ }
8289
8318
  };
8290
8319
  const findTrackChangeForPermission = (id) => {
8291
8320
  syncCoordinatorEditor();
@@ -8305,20 +8334,22 @@ function createSuperDocUI(options) {
8305
8334
  if (typeof canPerform !== "function" || ids.length === 0) return void 0;
8306
8335
  const currentUser = superdoc.config && typeof superdoc.config === "object" ? superdoc.config.user : null;
8307
8336
  for (const id of ids) {
8308
- const trackedChange = findTrackChangeForPermission(id) ?? { id };
8309
- const isOwn = trackedChangeOwnedByCurrentUser(trackedChange, currentUser);
8310
- const permission = kind === "accept" ? isOwn ? "RESOLVE_OWN" : "RESOLVE_OTHER" : isOwn ? "REJECT_OWN" : "REJECT_OTHER";
8311
- let allowed;
8312
- try {
8313
- allowed = canPerform.call(superdoc, {
8314
- permission,
8315
- trackedChange,
8316
- comment: null
8317
- });
8318
- } catch {
8319
- return;
8337
+ const trackedChange = findTrackChangeForPermission(id);
8338
+ const isOwn = trackedChange ? trackedChangeOwnedByCurrentUser(trackedChange, currentUser) : false;
8339
+ const permissions = trackedChange ? [kind === "accept" ? isOwn ? "RESOLVE_OWN" : "RESOLVE_OTHER" : isOwn ? "REJECT_OWN" : "REJECT_OTHER"] : kind === "accept" ? ["RESOLVE_OWN", "RESOLVE_OTHER"] : ["REJECT_OWN", "REJECT_OTHER"];
8340
+ for (const candidate of permissions) {
8341
+ let allowed;
8342
+ try {
8343
+ allowed = canPerform.call(superdoc, {
8344
+ permission: candidate,
8345
+ trackedChange: trackedChange ?? { id },
8346
+ comment: null
8347
+ });
8348
+ } catch {
8349
+ return trackedChange ? void 0 : SUPERDOC_UI_REASONS.permissionDenied;
8350
+ }
8351
+ if (allowed === false || !trackedChange && allowed !== true) return SUPERDOC_UI_REASONS.permissionDenied;
8320
8352
  }
8321
- if (allowed === false) return SUPERDOC_UI_REASONS.permissionDenied;
8322
8353
  }
8323
8354
  };
8324
8355
  const hostCommandSupport = (command) => {
@@ -9998,14 +10029,22 @@ function createSuperDocUI(options) {
9998
10029
  * ordinary/insertion selections are visible-space, while a selection that
9999
10030
  * reaches painted deleted text is projected in tracked space by the host.
10000
10031
  *
10001
- * A cross-block or malformed selection fails closed. Its intermediate block
10002
- * coverage cannot be reconstructed from two endpoints without guessing.
10032
+ * Cross-block coverage must come from the model-backed tracked target;
10033
+ * intermediate block lengths cannot be inferred from the endpoints.
10003
10034
  */
10004
10035
  function trackDecisionRangeFromSelection(selection) {
10005
10036
  if (selection.empty) return null;
10006
10037
  const explicit = selection.selectionTarget;
10007
10038
  const start = explicit?.start;
10008
10039
  const end = explicit?.end;
10040
+ if (start?.kind === "text" && end?.kind === "text" && start.blockId !== end.blockId) {
10041
+ const range = selection.trackedTarget;
10042
+ return range ? {
10043
+ kind: "range",
10044
+ coordinateSpace: "tracked",
10045
+ range
10046
+ } : null;
10047
+ }
10009
10048
  if (explicit?.kind !== "selection" || start?.kind !== "text" || end?.kind !== "text" || typeof start.blockId !== "string" || start.blockId !== end.blockId || !Number.isInteger(start.offset) || !Number.isInteger(end.offset) || start.offset < 0 || end.offset < 0 || start.offset === end.offset) return null;
10010
10049
  const rangeStart = Math.min(start.offset, end.offset);
10011
10050
  const rangeEnd = Math.max(start.offset, end.offset);
@@ -10050,32 +10089,20 @@ function createSuperDocUI(options) {
10050
10089
  function normalizeTrackDecisionText(value) {
10051
10090
  return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
10052
10091
  }
10053
- function activeChangePartialDecisionRoute() {
10054
- const activeId = state.selection.activeChangeIds[0];
10092
+ function activeChangePartialDecisionRoute(selection = state.selection) {
10093
+ const activeId = selection.activeChangeIds[0];
10055
10094
  if (!activeId) return "unavailable";
10056
- const story = selectionStoryLocator(state.selection);
10057
- const item = (state.trackChanges.status === "ready" ? state.trackChanges.items.find((candidate) => entityRowMatchesRequest(candidate, activeId, story)) : void 0) ?? readBodyTrackChangeForDecision(activeId, story) ?? readAllStoryTrackChanges()?.find((candidate) => entityRowMatchesRequest(candidate, activeId, story));
10095
+ const story = selectionStoryLocator(selection);
10096
+ const trackChanges = state?.trackChanges;
10097
+ const item = (trackChanges?.status === "ready" ? trackChanges.items.find((candidate) => entityRowMatchesRequest(candidate, activeId, story)) : void 0) ?? readBodyTrackChangeForDecision(activeId, story) ?? readAllStoryTrackChanges()?.find((candidate) => entityRowMatchesRequest(candidate, activeId, story));
10058
10098
  if (!item) return "unavailable";
10059
10099
  const change = trackChangesItemPayload(item);
10060
10100
  const type = change.type;
10061
10101
  const grouping = change.grouping;
10062
- if (selectedTextCoversTrackChangeText(state.selection, item)) return "id";
10102
+ if (selectedTextCoversTrackChangeText(selection, item)) return "id";
10063
10103
  if (type === "replacement" && grouping === "replacement-pair") return "range";
10064
10104
  return (type === "insertion" || type === "deletion" || type === "insert" || type === "delete") && grouping === "standalone" ? "range" : "id";
10065
10105
  }
10066
- function executeTrackDecisionRangeWithIdFallback(kind, rangeTarget, changeId, story) {
10067
- const fallback = () => callTrackDecisionTarget(kind, trackDecisionIdTarget(changeId, story), changeId, story);
10068
- try {
10069
- const result = callTrackDecisionTarget(kind, rangeTarget, changeId, story);
10070
- if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
10071
- return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
10072
- }));
10073
- const commandResult = commandResultFromOperationResult(result);
10074
- return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
10075
- } catch {
10076
- return false;
10077
- }
10078
- }
10079
10106
  /** A decision target carried by the payload itself, independent of the selection. */
10080
10107
  function explicitTrackDecisionTarget(payload) {
10081
10108
  if (typeof payload === "string" && payload.length > 0) return {
@@ -10119,29 +10146,88 @@ function createSuperDocUI(options) {
10119
10146
  story
10120
10147
  } : null;
10121
10148
  }
10149
+ /**
10150
+ * The selection-derived decision for a review command. The command gate and
10151
+ * execution share these rules so an enabled command always has a target.
10152
+ * `null` means the selection does not drive the decision (no active change
10153
+ * or a collapsed caret) and the caller falls back to the active change id.
10154
+ * `gateOnly` stops after the blocking rules and returns only a block or null.
10155
+ */
10156
+ function planSelectionTrackDecision(selection, gateOnly = false) {
10157
+ if (selection.tableSelection?.kind === "tableCells" && (selection.tableSelection.type === "cells" || selection.tableSelection.type === "table")) return {
10158
+ kind: "blocked",
10159
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10160
+ };
10161
+ const ids = selection.activeChangeIds;
10162
+ if (ids.length > 1) {
10163
+ if (selection.empty || selection.trackedChangesWhollySelected === true) return {
10164
+ kind: "ids",
10165
+ ids
10166
+ };
10167
+ if (selection.trackedChangeRangeDecisionSupported === false) return {
10168
+ kind: "blocked",
10169
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10170
+ };
10171
+ const target = trackDecisionRangeFromSelection(selection);
10172
+ return target ? {
10173
+ kind: "range",
10174
+ target,
10175
+ changeId: null,
10176
+ permissionIds: ids
10177
+ } : {
10178
+ kind: "blocked",
10179
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10180
+ };
10181
+ }
10182
+ if (ids.length !== 1 || selection.empty) return null;
10183
+ const activeId = ids[0];
10184
+ let routeValue;
10185
+ const route = () => routeValue ??= selection.trackedChangesWhollySelected === true ? "id" : activeChangePartialDecisionRoute(selection);
10186
+ if (selection.trackedChangeRangeDecisionSupported === false && route() !== "id") return {
10187
+ kind: "blocked",
10188
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10189
+ };
10190
+ if (!selection.selectionTarget) return null;
10191
+ const target = trackDecisionRangeFromSelection(selection);
10192
+ if (!target && route() !== "id") return {
10193
+ kind: "blocked",
10194
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10195
+ };
10196
+ if (gateOnly) return null;
10197
+ if (route() === "id") {
10198
+ const selectedStory = selectionStoryLocator(selection);
10199
+ return {
10200
+ kind: "id",
10201
+ id: activeId,
10202
+ story: selectedStory && storyLocatorSignature(selectedStory) !== storyLocatorSignature(null) ? selectedStory : void 0
10203
+ };
10204
+ }
10205
+ if (route() === "unavailable" && !selection.trackedTarget) return { kind: "unavailable" };
10206
+ return target ? {
10207
+ kind: "range",
10208
+ target,
10209
+ changeId: activeId,
10210
+ story: nonBodySelectionStoryLocator(selection)
10211
+ } : {
10212
+ kind: "blocked",
10213
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10214
+ };
10215
+ }
10122
10216
  function executeTrackDecisionCommand(command, payload) {
10123
10217
  if (command.scope === "id" && payload == null) {
10124
- if (state.selection.activeChangeIds.length > 1) return executeTrackDecisionTarget(command.kind, {
10125
- kind: "ids",
10126
- ids: state.selection.activeChangeIds
10127
- }, null);
10128
- if (state.selection.activeChangeIds.length === 1 && !state.selection.empty && state.selection.selectionTarget) {
10129
- const route = activeChangePartialDecisionRoute();
10130
- if (route === "unavailable") return false;
10131
- if (route === "range") {
10132
- const target = trackDecisionRangeFromSelection(state.selection);
10133
- const story = nonBodySelectionStoryLocator(state.selection);
10134
- return target ? executeTrackDecisionRangeWithIdFallback(command.kind, target, state.selection.activeChangeIds[0], story) : false;
10135
- }
10136
- if (route === "id") {
10137
- const activeId = state.selection.activeChangeIds[0];
10138
- const selectedStory = selectionStoryLocator(state.selection);
10139
- const story = selectedStory && storyLocatorSignature(selectedStory) !== storyLocatorSignature(null) ? selectedStory : void 0;
10140
- return executeTrackDecision(command.kind, story ? {
10141
- id: activeId,
10142
- story
10143
- } : activeId);
10144
- }
10218
+ const plan = planSelectionTrackDecision(state.selection);
10219
+ switch (plan?.kind) {
10220
+ case "blocked":
10221
+ case "unavailable": return false;
10222
+ case "ids": return executeTrackDecisionTarget(command.kind, {
10223
+ kind: "ids",
10224
+ ids: plan.ids
10225
+ }, null, void 0, plan.ids);
10226
+ case "range": return executeTrackDecisionTarget(command.kind, plan.target, plan.changeId, plan.story, plan.permissionIds);
10227
+ case "id": return executeTrackDecision(command.kind, plan.story ? {
10228
+ id: plan.id,
10229
+ story: plan.story
10230
+ } : plan.id);
10145
10231
  }
10146
10232
  }
10147
10233
  const resolved = resolveTrackDecisionTarget(command, payload);
@@ -11810,11 +11896,14 @@ function createSuperDocUI(options) {
11810
11896
  if (snapshot.empty) return null;
11811
11897
  const cells = currentCellSelectionTarget();
11812
11898
  if (!snapshot.target && !snapshot.selectionTarget && !cells) return null;
11813
- return {
11899
+ const capture = {
11814
11900
  ...snapshot,
11815
11901
  capturedAt: Date.now(),
11816
11902
  ...cells ? { _tableCellSelectionTarget: cells } : {}
11817
11903
  };
11904
+ const hostSelection = selectionHostSnapshots.get(snapshot);
11905
+ if (hostSelection) selectionHostSnapshots.set(capture, hostSelection);
11906
+ return capture;
11818
11907
  },
11819
11908
  restore: (capture) => {
11820
11909
  const done = (result) => {
@@ -11843,6 +11932,8 @@ function createSuperDocUI(options) {
11843
11932
  ok: false,
11844
11933
  reason: SUPERDOC_UI_REASONS.targetUnresolved
11845
11934
  });
11935
+ const heldHostSelection = selectionHostSnapshots.get(capture);
11936
+ if (!hasCellTarget && target.coordinateSpace === "tracked" && heldHostSelection !== void 0 && readHostSelectionSnapshot() === heldHostSelection) return done({ ok: true });
11846
11937
  const resolved = getSelectionApplyHelper();
11847
11938
  if (!("helper" in resolved)) return done({
11848
11939
  ok: false,
@@ -12679,20 +12770,20 @@ function createSuperDocUI(options) {
12679
12770
  if (run() === false) return Promise.resolve(false);
12680
12771
  return lastCommandSettlement;
12681
12772
  };
12682
- const executeTrackDecisionTarget = (kind, target, changeId, story) => {
12773
+ const executeTrackDecisionTarget = (kind, target, changeId, story, selectionChangeIds) => {
12683
12774
  try {
12684
- return settleCommandExecution(callTrackDecisionTarget(kind, target, changeId, story));
12775
+ return settleCommandExecution(callTrackDecisionTarget(kind, target, changeId, story, selectionChangeIds));
12685
12776
  } catch {
12686
12777
  return false;
12687
12778
  }
12688
12779
  };
12689
- const callTrackDecisionTarget = (kind, target, changeId, story) => {
12780
+ const callTrackDecisionTarget = (kind, target, changeId, story, selectionChangeIds) => {
12690
12781
  if (trackedChangeDecisionsAreDisabled()) return false;
12691
12782
  const tcApi = getDoc()?.trackChanges;
12692
12783
  const isAllTarget = target.kind === "all" || target.scope === "all";
12693
12784
  const isRangeTarget = target.kind === "range";
12694
12785
  const isMultiIdTarget = target.kind === "ids";
12695
- const permissionIds = isAllTarget ? [] : isMultiIdTarget && Array.isArray(target.ids) ? target.ids.filter((id) => typeof id === "string" && id.length > 0) : changeId ? [changeId] : [];
12786
+ const permissionIds = isAllTarget ? [] : selectionChangeIds ?? (isMultiIdTarget && Array.isArray(target.ids) ? target.ids.filter((id) => typeof id === "string" && id.length > 0) : changeId ? [changeId] : []);
12696
12787
  if (permissionIds.length > 0 && trackedChangeDecisionPermissionReason(kind, permissionIds)) return false;
12697
12788
  if (bulkTrackDecisionBlockedReason({
12698
12789
  kind,