@domternal/core 1.0.3 → 1.1.0

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/README.md CHANGED
@@ -29,6 +29,9 @@ the DOM, so it runs anywhere without it.
29
29
  pnpm add linkedom
30
30
  ```
31
31
 
32
+ The 1.1 release installs `@domternal/pm` in the range `>=1.1.0 <2.0.0`.
33
+ Use matching 1.1 versions of the framework wrappers and extensions when upgrading.
34
+
32
35
  ### One copy of the core, and of ProseMirror
33
36
 
34
37
  ProseMirror compares classes by identity, and so does this package. Two copies of
@@ -97,6 +100,25 @@ const editor = new Editor({
97
100
  });
98
101
  ```
99
102
 
103
+ ## Moving an existing editor
104
+
105
+ Use `editor.adoptDom(element)` to move the existing editor view into a new mount
106
+ element while preserving its document, selection, undo history, and plugin state:
107
+
108
+ ```ts
109
+ editor.adoptDom(document.getElementById('next-editor-mount')!);
110
+ ```
111
+
112
+ The `adopt` event reports a changed DOM context and lets host-dependent UI rebind.
113
+ The original `mount` and `create` events remain creation-time events; they are not
114
+ repeated by adoption. Calling `adoptDom` with an unchanged DOM context is a no-op.
115
+ React and Vue wrappers use this path when attaching an existing editor.
116
+
117
+ Custom plugin views can use `createAdoptablePluginView(editor, view, createView)`
118
+ to recreate their DOM bindings after adoption while retaining plugin state. The
119
+ factory must return a ProseMirror plugin view and clean up its own DOM resources
120
+ in `destroy`. The event payload type is exported as `AdoptEventProps`.
121
+
100
122
  ## Presets
101
123
 
102
124
  `preset: 'notion'` switches the editor to the Notion-style experience in one option. It paints
