@domternal/core 1.0.2 → 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 +22 -0
- package/dist/index.cjs +121 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -10
- package/dist/index.d.ts +38 -10
- package/dist/index.js +122 -27
- package/dist/index.js.map +1 -1
- package/package.json +12 -14
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
|
|
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"
|
|
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: (
|
|
5871
|
-
if (isVisibleNow(
|
|
5945
|
+
update: (view2) => {
|
|
5946
|
+
if (isVisibleNow(view2)) updatePosition(view2);
|
|
5872
5947
|
else {
|
|
5873
5948
|
hideMenu();
|
|
5874
|
-
clearTriggeredFlag(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: (
|
|
11108
|
-
if (
|
|
11109
|
-
if (!
|
|
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(
|
|
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 &&
|
|
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(
|
|
11221
|
+
updatePosition(view2, state.from, state.to);
|
|
11127
11222
|
}, updateDelay);
|
|
11128
11223
|
} else {
|
|
11129
|
-
updatePosition(
|
|
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
|
|
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;
|