@tiptap/extensions 3.23.5 → 3.24.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/dist/index.cjs +285 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -7
- package/dist/index.d.ts +24 -7
- package/dist/index.js +268 -47
- package/dist/index.js.map +1 -1
- package/dist/placeholder/index.cjs +265 -45
- package/dist/placeholder/index.cjs.map +1 -1
- package/dist/placeholder/index.d.cts +25 -8
- package/dist/placeholder/index.d.ts +25 -8
- package/dist/placeholder/index.js +264 -46
- package/dist/placeholder/index.js.map +1 -1
- package/dist/trailing-node/index.cjs +4 -1
- package/dist/trailing-node/index.cjs.map +1 -1
- package/dist/trailing-node/index.js +4 -1
- package/dist/trailing-node/index.js.map +1 -1
- package/package.json +19 -20
- package/src/placeholder/constants.ts +17 -0
- package/src/placeholder/index.ts +3 -0
- package/src/placeholder/placeholder.ts +5 -147
- package/src/placeholder/plugins/PlaceholderPlugin.ts +35 -0
- package/src/placeholder/types.ts +75 -0
- package/src/placeholder/utils/buildPlaceholderDecorations.ts +111 -0
- package/src/placeholder/utils/createPlaceholderDecoration.ts +61 -0
- package/src/placeholder/utils/findScrollParent.ts +38 -0
- package/src/placeholder/utils/getViewportBoundaryPositions.ts +49 -0
- package/src/placeholder/utils/index.ts +6 -0
- package/src/placeholder/utils/preparePlaceholderAttribute.ts +21 -0
- package/src/placeholder/utils/throttle.ts +28 -0
- package/src/placeholder/utils/viewportTracking.ts +118 -0
- package/src/trailing-node/trailing-node.ts +10 -2
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PluginKey } from '@tiptap/pm/state';
|
|
2
|
+
import { Editor, Extension } from '@tiptap/core';
|
|
2
3
|
import { Node } from '@tiptap/pm/model';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The viewport positions tracked by the placeholder plugin.
|
|
7
|
+
* `null` means "no viewport info yet" — the decoration callback falls back to
|
|
8
|
+
* a full document scan until the scroll handler fires.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
+
interface ViewportState {
|
|
11
|
+
topPos: number | null;
|
|
12
|
+
bottomPos: number | null;
|
|
13
|
+
}
|
|
10
14
|
interface PlaceholderOptions {
|
|
11
15
|
/**
|
|
12
16
|
* **The class name for the empty editor**
|
|
@@ -53,14 +57,20 @@ interface PlaceholderOptions {
|
|
|
53
57
|
*/
|
|
54
58
|
showOnlyCurrent: boolean;
|
|
55
59
|
/**
|
|
56
|
-
* **Controls if the placeholder should be shown for all
|
|
60
|
+
* **Controls if the placeholder should be shown for all descendants.**
|
|
57
61
|
*
|
|
58
|
-
* If true, the placeholder will be shown for all
|
|
62
|
+
* If true, the placeholder will be shown for all descendants.
|
|
59
63
|
* If false, the placeholder will only be shown for the current node.
|
|
60
64
|
* @default false
|
|
61
65
|
*/
|
|
62
66
|
includeChildren: boolean;
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
/** The default data attribute label */
|
|
70
|
+
declare const DEFAULT_DATA_ATTRIBUTE = "placeholder";
|
|
71
|
+
/** The plugin key used to store and read the placeholder viewport state */
|
|
72
|
+
declare const PLUGIN_KEY: PluginKey<ViewportState>;
|
|
73
|
+
|
|
64
74
|
/**
|
|
65
75
|
* This extension allows you to add a placeholder to your editor.
|
|
66
76
|
* A placeholder is a text that appears when the editor or a node is empty.
|
|
@@ -68,4 +78,11 @@ interface PlaceholderOptions {
|
|
|
68
78
|
*/
|
|
69
79
|
declare const Placeholder: Extension<PlaceholderOptions, any>;
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Prepares the placeholder attribute by ensuring it is properly formatted.
|
|
83
|
+
* @param attr - The placeholder attribute string.
|
|
84
|
+
* @returns The prepared placeholder attribute string.
|
|
85
|
+
*/
|
|
86
|
+
declare function preparePlaceholderAttribute(attr: string): string;
|
|
87
|
+
|
|
88
|
+
export { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY, Placeholder, type PlaceholderOptions, type ViewportState, preparePlaceholderAttribute };
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PluginKey } from '@tiptap/pm/state';
|
|
2
|
+
import { Editor, Extension } from '@tiptap/core';
|
|
2
3
|
import { Node } from '@tiptap/pm/model';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The viewport positions tracked by the placeholder plugin.
|
|
7
|
+
* `null` means "no viewport info yet" — the decoration callback falls back to
|
|
8
|
+
* a full document scan until the scroll handler fires.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
+
interface ViewportState {
|
|
11
|
+
topPos: number | null;
|
|
12
|
+
bottomPos: number | null;
|
|
13
|
+
}
|
|
10
14
|
interface PlaceholderOptions {
|
|
11
15
|
/**
|
|
12
16
|
* **The class name for the empty editor**
|
|
@@ -53,14 +57,20 @@ interface PlaceholderOptions {
|
|
|
53
57
|
*/
|
|
54
58
|
showOnlyCurrent: boolean;
|
|
55
59
|
/**
|
|
56
|
-
* **Controls if the placeholder should be shown for all
|
|
60
|
+
* **Controls if the placeholder should be shown for all descendants.**
|
|
57
61
|
*
|
|
58
|
-
* If true, the placeholder will be shown for all
|
|
62
|
+
* If true, the placeholder will be shown for all descendants.
|
|
59
63
|
* If false, the placeholder will only be shown for the current node.
|
|
60
64
|
* @default false
|
|
61
65
|
*/
|
|
62
66
|
includeChildren: boolean;
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
/** The default data attribute label */
|
|
70
|
+
declare const DEFAULT_DATA_ATTRIBUTE = "placeholder";
|
|
71
|
+
/** The plugin key used to store and read the placeholder viewport state */
|
|
72
|
+
declare const PLUGIN_KEY: PluginKey<ViewportState>;
|
|
73
|
+
|
|
64
74
|
/**
|
|
65
75
|
* This extension allows you to add a placeholder to your editor.
|
|
66
76
|
* A placeholder is a text that appears when the editor or a node is empty.
|
|
@@ -68,4 +78,11 @@ interface PlaceholderOptions {
|
|
|
68
78
|
*/
|
|
69
79
|
declare const Placeholder: Extension<PlaceholderOptions, any>;
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Prepares the placeholder attribute by ensuring it is properly formatted.
|
|
83
|
+
* @param attr - The placeholder attribute string.
|
|
84
|
+
* @returns The prepared placeholder attribute string.
|
|
85
|
+
*/
|
|
86
|
+
declare function preparePlaceholderAttribute(attr: string): string;
|
|
87
|
+
|
|
88
|
+
export { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY, Placeholder, type PlaceholderOptions, type ViewportState, preparePlaceholderAttribute };
|
|
@@ -1,11 +1,268 @@
|
|
|
1
|
-
// src/placeholder/
|
|
2
|
-
import {
|
|
3
|
-
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
4
|
-
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
1
|
+
// src/placeholder/constants.ts
|
|
2
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
5
3
|
var DEFAULT_DATA_ATTRIBUTE = "placeholder";
|
|
4
|
+
var PLUGIN_KEY = new PluginKey("tiptap__placeholder");
|
|
5
|
+
var VIEWPORT_OVERSCAN_PX = 200;
|
|
6
|
+
|
|
7
|
+
// src/placeholder/placeholder.ts
|
|
8
|
+
import { Extension } from "@tiptap/core";
|
|
9
|
+
|
|
10
|
+
// src/placeholder/plugins/PlaceholderPlugin.ts
|
|
11
|
+
import { Plugin } from "@tiptap/pm/state";
|
|
12
|
+
|
|
13
|
+
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
14
|
+
import { isNodeEmpty } from "@tiptap/core";
|
|
15
|
+
import { DecorationSet } from "@tiptap/pm/view";
|
|
16
|
+
|
|
17
|
+
// src/placeholder/utils/createPlaceholderDecoration.ts
|
|
18
|
+
import { Decoration } from "@tiptap/pm/view";
|
|
19
|
+
function createPlaceholderDecoration(options) {
|
|
20
|
+
const {
|
|
21
|
+
editor,
|
|
22
|
+
placeholder,
|
|
23
|
+
dataAttribute,
|
|
24
|
+
pos,
|
|
25
|
+
node,
|
|
26
|
+
isEmptyDoc,
|
|
27
|
+
hasAnchor,
|
|
28
|
+
classes: { emptyNode, emptyEditor }
|
|
29
|
+
} = options;
|
|
30
|
+
const classes = [emptyNode];
|
|
31
|
+
if (isEmptyDoc) {
|
|
32
|
+
classes.push(emptyEditor);
|
|
33
|
+
}
|
|
34
|
+
return Decoration.node(pos, pos + node.nodeSize, {
|
|
35
|
+
class: classes.join(" "),
|
|
36
|
+
[dataAttribute]: typeof placeholder === "function" ? placeholder({
|
|
37
|
+
editor,
|
|
38
|
+
node,
|
|
39
|
+
pos,
|
|
40
|
+
hasAnchor
|
|
41
|
+
}) : placeholder
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
46
|
+
function buildPlaceholderDecorations({
|
|
47
|
+
editor,
|
|
48
|
+
options,
|
|
49
|
+
dataAttribute,
|
|
50
|
+
doc,
|
|
51
|
+
selection
|
|
52
|
+
}) {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
const active = editor.isEditable || !options.showOnlyWhenEditable;
|
|
55
|
+
if (!active) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const { anchor } = selection;
|
|
59
|
+
const decorations = [];
|
|
60
|
+
const isEmptyDoc = editor.isEmpty;
|
|
61
|
+
const classes = {
|
|
62
|
+
emptyEditor: options.emptyEditorClass,
|
|
63
|
+
emptyNode: options.emptyNodeClass
|
|
64
|
+
};
|
|
65
|
+
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
|
|
66
|
+
if (useResolvedPath) {
|
|
67
|
+
const resolved = doc.resolve(anchor);
|
|
68
|
+
const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter;
|
|
69
|
+
const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor;
|
|
70
|
+
if (node && node.type.isTextblock && isNodeEmpty(node)) {
|
|
71
|
+
const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize;
|
|
72
|
+
decorations.push(
|
|
73
|
+
createPlaceholderDecoration({
|
|
74
|
+
editor,
|
|
75
|
+
isEmptyDoc,
|
|
76
|
+
dataAttribute,
|
|
77
|
+
hasAnchor,
|
|
78
|
+
placeholder: options.placeholder,
|
|
79
|
+
classes,
|
|
80
|
+
node,
|
|
81
|
+
pos: nodeStart
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
const pluginState = PLUGIN_KEY.getState(editor.state);
|
|
87
|
+
const from = (_a = pluginState == null ? void 0 : pluginState.topPos) != null ? _a : 0;
|
|
88
|
+
const to = (_b = pluginState == null ? void 0 : pluginState.bottomPos) != null ? _b : doc.content.size;
|
|
89
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
90
|
+
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
|
|
91
|
+
const isEmpty = !node.isLeaf && isNodeEmpty(node);
|
|
92
|
+
if (!node.type.isTextblock) {
|
|
93
|
+
return options.includeChildren;
|
|
94
|
+
}
|
|
95
|
+
if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
|
|
96
|
+
decorations.push(
|
|
97
|
+
createPlaceholderDecoration({
|
|
98
|
+
editor,
|
|
99
|
+
isEmptyDoc,
|
|
100
|
+
dataAttribute,
|
|
101
|
+
hasAnchor,
|
|
102
|
+
placeholder: options.placeholder,
|
|
103
|
+
classes,
|
|
104
|
+
node,
|
|
105
|
+
pos
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return options.includeChildren;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return DecorationSet.create(doc, decorations);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/placeholder/utils/preparePlaceholderAttribute.ts
|
|
6
116
|
function preparePlaceholderAttribute(attr) {
|
|
7
117
|
return attr.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/^[0-9-]+/, "").replace(/^-+/, "").toLowerCase();
|
|
8
118
|
}
|
|
119
|
+
|
|
120
|
+
// src/placeholder/utils/findScrollParent.ts
|
|
121
|
+
function isScrollable(el) {
|
|
122
|
+
const style = getComputedStyle(el);
|
|
123
|
+
const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`;
|
|
124
|
+
return /auto|scroll|overlay/.test(overflow);
|
|
125
|
+
}
|
|
126
|
+
function findScrollParent(element) {
|
|
127
|
+
let el = element;
|
|
128
|
+
while (el) {
|
|
129
|
+
if (isScrollable(el)) {
|
|
130
|
+
return el;
|
|
131
|
+
}
|
|
132
|
+
const parent = el.parentElement;
|
|
133
|
+
if (!parent) {
|
|
134
|
+
const root = el.getRootNode();
|
|
135
|
+
if (root instanceof ShadowRoot) {
|
|
136
|
+
el = root.host;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return window;
|
|
140
|
+
}
|
|
141
|
+
el = parent;
|
|
142
|
+
}
|
|
143
|
+
return window;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/placeholder/utils/getViewportBoundaryPositions.ts
|
|
147
|
+
function getContainerRect(container) {
|
|
148
|
+
if (container === window) {
|
|
149
|
+
return { top: 0, bottom: window.innerHeight };
|
|
150
|
+
}
|
|
151
|
+
return container.getBoundingClientRect();
|
|
152
|
+
}
|
|
153
|
+
function getViewportBoundaryPositions({
|
|
154
|
+
doc,
|
|
155
|
+
view,
|
|
156
|
+
scrollContainer
|
|
157
|
+
}) {
|
|
158
|
+
const editorRect = view.dom.getBoundingClientRect();
|
|
159
|
+
const containerRect = scrollContainer ? getContainerRect(scrollContainer) : { top: 0, bottom: window.innerHeight };
|
|
160
|
+
const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX;
|
|
161
|
+
const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX;
|
|
162
|
+
if (visibleTop >= visibleBottom) {
|
|
163
|
+
return { top: 0, bottom: doc.content.size };
|
|
164
|
+
}
|
|
165
|
+
const isRTL = getComputedStyle(view.dom).direction === "rtl";
|
|
166
|
+
const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2;
|
|
167
|
+
const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 });
|
|
168
|
+
const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 });
|
|
169
|
+
return {
|
|
170
|
+
top: topPos ? topPos.pos : 0,
|
|
171
|
+
bottom: bottomPos ? bottomPos.pos : doc.content.size
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/placeholder/utils/viewportTracking.ts
|
|
176
|
+
var viewportPluginState = {
|
|
177
|
+
/**
|
|
178
|
+
* Initialises the viewport state with no known positions.
|
|
179
|
+
* @returns The initial viewport state.
|
|
180
|
+
*/
|
|
181
|
+
init() {
|
|
182
|
+
return { topPos: null, bottomPos: null };
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* Updates the viewport state from incoming transactions.
|
|
186
|
+
* @param tr - The transaction being applied.
|
|
187
|
+
* @param prev - The previous viewport state.
|
|
188
|
+
* @returns The next viewport state.
|
|
189
|
+
*/
|
|
190
|
+
apply(tr, prev) {
|
|
191
|
+
const meta = tr.getMeta(PLUGIN_KEY);
|
|
192
|
+
if (meta == null ? void 0 : meta.positions) {
|
|
193
|
+
return { topPos: meta.positions.top, bottomPos: meta.positions.bottom };
|
|
194
|
+
}
|
|
195
|
+
if (!tr.docChanged) {
|
|
196
|
+
return prev;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,
|
|
200
|
+
bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function createViewportPluginView(view) {
|
|
205
|
+
const scrollContainer = findScrollParent(view.dom);
|
|
206
|
+
const computeAndDispatch = () => {
|
|
207
|
+
const positions = getViewportBoundaryPositions({
|
|
208
|
+
view,
|
|
209
|
+
doc: view.state.doc,
|
|
210
|
+
scrollContainer
|
|
211
|
+
});
|
|
212
|
+
const prev = PLUGIN_KEY.getState(view.state);
|
|
213
|
+
if ((prev == null ? void 0 : prev.topPos) === positions.top && (prev == null ? void 0 : prev.bottomPos) === positions.bottom) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions });
|
|
217
|
+
view.dispatch(tr);
|
|
218
|
+
};
|
|
219
|
+
let frame = null;
|
|
220
|
+
let lastCompute = 0;
|
|
221
|
+
const MIN_SCROLL_INTERVAL = 150;
|
|
222
|
+
const scheduleFrame = () => {
|
|
223
|
+
if (frame !== null) return;
|
|
224
|
+
frame = requestAnimationFrame(() => {
|
|
225
|
+
frame = null;
|
|
226
|
+
const now = performance.now();
|
|
227
|
+
if (now - lastCompute >= MIN_SCROLL_INTERVAL) {
|
|
228
|
+
lastCompute = now;
|
|
229
|
+
computeAndDispatch();
|
|
230
|
+
} else {
|
|
231
|
+
scheduleFrame();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
scrollContainer.addEventListener("scroll", scheduleFrame, { passive: true });
|
|
236
|
+
computeAndDispatch();
|
|
237
|
+
return {
|
|
238
|
+
update(_view, prevState) {
|
|
239
|
+
if (view.state.doc.content.size !== prevState.doc.content.size) {
|
|
240
|
+
scheduleFrame();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
destroy: () => {
|
|
244
|
+
if (frame !== null) {
|
|
245
|
+
cancelAnimationFrame(frame);
|
|
246
|
+
}
|
|
247
|
+
scrollContainer.removeEventListener("scroll", scheduleFrame);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/placeholder/plugins/PlaceholderPlugin.ts
|
|
253
|
+
function createPlaceholderPlugin({ editor, options }) {
|
|
254
|
+
const dataAttribute = options.dataAttribute ? `data-${preparePlaceholderAttribute(options.dataAttribute)}` : `data-${DEFAULT_DATA_ATTRIBUTE}`;
|
|
255
|
+
return new Plugin({
|
|
256
|
+
key: PLUGIN_KEY,
|
|
257
|
+
state: viewportPluginState,
|
|
258
|
+
view: createViewportPluginView,
|
|
259
|
+
props: {
|
|
260
|
+
decorations: ({ doc, selection }) => buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection })
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/placeholder/placeholder.ts
|
|
9
266
|
var Placeholder = Extension.create({
|
|
10
267
|
name: "placeholder",
|
|
11
268
|
addOptions() {
|
|
@@ -20,51 +277,12 @@ var Placeholder = Extension.create({
|
|
|
20
277
|
};
|
|
21
278
|
},
|
|
22
279
|
addProseMirrorPlugins() {
|
|
23
|
-
|
|
24
|
-
return [
|
|
25
|
-
new Plugin({
|
|
26
|
-
key: new PluginKey("placeholder"),
|
|
27
|
-
props: {
|
|
28
|
-
decorations: ({ doc, selection }) => {
|
|
29
|
-
const active = this.editor.isEditable || !this.options.showOnlyWhenEditable;
|
|
30
|
-
const { anchor } = selection;
|
|
31
|
-
const decorations = [];
|
|
32
|
-
if (!active) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
const isEmptyDoc = this.editor.isEmpty;
|
|
36
|
-
doc.descendants((node, pos) => {
|
|
37
|
-
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
|
|
38
|
-
const isEmpty = !node.isLeaf && isNodeEmpty(node);
|
|
39
|
-
if (!node.type.isTextblock) {
|
|
40
|
-
return this.options.includeChildren;
|
|
41
|
-
}
|
|
42
|
-
if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
|
|
43
|
-
const classes = [this.options.emptyNodeClass];
|
|
44
|
-
if (isEmptyDoc) {
|
|
45
|
-
classes.push(this.options.emptyEditorClass);
|
|
46
|
-
}
|
|
47
|
-
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
|
48
|
-
class: classes.join(" "),
|
|
49
|
-
[dataAttribute]: typeof this.options.placeholder === "function" ? this.options.placeholder({
|
|
50
|
-
editor: this.editor,
|
|
51
|
-
node,
|
|
52
|
-
pos,
|
|
53
|
-
hasAnchor
|
|
54
|
-
}) : this.options.placeholder
|
|
55
|
-
});
|
|
56
|
-
decorations.push(decoration);
|
|
57
|
-
}
|
|
58
|
-
return this.options.includeChildren;
|
|
59
|
-
});
|
|
60
|
-
return DecorationSet.create(doc, decorations);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
];
|
|
280
|
+
return [createPlaceholderPlugin({ editor: this.editor, options: this.options })];
|
|
65
281
|
}
|
|
66
282
|
});
|
|
67
283
|
export {
|
|
284
|
+
DEFAULT_DATA_ATTRIBUTE,
|
|
285
|
+
PLUGIN_KEY,
|
|
68
286
|
Placeholder,
|
|
69
287
|
preparePlaceholderAttribute
|
|
70
288
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/placeholder/placeholder.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Extension, isNodeEmpty } from '@tiptap/core'\nimport type { Node as ProsemirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\n/**\n * The default data attribute label\n */\nconst DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n\nexport interface PlaceholderOptions {\n /**\n * **The class name for the empty editor**\n * @default 'is-editor-empty'\n */\n emptyEditorClass: string\n\n /**\n * **The class name for empty nodes**\n * @default 'is-empty'\n */\n emptyNodeClass: string\n\n /**\n * **The data-attribute used for the placeholder label**\n * Will be prepended with `data-` and converted to kebab-case and cleaned of special characters.\n * @default 'placeholder'\n */\n dataAttribute: string\n\n /**\n * **The placeholder content**\n *\n * You can use a function to return a dynamic placeholder or a string.\n * @default 'Write something …'\n */\n placeholder:\n | ((PlaceholderProps: { editor: Editor; node: ProsemirrorNode; pos: number; hasAnchor: boolean }) => string)\n | string\n\n /**\n * **Checks if the placeholder should be only shown when the editor is editable.**\n *\n * If true, the placeholder will only be shown when the editor is editable.\n * If false, the placeholder will always be shown.\n * @default true\n */\n showOnlyWhenEditable: boolean\n\n /**\n * **Checks if the placeholder should be only shown when the current node is empty.**\n *\n * If true, the placeholder will only be shown when the current node is empty.\n * If false, the placeholder will be shown when any node is empty.\n * @default true\n */\n showOnlyCurrent: boolean\n\n /**\n * **Controls if the placeholder should be shown for all descendents.**\n *\n * If true, the placeholder will be shown for all descendents.\n * If false, the placeholder will only be shown for the current node.\n * @default false\n */\n includeChildren: boolean\n}\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n const dataAttribute = this.options.dataAttribute\n ? `data-${preparePlaceholderAttribute(this.options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return [\n new Plugin({\n key: new PluginKey('placeholder'),\n props: {\n decorations: ({ doc, selection }) => {\n const active = this.editor.isEditable || !this.options.showOnlyWhenEditable\n const { anchor } = selection\n const decorations: Decoration[] = []\n\n if (!active) {\n return null\n }\n\n const isEmptyDoc = this.editor.isEmpty\n\n doc.descendants((node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return this.options.includeChildren\n }\n\n if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {\n const classes = [this.options.emptyNodeClass]\n\n if (isEmptyDoc) {\n classes.push(this.options.emptyEditorClass)\n }\n\n const decoration = Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof this.options.placeholder === 'function'\n ? this.options.placeholder({\n editor: this.editor,\n node,\n pos,\n hasAnchor,\n })\n : this.options.placeholder,\n })\n\n decorations.push(decoration)\n }\n\n return this.options.includeChildren\n })\n\n return DecorationSet.create(doc, decorations)\n },\n },\n }),\n ]\n },\n})\n"],"mappings":";AACA,SAAS,WAAW,mBAAmB;AAEvC,SAAS,QAAQ,iBAAiB;AAClC,SAAS,YAAY,qBAAqB;AAK1C,IAAM,yBAAyB;AAOxB,SAAS,4BAA4B,MAAsB;AAChE,SACE,KAEG,QAAQ,QAAQ,GAAG,EAInB,QAAQ,kBAAkB,EAAE,EAE5B,QAAQ,YAAY,EAAE,EAEtB,QAAQ,OAAO,EAAE,EACjB,YAAY;AAEnB;AAiEO,IAAM,cAAc,UAAU,OAA2B;AAAA,EAC9D,MAAM;AAAA,EAEN,aAAa;AACX,WAAO;AAAA,MACL,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,aAAa;AAAA,MACb,sBAAsB;AAAA,MACtB,iBAAiB;AAAA,MACjB,iBAAiB;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,wBAAwB;AACtB,UAAM,gBAAgB,KAAK,QAAQ,gBAC/B,QAAQ,4BAA4B,KAAK,QAAQ,aAAa,CAAC,KAC/D,QAAQ,sBAAsB;AAElC,WAAO;AAAA,MACL,IAAI,OAAO;AAAA,QACT,KAAK,IAAI,UAAU,aAAa;AAAA,QAChC,OAAO;AAAA,UACL,aAAa,CAAC,EAAE,KAAK,UAAU,MAAM;AACnC,kBAAM,SAAS,KAAK,OAAO,cAAc,CAAC,KAAK,QAAQ;AACvD,kBAAM,EAAE,OAAO,IAAI;AACnB,kBAAM,cAA4B,CAAC;AAEnC,gBAAI,CAAC,QAAQ;AACX,qBAAO;AAAA,YACT;AAEA,kBAAM,aAAa,KAAK,OAAO;AAE/B,gBAAI,YAAY,CAAC,MAAM,QAAQ;AAC7B,oBAAM,YAAY,UAAU,OAAO,UAAU,MAAM,KAAK;AACxD,oBAAM,UAAU,CAAC,KAAK,UAAU,YAAY,IAAI;AAEhD,kBAAI,CAAC,KAAK,KAAK,aAAa;AAC1B,uBAAO,KAAK,QAAQ;AAAA,cACtB;AAEA,mBAAK,aAAa,CAAC,KAAK,QAAQ,oBAAoB,SAAS;AAC3D,sBAAM,UAAU,CAAC,KAAK,QAAQ,cAAc;AAE5C,oBAAI,YAAY;AACd,0BAAQ,KAAK,KAAK,QAAQ,gBAAgB;AAAA,gBAC5C;AAEA,sBAAM,aAAa,WAAW,KAAK,KAAK,MAAM,KAAK,UAAU;AAAA,kBAC3D,OAAO,QAAQ,KAAK,GAAG;AAAA,kBACvB,CAAC,aAAa,GACZ,OAAO,KAAK,QAAQ,gBAAgB,aAChC,KAAK,QAAQ,YAAY;AAAA,oBACvB,QAAQ,KAAK;AAAA,oBACb;AAAA,oBACA;AAAA,oBACA;AAAA,kBACF,CAAC,IACD,KAAK,QAAQ;AAAA,gBACrB,CAAC;AAED,4BAAY,KAAK,UAAU;AAAA,cAC7B;AAEA,qBAAO,KAAK,QAAQ;AAAA,YACtB,CAAC;AAED,mBAAO,cAAc,OAAO,KAAK,WAAW;AAAA,UAC9C;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/placeholder/constants.ts","../../src/placeholder/placeholder.ts","../../src/placeholder/plugins/PlaceholderPlugin.ts","../../src/placeholder/utils/buildPlaceholderDecorations.ts","../../src/placeholder/utils/createPlaceholderDecoration.ts","../../src/placeholder/utils/preparePlaceholderAttribute.ts","../../src/placeholder/utils/findScrollParent.ts","../../src/placeholder/utils/getViewportBoundaryPositions.ts","../../src/placeholder/utils/viewportTracking.ts"],"sourcesContent":["import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const classes = {\n emptyEditor: options.emptyEditorClass,\n emptyNode: options.emptyNodeClass,\n }\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: PlaceholderOptions['emptyNodeClass']\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -29,7 +29,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
var import_core = require("@tiptap/core");
|
|
30
30
|
var import_state = require("@tiptap/pm/state");
|
|
31
31
|
var skipTrailingNodeMeta = "skipTrailingNode";
|
|
32
|
-
function nodeEqualsType({
|
|
32
|
+
function nodeEqualsType({
|
|
33
|
+
types,
|
|
34
|
+
node
|
|
35
|
+
}) {
|
|
33
36
|
return node && Array.isArray(types) && types.includes(node.type) || (node == null ? void 0 : node.type) === types;
|
|
34
37
|
}
|
|
35
38
|
var TrailingNode = import_core.Extension.create({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/trailing-node/index.ts","../../src/trailing-node/trailing-node.ts"],"sourcesContent":["export * from './trailing-node.js'\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({
|
|
1
|
+
{"version":3,"sources":["../../src/trailing-node/index.ts","../../src/trailing-node/trailing-node.ts"],"sourcesContent":["export * from './trailing-node.js'\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA0B;AAE1B,mBAAkC;AAE3B,IAAM,uBAAuB;AAEpC,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AACF,GAGG;AACD,SAAQ,QAAQ,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,IAAI,MAAM,6BAAM,UAAS;AACvF;AA2BO,IAAM,eAAe,sBAAU,OAA4B;AAAA,EAChE,MAAM;AAAA,EAEN,aAAa;AACX,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAAA,EAEA,wBAAwB;AAnD1B;AAoDI,UAAM,SAAS,IAAI,uBAAU,KAAK,IAAI;AACtC,UAAM,cACJ,KAAK,QAAQ,UACb,UAAK,OAAO,OAAO,YAAY,aAAa,gBAA5C,mBAAyD,SACzD;AAEF,UAAM,gBAAgB,OAAO,QAAQ,KAAK,OAAO,OAAO,KAAK,EAC1D,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM,KAAK,EACxB,OAAO,WAAS,KAAK,QAAQ,YAAY,CAAC,GAAG,OAAO,WAAW,EAAE,SAAS,KAAK,IAAI,CAAC;AAEvF,WAAO;AAAA,MACL,IAAI,oBAAO;AAAA,QACT,KAAK;AAAA,QACL,mBAAmB,CAAC,cAAc,IAAI,UAAU;AAC9C,gBAAM,EAAE,KAAK,IAAI,OAAO,IAAI;AAC5B,gBAAM,wBAAwB,OAAO,SAAS,KAAK;AACnD,gBAAM,cAAc,IAAI,QAAQ;AAChC,gBAAM,OAAO,OAAO,MAAM,WAAW;AAErC,cAAI,aAAa,KAAK,iBAAe,YAAY,QAAQ,oBAAoB,CAAC,GAAG;AAC/E;AAAA,UACF;AAEA,cAAI,CAAC,uBAAuB;AAC1B;AAAA,UACF;AAEA,iBAAO,GAAG,OAAO,aAAa,KAAK,OAAO,CAAC;AAAA,QAC7C;AAAA,QACA,OAAO;AAAA,UACL,MAAM,CAAC,GAAG,UAAU;AAClB,kBAAM,WAAW,MAAM,GAAG,IAAI;AAE9B,mBAAO,CAAC,eAAe,EAAE,MAAM,UAAU,OAAO,cAAc,CAAC;AAAA,UACjE;AAAA,UACA,OAAO,CAAC,IAAI,UAAU;AACpB,gBAAI,CAAC,GAAG,YAAY;AAClB,qBAAO;AAAA,YACT;AAIA,gBAAI,GAAG,QAAQ,uBAAuB,GAAG;AACvC,qBAAO;AAAA,YACT;AAEA,kBAAM,WAAW,GAAG,IAAI;AAExB,mBAAO,CAAC,eAAe,EAAE,MAAM,UAAU,OAAO,cAAc,CAAC;AAAA,UACjE;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import { Extension } from "@tiptap/core";
|
|
3
3
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
4
4
|
var skipTrailingNodeMeta = "skipTrailingNode";
|
|
5
|
-
function nodeEqualsType({
|
|
5
|
+
function nodeEqualsType({
|
|
6
|
+
types,
|
|
7
|
+
node
|
|
8
|
+
}) {
|
|
6
9
|
return node && Array.isArray(types) && types.includes(node.type) || (node == null ? void 0 : node.type) === types;
|
|
7
10
|
}
|
|
8
11
|
var TrailingNode = Extension.create({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/trailing-node/trailing-node.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({
|
|
1
|
+
{"version":3,"sources":["../../src/trailing-node/trailing-node.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n"],"mappings":";AAAA,SAAS,iBAAiB;AAE1B,SAAS,QAAQ,iBAAiB;AAE3B,IAAM,uBAAuB;AAEpC,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AACF,GAGG;AACD,SAAQ,QAAQ,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,IAAI,MAAM,6BAAM,UAAS;AACvF;AA2BO,IAAM,eAAe,UAAU,OAA4B;AAAA,EAChE,MAAM;AAAA,EAEN,aAAa;AACX,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAAA,EAEA,wBAAwB;AAnD1B;AAoDI,UAAM,SAAS,IAAI,UAAU,KAAK,IAAI;AACtC,UAAM,cACJ,KAAK,QAAQ,UACb,UAAK,OAAO,OAAO,YAAY,aAAa,gBAA5C,mBAAyD,SACzD;AAEF,UAAM,gBAAgB,OAAO,QAAQ,KAAK,OAAO,OAAO,KAAK,EAC1D,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM,KAAK,EACxB,OAAO,WAAS,KAAK,QAAQ,YAAY,CAAC,GAAG,OAAO,WAAW,EAAE,SAAS,KAAK,IAAI,CAAC;AAEvF,WAAO;AAAA,MACL,IAAI,OAAO;AAAA,QACT,KAAK;AAAA,QACL,mBAAmB,CAAC,cAAc,IAAI,UAAU;AAC9C,gBAAM,EAAE,KAAK,IAAI,OAAO,IAAI;AAC5B,gBAAM,wBAAwB,OAAO,SAAS,KAAK;AACnD,gBAAM,cAAc,IAAI,QAAQ;AAChC,gBAAM,OAAO,OAAO,MAAM,WAAW;AAErC,cAAI,aAAa,KAAK,iBAAe,YAAY,QAAQ,oBAAoB,CAAC,GAAG;AAC/E;AAAA,UACF;AAEA,cAAI,CAAC,uBAAuB;AAC1B;AAAA,UACF;AAEA,iBAAO,GAAG,OAAO,aAAa,KAAK,OAAO,CAAC;AAAA,QAC7C;AAAA,QACA,OAAO;AAAA,UACL,MAAM,CAAC,GAAG,UAAU;AAClB,kBAAM,WAAW,MAAM,GAAG,IAAI;AAE9B,mBAAO,CAAC,eAAe,EAAE,MAAM,UAAU,OAAO,cAAc,CAAC;AAAA,UACjE;AAAA,UACA,OAAO,CAAC,IAAI,UAAU;AACpB,gBAAI,CAAC,GAAG,YAAY;AAClB,qBAAO;AAAA,YACT;AAIA,gBAAI,GAAG,QAAQ,uBAAuB,GAAG;AACvC,qBAAO;AAAA,YACT;AAEA,kBAAM,WAAW,GAAG,IAAI;AAExB,mBAAO,CAAC,eAAe,EAAE,MAAM,UAAU,OAAO,cAAc,CAAC;AAAA,UACjE;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/extensions",
|
|
3
|
+
"version": "3.24.0",
|
|
3
4
|
"description": "various extensions for tiptap",
|
|
4
|
-
"version": "3.23.5",
|
|
5
|
-
"homepage": "https://tiptap.dev",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"tiptap",
|
|
8
7
|
"tiptap extension"
|
|
9
8
|
],
|
|
9
|
+
"homepage": "https://tiptap.dev",
|
|
10
10
|
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/ueberdosis/tiptap",
|
|
14
|
+
"directory": "packages/extension"
|
|
15
|
+
},
|
|
11
16
|
"funding": {
|
|
12
17
|
"type": "github",
|
|
13
18
|
"url": "https://github.com/sponsors/ueberdosis"
|
|
14
19
|
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
15
24
|
"type": "module",
|
|
25
|
+
"main": "dist/index.cjs",
|
|
26
|
+
"module": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
16
28
|
"exports": {
|
|
17
29
|
".": {
|
|
18
30
|
"types": {
|
|
@@ -87,28 +99,15 @@
|
|
|
87
99
|
"require": "./dist/trailing-node/index.cjs"
|
|
88
100
|
}
|
|
89
101
|
},
|
|
90
|
-
"main": "dist/index.cjs",
|
|
91
|
-
"module": "dist/index.js",
|
|
92
|
-
"types": "dist/index.d.ts",
|
|
93
|
-
"files": [
|
|
94
|
-
"src",
|
|
95
|
-
"dist"
|
|
96
|
-
],
|
|
97
102
|
"devDependencies": {
|
|
98
|
-
"@tiptap/core": "^3.
|
|
99
|
-
"@tiptap/pm": "^3.
|
|
103
|
+
"@tiptap/core": "^3.24.0",
|
|
104
|
+
"@tiptap/pm": "^3.24.0"
|
|
100
105
|
},
|
|
101
106
|
"peerDependencies": {
|
|
102
|
-
"@tiptap/core": "3.
|
|
103
|
-
"@tiptap/pm": "3.
|
|
104
|
-
},
|
|
105
|
-
"repository": {
|
|
106
|
-
"type": "git",
|
|
107
|
-
"url": "https://github.com/ueberdosis/tiptap",
|
|
108
|
-
"directory": "packages/extension"
|
|
107
|
+
"@tiptap/core": "3.24.0",
|
|
108
|
+
"@tiptap/pm": "3.24.0"
|
|
109
109
|
},
|
|
110
110
|
"scripts": {
|
|
111
|
-
"build": "tsup"
|
|
112
|
-
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
|
|
111
|
+
"build": "tsup"
|
|
113
112
|
}
|
|
114
113
|
}
|