@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.
package/dist/Editor.js CHANGED
@@ -12,7 +12,7 @@ import Label from '@bigbinary/neetoui/Label';
12
12
  import { createPortal } from 'react-dom';
13
13
  import { is, intersection, difference, union, isEmpty, isNil, mergeRight, assoc, equals } from 'ramda';
14
14
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
15
- import { B as BulletList$1, i as index_default, a as index_default$1, R as Resizable, b as index_default$2, L as Link$1, s as scrollHandler, c as index_default$3, O as OrderedList$1, d as index_default$4, f as fuzzySearch, T as Table$2, e as TaskList, g as TaskItem, C as CharacterCount, h as index_default$5, j as index_default$6, k as TextStyle, l as index_default$7, F as Focus, m as index_default$8, n as TableRow, o as TableHeader, p as TableCell, q as Color, r as ListItem, t as index_default$9, u as index_default$a, v as useFetchKbArticles, M as MODE, w as createArticleOptions, x as MODAL_LEFT_OFFSET, y as MODAL_TOP_OFFSET, z as MODAL_TRANSFORM_Y, A as MODAL_TRANSFORM_X, D as MODAL_BOTTOM_MARGIN, E as buildArticleFullUrl, G as useEditorWarnings, H as useDeletedArticles } from './chunk-Bmhz4YEy.js';
15
+ import { B as BulletList$1, i as index_default, a as index_default$1, R as Resizable, b as index_default$2, L as Link$1, s as scrollHandler, c as index_default$3, O as OrderedList$1, d as index_default$4, f as fuzzySearch, T as Table$2, e as TaskList, g as TaskItem, C as CharacterCount, h as index_default$5, j as index_default$6, k as TextStyle, l as index_default$7, F as Focus, m as index_default$8, n as TableRow, o as TableHeader, p as TableCell, q as Color, r as ListItem, t as index_default$9, u as index_default$a, v as useFetchKbArticles, M as MODE, w as createArticleOptions, x as MODAL_LEFT_OFFSET, y as MODAL_TOP_OFFSET, z as MODAL_TRANSFORM_Y, A as MODAL_TRANSFORM_X, D as MODAL_BOTTOM_MARGIN, E as buildArticleFullUrl, G as useEditorWarnings, H as useDeletedArticles } from './chunk-Be5ikGpm.js';
16
16
  import { n } from './chunk-C2dztUxs.js';
17
17
  import { b as buildLevelsFromOptions, v as validateAndFormatUrl, g as getEditorStyles, c as transformPastedHTML, h as handlePaste, d as clipboardTextParser, s as setInitialPosition } from './chunk-C_jttcYg.js';
18
18
  import ReactDOM from 'react-dom/server';
@@ -17,7 +17,7 @@ import 'zustand';
17
17
  import 'classnames';
18
18
  import './chunk-DDwycKmK.js';
19
19
  import '@bigbinary/neetoui/Label';
20
- import './chunk-Bmhz4YEy.js';
20
+ import './chunk-Be5ikGpm.js';
21
21
  import './chunk-Qg4uQqj3.js';
22
22
  import './chunk-BFTU3MAI.js';
23
23
  import '@bigbinary/neeto-commons-frontend/initializers';
