@bigbinary/neeto-editor 1.47.123 → 1.47.124

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.
@@ -14,7 +14,7 @@ var Label = require('@bigbinary/neetoui/Label');
14
14
  var ReactDOM$1 = require('react-dom');
15
15
  var ramda = require('ramda');
16
16
  var jsxRuntime = require('react/jsx-runtime');
17
- var fuzzySearch = require('./chunk-BXZTd0jZ.cjs.js');
17
+ var fuzzySearch = require('./chunk-Ch4y-VZi.cjs.js');
18
18
  var injectCss = require('./chunk-B6qYtOJe.cjs.js');
19
19
  var utils = require('./chunk-B_7qxIrt.cjs.js');
20
20
  var ReactDOM = require('react-dom/server');
@@ -19,7 +19,7 @@ require('zustand');
19
19
  require('classnames');
20
20
  require('./chunk-2HrSPdAV.cjs.js');
21
21
  require('@bigbinary/neetoui/Label');
22
- require('./chunk-BXZTd0jZ.cjs.js');
22
+ require('./chunk-Ch4y-VZi.cjs.js');
23
23
  require('./chunk-BKvkHB8E.cjs.js');
24
24
  require('./chunk-BJu3ubxk.cjs.js');
25
25
  require('@bigbinary/neeto-commons-frontend/initializers');