package/dist/index.cjs CHANGED
@@ -3664,6 +3664,7 @@ var Editor = class _Editor extends EventEmitter {
3664
3664
  * editor itself added, never one the consumer wrote.
3665
3665
  */
3666
3666
  _presetClassHost = null;
3667
+ _domContext = null;
3667
3668
  /**
3668
3669
  * Creates a new Editor instance
3669
3670
  *
@@ -3742,18 +3743,50 @@ var Editor = class _Editor extends EventEmitter {
3742
3743
  * wrappers call it again after adopting the view's DOM: they construct
3743
3744
  * the editor in a detached element, so the creation-time run cannot see
3744
3745
  * the host yet. Idempotent; a no-op for any other preset. Only a class
3745
- * added here is removed again on destroy.
3746
+ * added here is removed when the host changes or the editor is destroyed.
3746
3747
  */
3747
3748
  adoptPresetClass() {
3748
- if (this.options.preset !== "notion" || this._presetClassHost) {
3749
+ if (this.options.preset !== "notion") {
3749
3750
  return;
3750
3751
  }
3751
3752
  const host = this.view.dom.closest(".dm-editor");
3753
+ if (this._presetClassHost && this._presetClassHost !== host) {
3754
+ this._presetClassHost.classList.remove("dm-notion-mode");
3755
+ this._presetClassHost = null;
3756
+ }
3752
3757
  if (host && !host.classList.contains("dm-notion-mode")) {
3753
3758
  host.classList.add("dm-notion-mode");
3754
3759
  this._presetClassHost = host;
3755
3760
  }
3756
3761
  }
3762
+ /**
3763
+ * Moves the existing view into a mount element without recreating its state.
3764
+ * Host-dependent UI receives an `adopt` event after the move. Calling this
3765
+ * again with the same DOM context is a no-op. The original `mount` and
3766
+ * `create` events remain creation-time events.
3767
+ */
3768
+ adoptDom(element) {
3769
+ if (this._isDestroyed) return this;
3770
+ if (this.view.dom.parentElement !== element) {
3771
+ element.appendChild(this.view.dom);
3772
+ }
3773
+ this.view.updateRoot();
3774
+ this.adoptPresetClass();
3775
+ const previous = this._domContext;
3776
+ const current = this.captureDomContext();
3777
+ this._domContext = current;
3778
+ if (previous?.element === current.element && previous.host === current.host && previous.hostParent === current.hostParent && previous.root === current.root && previous.connected === current.connected) {
3779
+ return this;
3780
+ }
3781
+ this.emit("adopt", {
3782
+ editor: this,
3783
+ view: this.view,
3784
+ element,
3785
+ host: current.host,
3786
+ previousHost: previous?.host ?? null
3787
+ });
3788
+ return this;
3789
+ }
3757
3790
  /**
3758
3791
  * Checks if the editor content is empty
3759
3792
  */
@@ -4101,6 +4134,7 @@ var Editor = class _Editor extends EventEmitter {
4101
4134
  this._presetClassHost = null;
4102
4135
  }
4103
4136
  this.view.destroy();
4137
+ this._domContext = null;
4104
4138
  this._extensionManager.destroy();
4105
4139
  this.removeAllListeners();
4106
4140
  this._isDestroyed = true;
@@ -4203,6 +4237,7 @@ var Editor = class _Editor extends EventEmitter {
4203
4237
  });
4204
4238
  this._isViewConstructing = false;
4205
4239
  this.adoptPresetClass();
4240
+ this._domContext = this.captureDomContext();
4206
4241
  this.emit("mount", { editor: this, view: this.view });
4207
4242
  this.options.onMount?.({ editor: this, view: this.view });
4208
4243
  this.commandManager = new CommandManager(this);
@@ -4218,6 +4253,16 @@ var Editor = class _Editor extends EventEmitter {
4218
4253
  this.options.onCreate?.({ editor: this });
4219
4254
  this._extensionManager.callOnCreate();
4220
4255
  }
4256
+ captureDomContext() {
4257
+ const host = this.view.dom.closest(".dm-editor");
4258
+ return {
4259
+ element: this.view.dom.parentElement,
4260
+ host,
4261
+ hostParent: host?.parentElement ?? null,
4262
+ root: this.view.dom.getRootNode(),
4263
+ connected: this.view.dom.isConnected
4264
+ };
4265
+ }
4221
4266
  /**
4222
4267
  * Builds the dispatchTransaction prop. Plugin views can dispatch synchronously
4223
4268
  * inside EditorView's constructor, before `editor.view` is assigned; ProseMirror
@@ -4438,6 +4483,36 @@ function refocusEditorAfterCommand(view) {
4438
4483
  });
4439
4484
  }
4440
4485
 
4486
+ // src/utils/createAdoptablePluginView.ts
4487
+ function createAdoptablePluginView(editor, view, createView) {
4488
+ let currentView = view;
4489
+ let binding = createView(currentView);
4490
+ let destroyed = false;
4491
+ const rebind = () => {
4492
+ const previous = binding;
4493
+ binding = void 0;
4494
+ previous?.destroy?.();
4495
+ if (!destroyed) {
4496
+ binding = createView(currentView);
4497
+ }
4498
+ };
4499
+ editor?.on("adopt", rebind);
4500
+ return {
4501
+ update(nextView, previousState) {
4502
+ currentView = nextView;
4503
+ binding?.update?.(nextView, previousState);
4504
+ },
4505
+ destroy() {
4506
+ if (destroyed) return;
4507
+ destroyed = true;
4508
+ editor?.off("adopt", rebind);
4509
+ const previous = binding;
4510
+ binding = void 0;
4511
+ previous?.destroy?.();
4512
+ }
4513
+ };
4514
+ }
4515
+
4441
4516
  // src/utils/defaultBubbleContexts.ts
4442
4517
  var NOTION_TEXT_CONTEXT = Object.freeze([
4443
4518
  "ai",
@@ -5831,7 +5906,7 @@ function createFloatingMenuPlugin(options) {
5831
5906
  return false;
5832
5907
  }
5833
5908
  },
5834
- view: (editorView) => {
5909
+ view: (view) => createAdoptablePluginView(editor, view, (editorView) => {
5835
5910
  editorEl = editorView.dom.closest(".dm-editor");
5836
5911
  if (editorEl && element.parentElement !== editorEl) {
5837
5912
  editorEl.appendChild(element);
@@ -5867,11 +5942,11 @@ function createFloatingMenuPlugin(options) {
5867
5942
  editor.on("focus", onFocus);
5868
5943
  editor.on("blur", onBlur);
5869
5944
  return {
5870
- update: (view) => {
5871
- if (isVisibleNow(view)) updatePosition(view);
5945
+ update: (view2) => {
5946
+ if (isVisibleNow(view2)) updatePosition(view2);
5872
5947
  else {
5873
5948
  hideMenu();
5874
- clearTriggeredFlag(view);
5949
+ clearTriggeredFlag(view2);
5875
5950
  }
5876
5951
  },
5877
5952
  destroy: () => {
@@ -5889,7 +5964,7 @@ function createFloatingMenuPlugin(options) {
5889
5964
  editorEl = null;
5890
5965
  }
5891
5966
  };
5892
- }
5967
+ })
5893
5968
  });
5894
5969
  }
5895
5970
 
@@ -7953,9 +8028,9 @@ function autolinkPlugin(options) {
7953
8028
  }
7954
8029
  const { state } = view;
7955
8030
  const $from = state.doc.resolve(from);
8031
+ const lookbackStart = Math.max(0, $from.parentOffset - 500);
7956
8032
  const textBefore = $from.parent.textBetween(
7957
- Math.max(0, $from.parentOffset - 500),
7958
- // Look back max 500 chars
8033
+ lookbackStart,
7959
8034
  $from.parentOffset,
7960
8035
  void 0,
7961
8036
  "\uFFFC"
@@ -7986,15 +8061,35 @@ function autolinkPlugin(options) {
7986
8061
  return false;
7987
8062
  }
7988
8063
  const blockStart = from - $from.parentOffset;
7989
- const linkStart = blockStart + lastMatch.start;
7990
- const linkEnd = blockStart + lastMatch.end;
8064
+ const linkStart = blockStart + lookbackStart + lastMatch.start;
8065
+ const linkEnd = blockStart + lookbackStart + lastMatch.end;
7991
8066
  const $linkStart = state.doc.resolve(linkStart);
7992
- if ($linkStart.marks().some((m) => m.type === type)) {
8067
+ const existingLink = $linkStart.marks().find((mark2) => mark2.type === type);
8068
+ if (existingLink && state.doc.resolve(to).nodeAfter?.marks.some((mark2) => mark2.eq(existingLink))) {
7993
8069
  return false;
7994
8070
  }
7995
8071
  const tr = state.tr;
7996
- tr.addMark(linkStart, linkEnd, type.create({ href }));
8072
+ if (!existingLink) {
8073
+ tr.addMark(linkStart, linkEnd, type.create({ href }));
8074
+ } else {
8075
+ let markedEnd = linkStart;
8076
+ state.doc.nodesBetween(linkStart, linkEnd, (node, pos) => {
8077
+ if (node.isText && pos <= markedEnd && node.marks.some((mark2) => mark2.eq(existingLink))) {
8078
+ markedEnd = Math.min(pos + node.nodeSize, linkEnd);
8079
+ }
8080
+ });
8081
+ if (markedEnd > linkStart && markedEnd < linkEnd) {
8082
+ const prefix = state.doc.textBetween(linkStart, markedEnd);
8083
+ const prefixMatch = linkifyjs.find(prefix, { defaultProtocol })[0];
8084
+ if (prefixMatch?.start === 0 && prefixMatch.end === prefix.length && prefixMatch.href === existingLink.attrs["href"]) {
8085
+ tr.addMark(linkStart, linkEnd, type.create({ ...existingLink.attrs, href }));
8086
+ }
8087
+ }
8088
+ }
8089
+ const insertionMarks = type.removeFromSet(state.storedMarks ?? $from.marks());
8090
+ tr.setStoredMarks(insertionMarks);
7997
8091
  tr.insertText(text, from, to);
8092
+ tr.setStoredMarks(insertionMarks);
7998
8093
  view.dispatch(tr);
7999
8094
  return true;
8000
8095
  }
@@ -9730,11 +9825,11 @@ var SelectionDecoration = Extension.create(
9730
9825
  return false;
9731
9826
  }
9732
9827
  }
9733
- const { from, to } = view.state.selection;
9828
+ const { from, to, $from } = view.state.selection;
9734
9829
  if (from !== to) {
9735
9830
  view.dispatch(
9736
9831
  view.state.tr.setSelection(
9737
- state.TextSelection.create(view.state.doc, from)
9832
+ state.TextSelection.between($from, $from, 1)
9738
9833
  )
9739
9834
  );
9740
9835
  }
@@ -11040,7 +11135,7 @@ function createBubbleMenuPlugin(options) {
11040
11135
  return { visible, from, to };
11041
11136
  }
11042
11137
  },
11043
- view: (editorView) => {
11138
+ view: (view) => createAdoptablePluginView(editor, view, (editorView) => {
11044
11139
  const editorEl = editorView.dom.closest(".dm-editor");
11045
11140
  if (editorEl && element.parentElement !== editorEl) {
11046
11141
  editorEl.appendChild(element);
@@ -11104,16 +11199,16 @@ function createBubbleMenuPlugin(options) {
11104
11199
  editor.on("focus", onFocus);
11105
11200
  editor.on("blur", onBlur);
11106
11201
  return {
11107
- update: (view, prevState) => {
11108
- if (view.composing) return;
11109
- if (!view.editable) {
11202
+ update: (view2, prevState) => {
11203
+ if (view2.composing) return;
11204
+ if (!view2.editable) {
11110
11205
  hideMenu();
11111
11206
  return;
11112
11207
  }
11113
- const state = pluginKey.getState(view.state);
11208
+ const state = pluginKey.getState(view2.state);
11114
11209
  const prevPluginState = pluginKey.getState(prevState);
11115
11210
  const domShown = element.hasAttribute("data-show") || updateTimeout !== null;
11116
- if (state?.visible === prevPluginState?.visible && state?.from === prevPluginState?.from && state?.to === prevPluginState?.to && !(state?.visible && view.state.doc !== prevState.doc) && domShown === Boolean(state?.visible)) {
11211
+ if (state?.visible === prevPluginState?.visible && state?.from === prevPluginState?.from && state?.to === prevPluginState?.to && !(state?.visible && view2.state.doc !== prevState.doc) && domShown === Boolean(state?.visible)) {
11117
11212
  return;
11118
11213
  }
11119
11214
  if (updateTimeout) {
@@ -11123,10 +11218,10 @@ function createBubbleMenuPlugin(options) {
11123
11218
  if (state?.visible && !mouseDown) {
11124
11219
  if (updateDelay > 0) {
11125
11220
  updateTimeout = setTimeout(() => {
11126
- updatePosition(view, state.from, state.to);
11221
+ updatePosition(view2, state.from, state.to);
11127
11222
  }, updateDelay);
11128
11223
  } else {
11129
- updatePosition(view, state.from, state.to);
11224
+ updatePosition(view2, state.from, state.to);
11130
11225
  }
11131
11226
  } else {
11132
11227
  hideMenu();
@@ -11154,7 +11249,7 @@ function createBubbleMenuPlugin(options) {
11154
11249
  hideMenu();
11155
11250
  }
11156
11251
  };
11157
- }
11252
+ })
11158
11253
  });
11159
11254
  }
11160
11255
  var BubbleMenu = Extension.create({
@@ -11240,7 +11335,7 @@ var StarterKit = Extension.create({
11240
11335
  });
11241
11336
 
11242
11337
  // src/index.ts
11243
- var VERSION = "1.0.3";
11338
+ var VERSION = "1.1.0";
11244
11339
 
11245
11340
  Object.defineProperty(exports, "PluginKey", {
11246
11341
  enumerable: true,
@@ -11333,6 +11428,7 @@ exports.clearContent = clearContent;
11333
11428
  exports.collapseSeparators = collapseSeparators;
11334
11429
  exports.copyThemeClass = copyThemeClass;
11335
11430
  exports.createAccumulatingDispatch = createAccumulatingDispatch;
11431
+ exports.createAdoptablePluginView = createAdoptablePluginView;
11336
11432
  exports.createBubbleMenuPlugin = createBubbleMenuPlugin;
11337
11433
  exports.createBubbleShouldShow = createBubbleShouldShow;
11338
11434
  exports.createCanChecker = createCanChecker;