@@ -3168,6 +3168,31 @@ const ySyncPlugin = (yXmlFragment, {
3168
3168
  return plugin
3169
3169
  };
3170
3170
 
3171
+ /**
3172
+ * Resolves one text-selection endpoint after a remote update. Falls back to
3173
+ * content-based matching when the Yjs resolution is missing or lands in the
3174
+ * wrong block, and keeps the Yjs resolution when the fallback finds nothing.
3175
+ *
3176
+ * @param {import('prosemirror-model').Node} newDoc
3177
+ * @param {import('prosemirror-model').Node} oldDoc
3178
+ * @param {number|null|undefined} oldAbs
3179
+ * @param {number|null} resolved
3180
+ * @return {number|null}
3181
+ */
3182
+ const recoverSelectionEndpoint = (newDoc, oldDoc, oldAbs, resolved) => {
3183
+ if (oldAbs == null) {
3184
+ return resolved
3185
+ }
3186
+ const misresolved = resolved === null ||
3187
+ (oldAbs > 1 && resolved <= 1) ||
3188
+ isMisresolvedAfterStructuralChange(oldDoc, newDoc, oldAbs, resolved);
3189
+ if (!misresolved) {
3190
+ return resolved
3191
+ }
3192
+ const recovered = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
3193
+ return recovered !== null ? recovered : resolved
3194
+ };
3195
+
3171
3196
  /**
3172
3197
  * @param {import('prosemirror-state').Transaction} tr
3173
3198
  * @param {ReturnType<typeof getRelativeSelection>} relSel
@@ -3220,28 +3245,17 @@ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
3220
3245
  relSel.head,
3221
3246
  binding.mapping
3222
3247
  );
3223
- const needsContentFallback = oldDoc != null &&
3224
- relSel.absAnchor != null &&
3225
- relSel.absHead != null &&
3226
- (
3227
- anchor === null ||
3228
- head === null ||
3229
- (relSel.absAnchor > 1 && anchor !== null && anchor <= 1) ||
3230
- (relSel.absHead > 1 && head !== null && head <= 1) ||
3231
- isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absAnchor, anchor) ||
3232
- isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absHead, head)
3233
- );
3234
- if (needsContentFallback) {
3235
- anchor = findAbsolutePositionAfterStructuralChange(
3236
- oldDoc,
3237
- tr.doc,
3238
- relSel.absAnchor
3239
- );
3240
- head = findAbsolutePositionAfterStructuralChange(
3241
- oldDoc,
3242
- tr.doc,
3243
- relSel.absHead
3244
- );
3248
+ if (oldDoc != null) {
3249
+ anchor = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absAnchor, anchor);
3250
+ head = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absHead, head);
3251
+ }
3252
+ // Collapse to the surviving endpoint instead of dropping the selection;
3253
+ // an unset selection maps through the full-doc replace to the doc start.
3254
+ if (anchor === null) {
3255
+ anchor = head;
3256
+ }
3257
+ if (head === null) {
3258
+ head = anchor;
3245
3259
  }
3246
3260
  if (anchor !== null && head !== null) {
3247
3261
  tr.setSelection(TextSelection$1.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
@@ -4657,6 +4671,73 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
4657
4671
  return absPos
4658
4672
  };
4659
4673
 
4674
+ /**
4675
+ * Shallow attrs comparison. Attr values are primitives in most schemas;
4676
+ * non-primitive values fail the check and callers fall back to text matching.
4677
+ *
4678
+ * @param {Object<string, any>} a
4679
+ * @param {Object<string, any>} b
4680
+ * @return {boolean}
4681
+ */
4682
+ const attrsEqual = (a, b) => {
4683
+ if (a === b) {
4684
+ return true
4685
+ }
4686
+ const aKeys = Object.keys(a);
4687
+ return aKeys.length === Object.keys(b).length && aKeys.every((k) => a[k] === b[k])
4688
+ };
4689
+
4690
+ /**
4691
+ * Returns true when any attr deviates from its spec default or has none.
4692
+ * Default-only attrs cannot tell same-type siblings apart.
4693
+ *
4694
+ * @param {import('prosemirror-model').Node} node
4695
+ * @return {boolean}
4696
+ */
4697
+ const hasDistinctiveAttrs = (node) => {
4698
+ const specAttrs = node.type.spec.attrs || {};
4699
+ return Object.keys(node.attrs).some((key) => {
4700
+ const spec = specAttrs[key];
4701
+ return spec == null ||
4702
+ !Object.prototype.hasOwnProperty.call(spec, 'default') ||
4703
+ spec.default !== node.attrs[key]
4704
+ })
4705
+ };
4706
+
4707
+ /**
4708
+ * Remaps a position into a matched block by walking the same child-index path
4709
+ * it had in the old block. A raw byte offset would overshoot into a sibling
4710
+ * inner textblock when the old block contains local keystrokes that are not
4711
+ * yet part of the rebuilt document.
4712
+ *
4713
+ * @param {import('prosemirror-model').ResolvedPos} $oldPos
4714
+ * @param {number} newBlockStart
4715
+ * @param {import('prosemirror-model').Node} newBlock
4716
+ * @return {number|null}
4717
+ */
4718
+ const remapIntoBlock = ($oldPos, newBlockStart, newBlock) => {
4719
+ let pos = newBlockStart + 1;
4720
+ let node = newBlock;
4721
+ for (let depth = 1; depth < $oldPos.depth; depth++) {
4722
+ const idx = $oldPos.index(depth);
4723
+ if (idx >= node.childCount) {
4724
+ return null
4725
+ }
4726
+ for (let i = 0; i < idx; i++) {
4727
+ pos += node.child(i).nodeSize;
4728
+ }
4729
+ pos += 1;
4730
+ node = node.child(idx);
4731
+ if (node.type !== $oldPos.node(depth + 1).type) {
4732
+ return null
4733
+ }
4734
+ }
4735
+ if (!node.isTextblock) {
4736
+ return null
4737
+ }
4738
+ return pos + Math.min($oldPos.parentOffset, node.content.size)
4739
+ };
4740
+
4660
4741
  /**
4661
4742
  * @param {import('prosemirror-model').Node} oldDoc
4662
4743
  * @param {import('prosemirror-model').Node} newDoc
@@ -4677,32 +4758,100 @@ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
4677
4758
  return null
4678
4759
  }
4679
4760
  const targetChild = oldDoc.child(targetIdx);
4680
- const offsetInChild = absPos - pos;
4681
-
4682
- let occurrence = 0;
4683
- for (let i = 0; i <= targetIdx; i++) {
4684
- const child = oldDoc.child(i);
4685
- if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
4686
- occurrence++;
4687
- }
4688
- }
4689
-
4690
- let matchCount = 0;
4691
- let newPos = 0;
4692
- for (let i = 0; i < newDoc.childCount; i++) {
4693
- const child = newDoc.child(i);
4694
- if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
4695
- matchCount++;
4696
- if (matchCount === occurrence) {
4697
- const remapped = newPos + offsetInChild;
4698
- const contentStart = newPos + 1;
4699
- const contentEnd = newPos + child.nodeSize - 1;
4700
- return Math.max(contentStart, Math.min(remapped, contentEnd))
4761
+ const $oldPos = oldDoc.resolve(absPos);
4762
+
4763
+ /**
4764
+ * @param {number} newBlockStart
4765
+ * @param {import('prosemirror-model').Node} newBlock
4766
+ * @return {number|null}
4767
+ */
4768
+ const place = (newBlockStart, newBlock) => {
4769
+ // Positions between top-level blocks carry no inner path; clamp them just
4770
+ // inside the matched block like the previous raw-offset remap did.
4771
+ if ($oldPos.depth === 0) {
4772
+ const remapped = newBlockStart + (absPos - pos);
4773
+ const contentStart = newBlockStart + 1;
4774
+ const contentEnd = newBlockStart + newBlock.nodeSize - 1;
4775
+ return Math.max(contentStart, Math.min(remapped, contentEnd))
4776
+ }
4777
+ return remapIntoBlock($oldPos, newBlockStart, newBlock)
4778
+ };
4779
+
4780
+ /**
4781
+ * Finds the Nth block in newDoc matching `pred`, where N is the number of
4782
+ * matching blocks in oldDoc up to and including the target block.
4783
+ *
4784
+ * @param {function(import('prosemirror-model').Node): boolean} pred
4785
+ * @param {boolean} requireUnique
4786
+ * @return {number|null}
4787
+ */
4788
+ const findByPredicate = (pred, requireUnique = false) => {
4789
+ let occurrence = 0;
4790
+ for (let i = 0; i <= targetIdx; i++) {
4791
+ if (pred(oldDoc.child(i))) {
4792
+ occurrence++;
4701
4793
  }
4702
4794
  }
4703
- newPos += child.nodeSize;
4795
+ let matchCount = 0;
4796
+ let matchStart = -1;
4797
+ let matchBlock = null;
4798
+ let newPos = 0;
4799
+ for (let i = 0; i < newDoc.childCount; i++) {
4800
+ const child = newDoc.child(i);
4801
+ if (pred(child)) {
4802
+ matchCount++;
4803
+ if (matchCount === occurrence) {
4804
+ matchStart = newPos;
4805
+ matchBlock = child;
4806
+ }
4807
+ }
4808
+ newPos += child.nodeSize;
4809
+ }
4810
+ if (matchBlock === null || (requireUnique && (occurrence !== 1 || matchCount !== 1))) {
4811
+ return null
4812
+ }
4813
+ return place(matchStart, matchBlock)
4814
+ };
4815
+
4816
+ /**
4817
+ * @param {import('prosemirror-model').Node} child
4818
+ * @return {boolean}
4819
+ */
4820
+ const sameTypeAndAttrs = (child) =>
4821
+ child.type === targetChild.type && attrsEqual(child.attrs, targetChild.attrs);
4822
+ const oldText = targetChild.textContent;
4823
+
4824
+ const byAll = findByPredicate((child) => sameTypeAndAttrs(child) && child.textContent === oldText);
4825
+ if (byAll !== null) {
4826
+ return byAll
4827
+ }
4828
+
4829
+ // Text must be matched before attrs: after a remote attr-only edit, the
4830
+ // attrs pass would steer the cursor into a sibling that kept the old attrs.
4831
+ const byText = findByPredicate(
4832
+ (child) => child.type === targetChild.type && child.textContent === oldText
4833
+ );
4834
+ if (byText !== null) {
4835
+ return byText
4836
+ }
4837
+
4838
+ // In-flight local typing diverges the text between both docs. Distinctive
4839
+ // attrs still identify the block; default-only attrs match every sibling.
4840
+ if (hasDistinctiveAttrs(targetChild)) {
4841
+ const byAttrs = findByPredicate(sameTypeAndAttrs, true);
4842
+ if (byAttrs !== null) {
4843
+ return byAttrs
4844
+ }
4704
4845
  }
4705
- return null
4846
+
4847
+ // Trailing in-flight keystrokes leave a prefix relation between old and new
4848
+ // text. Empty text is a prefix of everything and must never match.
4849
+ return findByPredicate(
4850
+ (child) => sameTypeAndAttrs(child) &&
4851
+ oldText !== '' && child.textContent !== '' &&
4852
+ (oldText.startsWith(child.textContent) || child.textContent.startsWith(oldText)),
4853
+ true
4854
+ )
4706
4855
  };