@@ -3197,6 +3197,31 @@ const ySyncPlugin = (yXmlFragment, {
3197
3197
  return plugin
3198
3198
  };
3199
3199
 
3200
+ /**
3201
+ * Resolves one text-selection endpoint after a remote update. Falls back to
3202
+ * content-based matching when the Yjs resolution is missing or lands in the
3203
+ * wrong block, and keeps the Yjs resolution when the fallback finds nothing.
3204
+ *
3205
+ * @param {import('prosemirror-model').Node} newDoc
3206
+ * @param {import('prosemirror-model').Node} oldDoc
3207
+ * @param {number|null|undefined} oldAbs
3208
+ * @param {number|null} resolved
3209
+ * @return {number|null}
3210
+ */
3211
+ const recoverSelectionEndpoint = (newDoc, oldDoc, oldAbs, resolved) => {
3212
+ if (oldAbs == null) {
3213
+ return resolved
3214
+ }
3215
+ const misresolved = resolved === null ||
3216
+ (oldAbs > 1 && resolved <= 1) ||
3217
+ isMisresolvedAfterStructuralChange(oldDoc, newDoc, oldAbs, resolved);
3218
+ if (!misresolved) {
3219
+ return resolved
3220
+ }
3221
+ const recovered = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
3222
+ return recovered !== null ? recovered : resolved
3223
+ };
3224
+
3200
3225
  /**
3201
3226
  * @param {import('prosemirror-state').Transaction} tr
3202
3227
  * @param {ReturnType<typeof getRelativeSelection>} relSel
@@ -3249,28 +3274,17 @@ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
3249
3274
  relSel.head,
3250
3275
  binding.mapping
3251
3276
  );
3252
- const needsContentFallback = oldDoc != null &&
3253
- relSel.absAnchor != null &&
3254
- relSel.absHead != null &&
3255
- (
3256
- anchor === null ||
3257
- head === null ||
3258
- (relSel.absAnchor > 1 && anchor !== null && anchor <= 1) ||
3259
- (relSel.absHead > 1 && head !== null && head <= 1) ||
3260
- isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absAnchor, anchor) ||
3261
- isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absHead, head)
3262
- );
3263
- if (needsContentFallback) {
3264
- anchor = findAbsolutePositionAfterStructuralChange(
3265
- oldDoc,
3266
- tr.doc,
3267
- relSel.absAnchor
3268
- );
3269
- head = findAbsolutePositionAfterStructuralChange(
3270
- oldDoc,
3271
- tr.doc,
3272
- relSel.absHead
3273
- );
3277
+ if (oldDoc != null) {
3278
+ anchor = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absAnchor, anchor);
3279
+ head = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absHead, head);
3280
+ }
3281
+ // Collapse to the surviving endpoint instead of dropping the selection;
3282
+ // an unset selection maps through the full-doc replace to the doc start.
3283
+ if (anchor === null) {
3284
+ anchor = head;
3285
+ }
3286
+ if (head === null) {
3287
+ head = anchor;
3274
3288
  }
3275
3289
  if (anchor !== null && head !== null) {
3276
3290
  tr.setSelection(index.TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
@@ -4686,6 +4700,73 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
4686
4700
  return absPos
4687
4701
  };
4688
4702
 
4703
+ /**
4704
+ * Shallow attrs comparison. Attr values are primitives in most schemas;
4705
+ * non-primitive values fail the check and callers fall back to text matching.
4706
+ *
4707
+ * @param {Object<string, any>} a
4708
+ * @param {Object<string, any>} b
4709
+ * @return {boolean}
4710
+ */
4711
+ const attrsEqual = (a, b) => {
4712
+ if (a === b) {
4713
+ return true
4714
+ }
4715
+ const aKeys = Object.keys(a);
4716
+ return aKeys.length === Object.keys(b).length && aKeys.every((k) => a[k] === b[k])
4717
+ };
4718
+
4719
+ /**
4720
+ * Returns true when any attr deviates from its spec default or has none.
4721
+ * Default-only attrs cannot tell same-type siblings apart.
4722
+ *
4723
+ * @param {import('prosemirror-model').Node} node
4724
+ * @return {boolean}
4725
+ */
4726
+ const hasDistinctiveAttrs = (node) => {
4727
+ const specAttrs = node.type.spec.attrs || {};
4728
+ return Object.keys(node.attrs).some((key) => {
4729
+ const spec = specAttrs[key];
4730
+ return spec == null ||
4731
+ !Object.prototype.hasOwnProperty.call(spec, 'default') ||
4732
+ spec.default !== node.attrs[key]
4733
+ })
4734
+ };
4735
+
4736
+ /**
4737
+ * Remaps a position into a matched block by walking the same child-index path
4738
+ * it had in the old block. A raw byte offset would overshoot into a sibling
4739
+ * inner textblock when the old block contains local keystrokes that are not
4740
+ * yet part of the rebuilt document.
4741
+ *
4742
+ * @param {import('prosemirror-model').ResolvedPos} $oldPos
4743
+ * @param {number} newBlockStart
4744
+ * @param {import('prosemirror-model').Node} newBlock
4745
+ * @return {number|null}
4746
+ */
4747
+ const remapIntoBlock = ($oldPos, newBlockStart, newBlock) => {
4748
+ let pos = newBlockStart + 1;
4749
+ let node = newBlock;
4750
+ for (let depth = 1; depth < $oldPos.depth; depth++) {
4751
+ const idx = $oldPos.index(depth);
4752
+ if (idx >= node.childCount) {
4753
+ return null
4754
+ }
4755
+ for (let i = 0; i < idx; i++) {
4756
+ pos += node.child(i).nodeSize;
4757
+ }
4758
+ pos += 1;
4759
+ node = node.child(idx);
4760
+ if (node.type !== $oldPos.node(depth + 1).type) {
4761
+ return null
4762
+ }
4763
+ }
4764
+ if (!node.isTextblock) {
4765
+ return null
4766
+ }
4767
+ return pos + Math.min($oldPos.parentOffset, node.content.size)
4768
+ };
4769
+
4689
4770
  /**
4690
4771
  * @param {import('prosemirror-model').Node} oldDoc
4691
4772
  * @param {import('prosemirror-model').Node} newDoc
@@ -4706,32 +4787,100 @@ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
4706
4787
  return null
4707
4788
  }
4708
4789
  const targetChild = oldDoc.child(targetIdx);
4709
- const offsetInChild = absPos - pos;
4710
-
4711
- let occurrence = 0;
4712
- for (let i = 0; i <= targetIdx; i++) {
4713
- const child = oldDoc.child(i);
4714
- if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
4715
- occurrence++;
4716
- }
4717
- }
4718
-
4719
- let matchCount = 0;
4720
- let newPos = 0;
4721
- for (let i = 0; i < newDoc.childCount; i++) {
4722
- const child = newDoc.child(i);
4723
- if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
4724
- matchCount++;
4725
- if (matchCount === occurrence) {
4726
- const remapped = newPos + offsetInChild;
4727
- const contentStart = newPos + 1;
4728
- const contentEnd = newPos + child.nodeSize - 1;
4729
- return Math.max(contentStart, Math.min(remapped, contentEnd))
4790
+ const $oldPos = oldDoc.resolve(absPos);
4791
+
4792
+ /**
4793
+ * @param {number} newBlockStart
4794
+ * @param {import('prosemirror-model').Node} newBlock
4795
+ * @return {number|null}
4796
+ */
4797
+ const place = (newBlockStart, newBlock) => {
4798
+ // Positions between top-level blocks carry no inner path; clamp them just
4799
+ // inside the matched block like the previous raw-offset remap did.
4800
+ if ($oldPos.depth === 0) {
4801
+ const remapped = newBlockStart + (absPos - pos);
4802
+ const contentStart = newBlockStart + 1;
4803
+ const contentEnd = newBlockStart + newBlock.nodeSize - 1;
4804
+ return Math.max(contentStart, Math.min(remapped, contentEnd))
4805
+ }
4806
+ return remapIntoBlock($oldPos, newBlockStart, newBlock)
4807
+ };
4808
+
4809
+ /**
4810
+ * Finds the Nth block in newDoc matching `pred`, where N is the number of
4811
+ * matching blocks in oldDoc up to and including the target block.
4812
+ *
4813
+ * @param {function(import('prosemirror-model').Node): boolean} pred
4814
+ * @param {boolean} requireUnique
4815
+ * @return {number|null}
4816
+ */
4817
+ const findByPredicate = (pred, requireUnique = false) => {
4818
+ let occurrence = 0;
4819
+ for (let i = 0; i <= targetIdx; i++) {
4820
+ if (pred(oldDoc.child(i))) {
4821
+ occurrence++;
4730
4822
  }
4731
4823
  }
4732
- newPos += child.nodeSize;
4824
+ let matchCount = 0;
4825
+ let matchStart = -1;
4826
+ let matchBlock = null;
4827
+ let newPos = 0;
4828
+ for (let i = 0; i < newDoc.childCount; i++) {
4829
+ const child = newDoc.child(i);
4830
+ if (pred(child)) {
4831
+ matchCount++;
4832
+ if (matchCount === occurrence) {
4833
+ matchStart = newPos;
4834
+ matchBlock = child;
4835
+ }
4836
+ }
4837
+ newPos += child.nodeSize;
4838
+ }
4839
+ if (matchBlock === null || (requireUnique && (occurrence !== 1 || matchCount !== 1))) {
4840
+ return null
4841
+ }
4842
+ return place(matchStart, matchBlock)
4843
+ };
4844
+
4845
+ /**
4846
+ * @param {import('prosemirror-model').Node} child
4847
+ * @return {boolean}
4848
+ */
4849
+ const sameTypeAndAttrs = (child) =>
4850
+ child.type === targetChild.type && attrsEqual(child.attrs, targetChild.attrs);
4851
+ const oldText = targetChild.textContent;
4852
+
4853
+ const byAll = findByPredicate((child) => sameTypeAndAttrs(child) && child.textContent === oldText);
4854
+ if (byAll !== null) {
4855
+ return byAll
4856
+ }
4857
+
4858
+ // Text must be matched before attrs: after a remote attr-only edit, the
4859
+ // attrs pass would steer the cursor into a sibling that kept the old attrs.
4860
+ const byText = findByPredicate(
4861
+ (child) => child.type === targetChild.type && child.textContent === oldText
4862
+ );
4863
+ if (byText !== null) {
4864
+ return byText
4865
+ }
4866
+
4867
+ // In-flight local typing diverges the text between both docs. Distinctive
4868
+ // attrs still identify the block; default-only attrs match every sibling.
4869
+ if (hasDistinctiveAttrs(targetChild)) {
4870
+ const byAttrs = findByPredicate(sameTypeAndAttrs, true);
4871
+ if (byAttrs !== null) {
4872
+ return byAttrs
4873
+ }
4733
4874
  }
4734
- return null
4875
+
4876
+ // Trailing in-flight keystrokes leave a prefix relation between old and new
4877
+ // text. Empty text is a prefix of everything and must never match.
4878
+ return findByPredicate(
4879
+ (child) => sameTypeAndAttrs(child) &&
4880
+ oldText !== '' && child.textContent !== '' &&
4881
+ (oldText.startsWith(child.textContent) || child.textContent.startsWith(oldText)),
4882
+ true
4883
+ )
4735
4884
  };
4736
4885
 
4737
4886
  /**
@@ -4792,16 +4941,27 @@ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs)
4792
4941
  }
4793
4942
  const $old = oldDoc.resolve(oldAbs);
4794
4943
  const $new = newDoc.resolve(resolvedAbs);
4795
- if (!$old.parent.isTextblock || !$new.parent.isTextblock) {
4944
+ if (!$old.parent.isTextblock) {
4796
4945
  return false
4797
4946
  }
4947
+ // A textblock cursor cannot legitimately resolve into a non-textblock;
4948
+ // a structural reorder replaced the block via delete + insert.
4949
+ if (!$new.parent.isTextblock) {
4950
+ return true
4951
+ }
4798
4952
  if ($old.parent.textContent !== $new.parent.textContent) {
4799
4953
  return true
4800
4954
  }
4801
4955
  if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
4802
4956
  return true
4803
4957
  }
4804
- if ($old.parentOffset === 0 && $new.parentOffset === 0) {
4958
+ const bothAtStart = $old.parentOffset === 0 && $new.parentOffset === 0;
4959
+ // A changed offset, type or attrs hints at a same-text sibling. When all
4960
+ // of them agree there is no signal left and the Yjs resolution must win.
4961
+ const suspicious = $old.parentOffset !== $new.parentOffset ||
4962
+ $old.parent.type !== $new.parent.type ||
4963
+ !attrsEqual($old.parent.attrs, $new.parent.attrs);
4964
+ if (bothAtStart || suspicious) {
4805
4965
  const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
4806
4966
  return expected !== null && expected !== resolvedAbs
4807
4967
  }
@@ -4980,8 +5140,18 @@ const yCursorPlugin = (
4980
5140
  apply (tr, prevState, oldState, newState) {
4981
5141
  const ystate = ySyncPluginKey.getState(newState);
4982
5142
  const yCursorState = tr.getMeta(yCursorPluginKey);
5143
+ const isRemoteChange = ystate && ystate.isChangeOrigin;
4983
5144
  if (
4984
- (ystate && ystate.isChangeOrigin) ||
5145
+ tr.docChanged &&
5146
+ !isRemoteChange &&
5147
+ isStructuralTransaction(tr, oldState.doc)
5148
+ ) {
5149
+ // The ProseMirror document leads the Yjs mapping during local moves.
5150
+ // Hide stale awareness until the collaborator publishes its new cursor.
5151
+ return useEditorStore.DecorationSet.empty
5152
+ }
5153
+ if (
5154
+ isRemoteChange ||
4985
5155
  (yCursorState && yCursorState.awarenessUpdated)
4986
5156
  ) {
4987
5157
  return createDecorations(
@@ -4993,15 +5163,6 @@ const yCursorPlugin = (
4993
5163
  )
4994
5164
  }
4995
5165
  if (tr.docChanged) {
4996
- if (isStructuralTransaction(tr, oldState.doc)) {
4997
- return createDecorations(
4998
- newState,
4999
- awareness,
5000
- awarenessStateFilter,
5001
- cursorBuilder,
5002
- selectionBuilder
5003
- )
5004
- }
5005
5166
  return prevState.map(tr.mapping, tr.doc)
5006
5167
  }
5007
5168
  return prevState
@@ -16841,4 +17002,4 @@ exports.scrollHandler = scrollHandler;
16841
17002
  exports.useDeletedArticles = useDeletedArticles;
16842
17003
  exports.useEditorWarnings = useEditorWarnings;
16843
17004
  exports.useFetchKbArticles = useFetchKbArticles;
16844
- //# sourceMappingURL=chunk-BXZTd0jZ.cjs.js.map
17005
+ //# sourceMappingURL=chunk-Ch4y-VZi.cjs.js.map