4707
4856
 
4708
4857
  /**
@@ -4763,16 +4912,27 @@ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs)
4763
4912
  }
4764
4913
  const $old = oldDoc.resolve(oldAbs);
4765
4914
  const $new = newDoc.resolve(resolvedAbs);
4766
- if (!$old.parent.isTextblock || !$new.parent.isTextblock) {
4915
+ if (!$old.parent.isTextblock) {
4767
4916
  return false
4768
4917
  }
4918
+ // A textblock cursor cannot legitimately resolve into a non-textblock;
4919
+ // a structural reorder replaced the block via delete + insert.
4920
+ if (!$new.parent.isTextblock) {
4921
+ return true
4922
+ }
4769
4923
  if ($old.parent.textContent !== $new.parent.textContent) {
4770
4924
  return true
4771
4925
  }
4772
4926
  if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
4773
4927
  return true
4774
4928
  }
4775
- if ($old.parentOffset === 0 && $new.parentOffset === 0) {
4929
+ const bothAtStart = $old.parentOffset === 0 && $new.parentOffset === 0;
4930
+ // A changed offset, type or attrs hints at a same-text sibling. When all
4931
+ // of them agree there is no signal left and the Yjs resolution must win.
4932
+ const suspicious = $old.parentOffset !== $new.parentOffset ||
4933
+ $old.parent.type !== $new.parent.type ||
4934
+ !attrsEqual($old.parent.attrs, $new.parent.attrs);
4935
+ if (bothAtStart || suspicious) {
4776
4936
  const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
4777
4937
  return expected !== null && expected !== resolvedAbs
4778
4938
  }
@@ -4951,8 +5111,18 @@ const yCursorPlugin = (
4951
5111
  apply (tr, prevState, oldState, newState) {
4952
5112
  const ystate = ySyncPluginKey.getState(newState);
4953
5113
  const yCursorState = tr.getMeta(yCursorPluginKey);
5114
+ const isRemoteChange = ystate && ystate.isChangeOrigin;
4954
5115
  if (
4955
- (ystate && ystate.isChangeOrigin) ||
5116
+ tr.docChanged &&
5117
+ !isRemoteChange &&
5118
+ isStructuralTransaction(tr, oldState.doc)
5119
+ ) {
5120
+ // The ProseMirror document leads the Yjs mapping during local moves.
5121
+ // Hide stale awareness until the collaborator publishes its new cursor.
5122
+ return DecorationSet.empty
5123
+ }
5124
+ if (
5125
+ isRemoteChange ||
4956
5126
  (yCursorState && yCursorState.awarenessUpdated)
4957
5127
  ) {
4958
5128
  return createDecorations(
@@ -4964,15 +5134,6 @@ const yCursorPlugin = (
4964
5134
  )
4965
5135
  }
4966
5136
  if (tr.docChanged) {
4967
- if (isStructuralTransaction(tr, oldState.doc)) {
4968
- return createDecorations(
4969
- newState,
4970
- awareness,
4971
- awarenessStateFilter,
4972
- cursorBuilder,
4973
- selectionBuilder
4974
- )
4975
- }
4976
5137
  return prevState.map(tr.mapping, tr.doc)
4977
5138
  }
4978
5139
  return prevState
@@ -16774,4 +16935,4 @@ var fuzzySearch = function fuzzySearch(items, query) {
16774
16935
  };
16775
16936
 
16776
16937
  export { MODAL_TRANSFORM_X as A, BulletList as B, CharacterCount as C, MODAL_BOTTOM_MARGIN as D, buildArticleFullUrl as E, Focus as F, useEditorWarnings as G, useDeletedArticles as H, Link as L, MODE as M, OrderedList as O, Resizable as R, Table as T, index_default$1 as a, index_default$4 as b, index_default as c, index_default$7 as d, TaskList as e, fuzzySearch as f, TaskItem as g, index_default$a as h, index_default$2 as i, index_default$6 as j, TextStyle as k, index_default$5 as l, index_default$3 as m, TableRow as n, TableHeader as o, TableCell as p, Color as q, ListItem as r, scrollHandler as s, index_default$9 as t, index_default$8 as u, useFetchKbArticles as v, createArticleOptions as w, MODAL_LEFT_OFFSET as x, MODAL_TOP_OFFSET as y, MODAL_TRANSFORM_Y as z };
16777
- //# sourceMappingURL=chunk-Bmhz4YEy.js.map
16938
+ //# sourceMappingURL=chunk-Be5ikGpm.js.map