prosemirror-view 1.39.2 → 1.39.3

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.39.3 (2025-05-14)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where clicks using different mouse buttons could be grouped into a single double- or triple-click.
6
+
7
+ Only delete the selection when starting a composition when it crosses block boundaries.
8
+
1
9
  ## 1.39.2 (2025-04-22)
2
10
 
3
11
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -2879,7 +2879,8 @@ var InputState = _createClass(function InputState() {
2879
2879
  time: 0,
2880
2880
  x: 0,
2881
2881
  y: 0,
2882
- type: ""
2882
+ type: "",
2883
+ button: 0
2883
2884
  };
2884
2885
  this.lastSelectionOrigin = null;
2885
2886
  this.lastSelectionTime = 0;
@@ -3105,14 +3106,15 @@ handlers.mousedown = function (view, _event) {
3105
3106
  var flushed = forceDOMFlush(view);
3106
3107
  var now = Date.now(),
3107
3108
  type = "singleClick";
3108
- if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier]) {
3109
+ if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier] && view.input.lastClick.button == event.button) {
3109
3110
  if (view.input.lastClick.type == "singleClick") type = "doubleClick";else if (view.input.lastClick.type == "doubleClick") type = "tripleClick";
3110
3111
  }
3111
3112
  view.input.lastClick = {
3112
3113
  time: now,
3113
3114
  x: event.clientX,
3114
3115
  y: event.clientY,
3115
- type: type
3116
+ type: type,
3117
+ button: event.button
3116
3118
  };
3117
3119
  var pos = view.posAtCoords(eventCoords(event));
3118
3120
  if (!pos) return;
@@ -3329,8 +3331,9 @@ function endComposition(view) {
3329
3331
  view.domObserver.forceFlush();
3330
3332
  clearComposition(view);
3331
3333
  if (restarting || view.docView && view.docView.dirty) {
3332
- var sel = selectionFromDOM(view);
3333
- if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel));else if ((view.markCursor || restarting) && !view.state.selection.empty) view.dispatch(view.state.tr.deleteSelection());else view.updateState(view.state);
3334
+ var sel = selectionFromDOM(view),
3335
+ cur = view.state.selection;
3336
+ if (sel && !sel.eq(cur)) view.dispatch(view.state.tr.setSelection(sel));else if ((view.markCursor || restarting) && !cur.$from.node(cur.$from.sharedDepth(cur.to)).inlineContent) view.dispatch(view.state.tr.deleteSelection());else view.updateState(view.state);
3334
3337
  return true;
3335
3338
  }
3336
3339
  return false;
package/dist/index.js CHANGED
@@ -3047,7 +3047,7 @@ class InputState {
3047
3047
  this.mouseDown = null;
3048
3048
  this.lastKeyCode = null;
3049
3049
  this.lastKeyCodeTime = 0;
3050
- this.lastClick = { time: 0, x: 0, y: 0, type: "" };
3050
+ this.lastClick = { time: 0, x: 0, y: 0, type: "", button: 0 };
3051
3051
  this.lastSelectionOrigin = null;
3052
3052
  this.lastSelectionTime = 0;
3053
3053
  this.lastIOSEnter = 0;
@@ -3289,13 +3289,14 @@ handlers.mousedown = (view, _event) => {
3289
3289
  view.input.shiftKey = event.shiftKey;
3290
3290
  let flushed = forceDOMFlush(view);
3291
3291
  let now = Date.now(), type = "singleClick";
3292
- if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier]) {
3292
+ if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier] &&
3293
+ view.input.lastClick.button == event.button) {
3293
3294
  if (view.input.lastClick.type == "singleClick")
3294
3295
  type = "doubleClick";
3295
3296
  else if (view.input.lastClick.type == "doubleClick")
3296
3297
  type = "tripleClick";
3297
3298
  }
3298
- view.input.lastClick = { time: now, x: event.clientX, y: event.clientY, type };
3299
+ view.input.lastClick = { time: now, x: event.clientX, y: event.clientY, type, button: event.button };
3299
3300
  let pos = view.posAtCoords(eventCoords(event));
3300
3301
  if (!pos)
3301
3302
  return;
@@ -3552,10 +3553,10 @@ function endComposition(view, restarting = false) {
3552
3553
  view.domObserver.forceFlush();
3553
3554
  clearComposition(view);
3554
3555
  if (restarting || view.docView && view.docView.dirty) {
3555
- let sel = selectionFromDOM(view);
3556
- if (sel && !sel.eq(view.state.selection))
3556
+ let sel = selectionFromDOM(view), cur = view.state.selection;
3557
+ if (sel && !sel.eq(cur))
3557
3558
  view.dispatch(view.state.tr.setSelection(sel));
3558
- else if ((view.markCursor || restarting) && !view.state.selection.empty)
3559
+ else if ((view.markCursor || restarting) && !cur.$from.node(cur.$from.sharedDepth(cur.to)).inlineContent)
3559
3560
  view.dispatch(view.state.tr.deleteSelection());
3560
3561
  else
3561
3562
  view.updateState(view.state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.39.2",
3
+ "version": "1.39.3",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/input.ts CHANGED
@@ -21,7 +21,7 @@ export class InputState {
21
21
  mouseDown: MouseDown | null = null
22
22
  lastKeyCode: number | null = null
23
23
  lastKeyCodeTime = 0
24
- lastClick = {time: 0, x: 0, y: 0, type: ""}
24
+ lastClick = {time: 0, x: 0, y: 0, type: "", button: 0}
25
25
  lastSelectionOrigin: string | null = null
26
26
  lastSelectionTime = 0
27
27
  lastIOSEnter = 0
@@ -278,11 +278,12 @@ handlers.mousedown = (view, _event) => {
278
278
  view.input.shiftKey = event.shiftKey
279
279
  let flushed = forceDOMFlush(view)
280
280
  let now = Date.now(), type = "singleClick"
281
- if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier]) {
281
+ if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier] &&
282
+ view.input.lastClick.button == event.button) {
282
283
  if (view.input.lastClick.type == "singleClick") type = "doubleClick"
283
284
  else if (view.input.lastClick.type == "doubleClick") type = "tripleClick"
284
285
  }
285
- view.input.lastClick = {time: now, x: event.clientX, y: event.clientY, type}
286
+ view.input.lastClick = {time: now, x: event.clientX, y: event.clientY, type, button: event.button}
286
287
 
287
288
  let pos = view.posAtCoords(eventCoords(event))
288
289
  if (!pos) return
@@ -544,9 +545,9 @@ export function endComposition(view: EditorView, restarting = false) {
544
545
  view.domObserver.forceFlush()
545
546
  clearComposition(view)
546
547
  if (restarting || view.docView && view.docView.dirty) {
547
- let sel = selectionFromDOM(view)
548
- if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel))
549
- else if ((view.markCursor || restarting) && !view.state.selection.empty) view.dispatch(view.state.tr.deleteSelection())
548
+ let sel = selectionFromDOM(view), cur = view.state.selection
549
+ if (sel && !sel.eq(cur)) view.dispatch(view.state.tr.setSelection(sel))
550
+ else if ((view.markCursor || restarting) && !cur.$from.node(cur.$from.sharedDepth(cur.to)).inlineContent) view.dispatch(view.state.tr.deleteSelection())
550
551
  else view.updateState(view.state)
551
552
  return true
552
553
  }
package/src/#index.ts# DELETED
@@ -1,822 +0,0 @@
1
- import {NodeSelection, EditorState, Plugin, PluginView, Transaction, Selection} from "prosemirror-state"
2
- import {Slice, ResolvedPos, DOMParser, DOMSerializer, Node, Mark} from "prosemirror-model"
3
-
4
- import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos,
5
- resetScrollPos, focusPreventScroll} from "./domcoords"
6
- import {docViewDesc, ViewDesc, NodeView, NodeViewDesc, MarkView} from "./viewdesc"
7
- import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition,
8
- InputState, doPaste, Dragging, findCompositionNode} from "./input"
9
- import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
10
- import {Decoration, viewDecorations, DecorationSource} from "./decoration"
11
- import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
12
- import {DOMChange} from "./domchange"
13
- import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReusedRange} from "./dom"
14
- import * as browser from "./browser"
15
-
16
- export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
17
- export {NodeView, MarkView, ViewMutationRecord} from "./viewdesc"
18
-
19
- // Exported for testing
20
- import {serializeForClipboard, parseFromClipboard} from "./clipboard"
21
- import {endComposition} from "./input"
22
- /// @internal
23
- export const __parseFromClipboard = parseFromClipboard
24
- /// @internal
25
- export const __endComposition = endComposition
26
-
27
- /// An editor view manages the DOM structure that represents an
28
- /// editable document. Its state and behavior are determined by its
29
- /// [props](#view.DirectEditorProps).
30
- export class EditorView {
31
- private _props: DirectEditorProps
32
- private directPlugins: readonly Plugin[]
33
- private _root: Document | ShadowRoot | null = null
34
- /// @internal
35
- focused = false
36
- /// Kludge used to work around a Chrome bug @internal
37
- trackWrites: DOMNode | null = null
38
- private mounted = false
39
- /// @internal
40
- markCursor: readonly Mark[] | null = null
41
- /// @internal
42
- cursorWrapper: {dom: DOMNode, deco: Decoration} | null = null
43
- /// @internal
44
- nodeViews: NodeViewSet
45
- /// @internal
46
- lastSelectedViewDesc: ViewDesc | undefined = undefined
47
- /// @internal
48
- docView: NodeViewDesc
49
- /// @internal
50
- input = new InputState
51
- private prevDirectPlugins: readonly Plugin[] = []
52
- private pluginViews: PluginView[] = []
53
- /// @internal
54
- declare domObserver: DOMObserver
55
- /// Holds `true` when a hack node is needed in Firefox to prevent the
56
- /// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
57
- /// @internal
58
- requiresGeckoHackNode: boolean = false
59
-
60
- /// The view's current [state](#state.EditorState).
61
- public state: EditorState
62
-
63
- /// Create a view. `place` may be a DOM node that the editor should
64
- /// be appended to, a function that will place it into the document,
65
- /// or an object whose `mount` property holds the node to use as the
66
- /// document container. If it is `null`, the editor will not be
67
- /// added to the document.
68
- constructor(place: null | DOMNode | ((editor: HTMLElement) => void) | {mount: HTMLElement}, props: DirectEditorProps) {
69
- this._props = props
70
- this.state = props.state
71
- this.directPlugins = props.plugins || []
72
- this.directPlugins.forEach(checkStateComponent)
73
-
74
- this.dispatch = this.dispatch.bind(this)
75
-
76
- this.dom = (place && (place as {mount: HTMLElement}).mount) || document.createElement("div")
77
- if (place) {
78
- if ((place as DOMNode).appendChild) (place as DOMNode).appendChild(this.dom)
79
- else if (typeof place == "function") place(this.dom)
80
- else if ((place as {mount: HTMLElement}).mount) this.mounted = true
81
- }
82
-
83
- this.editable = getEditable(this)
84
- updateCursorWrapper(this)
85
- this.nodeViews = buildNodeViews(this)
86
- this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this)
87
-
88
- this.domObserver = new DOMObserver(this, change => change.apply(this))
89
- this.domObserver.start()
90
- initInput(this)
91
- this.updatePluginViews()
92
- }
93
-
94
- /// An editable DOM node containing the document. (You probably
95
- /// should not directly interfere with its content.)
96
- readonly dom: HTMLElement
97
-
98
- /// Indicates whether the editor is currently [editable](#view.EditorProps.editable).
99
- editable: boolean
100
-
101
- /// When editor content is being dragged, this object contains
102
- /// information about the dragged slice and whether it is being
103
- /// copied or moved. At any other time, it is null.
104
- dragging: null | {slice: Slice, move: boolean} = null
105
-
106
- /// Holds `true` when a
107
- /// [composition](https://w3c.github.io/uievents/#events-compositionevents)
108
- /// is active.
109
- get composing() { return this.input.composing }
110
-
111
- /// The view's current [props](#view.EditorProps).
112
- get props() {
113
- if (this._props.state != this.state) {
114
- let prev = this._props
115
- this._props = {} as any
116
- for (let name in prev) (this._props as any)[name] = (prev as any)[name]
117
- this._props.state = this.state
118
- }
119
- return this._props
120
- }
121
-
122
- /// Update the view's props. Will immediately cause an update to
123
- /// the DOM.
124
- update(props: DirectEditorProps) {
125
- if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this)
126
- let prevProps = this._props
127
- this._props = props
128
- if (props.plugins) {
129
- props.plugins.forEach(checkStateComponent)
130
- this.directPlugins = props.plugins
131
- }
132
- this.updateStateInner(props.state, prevProps)
133
- }
134
-
135
- /// Update the view by updating existing props object with the object
136
- /// given as argument. Equivalent to `view.update(Object.assign({},
137
- /// view.props, props))`.
138
- setProps(props: Partial<DirectEditorProps>) {
139
- let updated = {} as DirectEditorProps
140
- for (let name in this._props) (updated as any)[name] = (this._props as any)[name]
141
- updated.state = this.state
142
- for (let name in props) (updated as any)[name] = (props as any)[name]
143
- this.update(updated)
144
- }
145
-
146
- /// Update the editor's `state` prop, without touching any of the
147
- /// other props.
148
- updateState(state: EditorState) {
149
- this.updateStateInner(state, this._props)
150
- }
151
-
152
- private updateStateInner(state: EditorState, prevProps: DirectEditorProps) {
153
- let prev = this.state, redraw = false, updateSel = false
154
- // When stored marks are added, stop composition, so that they can
155
- // be displayed.
156
- if (state.storedMarks && this.composing) {
157
- clearComposition(this)
158
- updateSel = true
159
- }
160
- this.state = state
161
- let pluginsChanged = prev.plugins != state.plugins || this._props.plugins != prevProps.plugins
162
- if (pluginsChanged || this._props.plugins != prevProps.plugins || this._props.nodeViews != prevProps.nodeViews) {
163
- let nodeViews = buildNodeViews(this)
164
- if (changedNodeViews(nodeViews, this.nodeViews)) {
165
- this.nodeViews = nodeViews
166
- redraw = true
167
- }
168
- }
169
- if (pluginsChanged || prevProps.handleDOMEvents != this._props.handleDOMEvents) {
170
- ensureListeners(this)
171
- }
172
-
173
- this.editable = getEditable(this)
174
- updateCursorWrapper(this)
175
- let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this)
176
-
177
- let scroll = prev.plugins != state.plugins && !prev.doc.eq(state.doc) ? "reset"
178
- : (state as any).scrollToSelection > (prev as any).scrollToSelection ? "to selection" : "preserve"
179
- let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco)
180
- if (updateDoc || !state.selection.eq(prev.selection)) updateSel = true
181
- let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this)
182
-
183
- if (updateSel) {
184
- this.domObserver.stop()
185
- // Work around an issue in Chrome, IE, and Edge where changing
186
- // the DOM around an active selection puts it into a broken
187
- // state where the thing the user sees differs from the
188
- // selection reported by the Selection object (#710, #973,
189
- // #1011, #1013, #1035).
190
- let forceSelUpdate = updateDoc && (browser.ie || browser.chrome) && !this.composing &&
191
- !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection)
192
- if (updateDoc) {
193
- // If the node that the selection points into is written to,
194
- // Chrome sometimes starts misreporting the selection, so this
195
- // tracks that and forces a selection reset when our update
196
- // did write to the node.
197
- let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null
198
- if (this.composing) this.input.compositionNode = findCompositionNode(this)
199
- if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
200
- this.docView.updateOuterDeco(outerDeco)
201
- this.docView.destroy()
202
- this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this)
203
- }
204
- if (chromeKludge && !this.trackWrites) forceSelUpdate = true
205
- }
206
- // Work around for an issue where an update arriving right between
207
- // a DOM selection change and the "selectionchange" event for it
208
- // can cause a spurious DOM selection update, disrupting mouse
209
- // drag selection.
210
- if (forceSelUpdate ||
211
- !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
212
- anchorInRightPlace(this))) {
213
- selectionToDOM(this, forceSelUpdate)
214
- } else {
215
- syncNodeSelection(this, state.selection)
216
- this.domObserver.setCurSelection()
217
- }
218
- this.domObserver.start()
219
- }
220
-
221
- this.updatePluginViews(prev)
222
- if ((this.dragging as Dragging)?.node && !prev.doc.eq(state.doc))
223
- this.updateDraggedNode(this.dragging as Dragging, prev)
224
-
225
- if (scroll == "reset") {
226
- this.dom.scrollTop = 0
227
- } else if (scroll == "to selection") {
228
- this.scrollToSelection()
229
- } else if (oldScrollPos) {
230
- resetScrollPos(oldScrollPos)
231
- }
232
- }
233
-
234
- /// @internal
235
- scrollToSelection() {
236
- let startDOM = this.domSelectionRange().focusNode
237
- if (!startDOM || !this.dom.contains(startDOM.nodeType == 1 ? startDOM : startDOM.parentNode)) {
238
- // Ignore selections outside the editor
239
- } else if (this.someProp("handleScrollToSelection", f => f(this))) {
240
- // Handled
241
- } else if (this.state.selection instanceof NodeSelection) {
242
- let target = this.docView.domAfterPos(this.state.selection.from)
243
- if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
244
- } else {
245
- scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM)
246
- }
247
- }
248
-
249
- private destroyPluginViews() {
250
- let view
251
- while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
252
- }
253
-
254
- private updatePluginViews(prevState?: EditorState) {
255
- if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) {
256
- this.prevDirectPlugins = this.directPlugins
257
- this.destroyPluginViews()
258
- for (let i = 0; i < this.directPlugins.length; i++) {
259
- let plugin = this.directPlugins[i]
260
- if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
261
- }
262
- for (let i = 0; i < this.state.plugins.length; i++) {
263
- let plugin = this.state.plugins[i]
264
- if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
265
- }
266
- } else {
267
- for (let i = 0; i < this.pluginViews.length; i++) {
268
- let pluginView = this.pluginViews[i]
269
- if (pluginView.update) pluginView.update(this, prevState)
270
- }
271
- }
272
- }
273
-
274
- private updateDraggedNode(dragging: Dragging, prev: EditorState) {
275
- let sel = dragging.node!, found = -1
276
- if (this.state.doc.nodeAt(sel.from) == sel.node) {
277
- found = sel.from
278
- } else {
279
- let movedPos = sel.from + (this.state.doc.content.size - prev.doc.content.size)
280
- let moved = movedPos > 0 && this.state.doc.nodeAt(movedPos)
281
- if (moved == sel.node) found = movedPos
282
- }
283
- this.dragging = new Dragging(dragging.slice, dragging.move,
284
- found < 0 ? undefined : NodeSelection.create(this.state.doc, found))
285
- }
286
-
287
- /// Goes over the values of a prop, first those provided directly,
288
- /// then those from plugins given to the view, then from plugins in
289
- /// the state (in order), and calls `f` every time a non-undefined
290
- /// value is found. When `f` returns a truthy value, that is
291
- /// immediately returned. When `f` isn't provided, it is treated as
292
- /// the identity function (the prop value is returned directly).
293
- someProp<PropName extends keyof EditorProps, Result>(
294
- propName: PropName,
295
- f: (value: NonNullable<EditorProps[PropName]>) => Result
296
- ): Result | undefined
297
- someProp<PropName extends keyof EditorProps>(propName: PropName): NonNullable<EditorProps[PropName]> | undefined
298
- someProp<PropName extends keyof EditorProps, Result>(
299
- propName: PropName,
300
- f?: (value: NonNullable<EditorProps[PropName]>) => Result
301
- ): Result | undefined {
302
- let prop = this._props && this._props[propName], value
303
- if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
304
- for (let i = 0; i < this.directPlugins.length; i++) {
305
- let prop = this.directPlugins[i].props[propName]
306
- if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
307
- }
308
- let plugins = this.state.plugins
309
- if (plugins) for (let i = 0; i < plugins.length; i++) {
310
- let prop = plugins[i].props[propName]
311
- if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
312
- }
313
- }
314
-
315
- /// Query whether the view has focus.
316
- hasFocus() {
317
- // Work around IE not handling focus correctly if resize handles are shown.
318
- // If the cursor is inside an element with resize handles, activeElement
319
- // will be that element instead of this.dom.
320
- if (browser.ie) {
321
- // If activeElement is within this.dom, and there are no other elements
322
- // setting `contenteditable` to false in between, treat it as focused.
323
- let node = this.root.activeElement
324
- if (node == this.dom) return true
325
- if (!node || !this.dom.contains(node)) return false
326
- while (node && this.dom != node && this.dom.contains(node)) {
327
- if ((node as HTMLElement).contentEditable == 'false') return false
328
- node = node.parentElement
329
- }
330
- return true
331
- }
332
- return this.root.activeElement == this.dom
333
- }
334
-
335
- /// Focus the editor.
336
- focus() {
337
- this.domObserver.stop()
338
- if (this.editable) focusPreventScroll(this.dom)
339
- selectionToDOM(this)
340
- this.domObserver.start()
341
- }
342
-
343
- /// Get the document root in which the editor exists. This will
344
- /// usually be the top-level `document`, but might be a [shadow
345
- /// DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
346
- /// root if the editor is inside one.
347
- get root(): Document | ShadowRoot {
348
- let cached = this._root
349
- if (cached == null) for (let search = this.dom.parentNode; search; search = search.parentNode) {
350
- if (search.nodeType == 9 || (search.nodeType == 11 && (search as any).host)) {
351
- if (!(search as any).getSelection)
352
- Object.getPrototypeOf(search).getSelection = () => (search as DOMNode).ownerDocument!.getSelection()
353
- return this._root = search as Document | ShadowRoot
354
- }
355
- }
356
- return cached || document
357
- }
358
-
359
- /// When an existing editor view is moved to a new document or
360
- /// shadow tree, call this to make it recompute its root.
361
- updateRoot() {
362
- this._root = null
363
- }
364
-
365
- /// Given a pair of viewport coordinates, return the document
366
- /// position that corresponds to them. May return null if the given
367
- /// coordinates aren't inside of the editor. When an object is
368
- /// returned, its `pos` property is the position nearest to the
369
- /// coordinates, and its `inside` property holds the position of the
370
- /// inner node that the position falls inside of, or -1 if it is at
371
- /// the top level, not in any node.
372
- posAtCoords(coords: {left: number, top: number}): {pos: number, inside: number} | null {
373
- return posAtCoords(this, coords)
374
- }
375
-
376
- /// Returns the viewport rectangle at a given document position.
377
- /// `left` and `right` will be the same number, as this returns a
378
- /// flat cursor-ish rectangle. If the position is between two things
379
- /// that aren't directly adjacent, `side` determines which element
380
- /// is used. When < 0, the element before the position is used,
381
- /// otherwise the element after.
382
- coordsAtPos(pos: number, side = 1): {left: number, right: number, top: number, bottom: number} {
383
- return coordsAtPos(this, pos, side)
384
- }
385
-
386
- /// Find the DOM position that corresponds to the given document
387
- /// position. When `side` is negative, find the position as close as
388
- /// possible to the content before the position. When positive,
389
- /// prefer positions close to the content after the position. When
390
- /// zero, prefer as shallow a position as possible.
391
- ///
392
- /// Note that you should **not** mutate the editor's internal DOM,
393
- /// only inspect it (and even that is usually not necessary).
394
- domAtPos(pos: number, side = 0): {node: DOMNode, offset: number} {
395
- return this.docView.domFromPos(pos, side)
396
- }
397
-
398
- /// Find the DOM node that represents the document node after the
399
- /// given position. May return `null` when the position doesn't point
400
- /// in front of a node or if the node is inside an opaque node view.
401
- ///
402
- /// This is intended to be able to call things like
403
- /// `getBoundingClientRect` on that DOM node. Do **not** mutate the
404
- /// editor DOM directly, or add styling this way, since that will be
405
- /// immediately overriden by the editor as it redraws the node.
406
- nodeDOM(pos: number): DOMNode | null {
407
- let desc = this.docView.descAt(pos)
408
- return desc ? (desc as NodeViewDesc).nodeDOM : null
409
- }
410
-
411
- /// Find the document position that corresponds to a given DOM
412
- /// position. (Whenever possible, it is preferable to inspect the
413
- /// document structure directly, rather than poking around in the
414
- /// DOM, but sometimes—for example when interpreting an event
415
- /// target—you don't have a choice.)
416
- ///
417
- /// The `bias` parameter can be used to influence which side of a DOM
418
- /// node to use when the position is inside a leaf node.
419
- posAtDOM(node: DOMNode, offset: number, bias = -1): number {
420
- let pos = this.docView.posFromDOM(node, offset, bias)
421
- if (pos == null) throw new RangeError("DOM position not inside the editor")
422
- return pos
423
- }
424
-
425
- /// Find out whether the selection is at the end of a textblock when
426
- /// moving in a given direction. When, for example, given `"left"`,
427
- /// it will return true if moving left from the current cursor
428
- /// position would leave that position's parent textblock. Will apply
429
- /// to the view's current state by default, but it is possible to
430
- /// pass a different state.
431
- endOfTextblock(dir: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean {
432
- return endOfTextblock(this, state || this.state, dir)
433
- }
434
-
435
- /// Run the editor's paste logic with the given HTML string. The
436
- /// `event`, if given, will be passed to the
437
- /// [`handlePaste`](#view.EditorProps.handlePaste) hook.
438
- pasteHTML(html: string, event?: ClipboardEvent) {
439
- return doPaste(this, "", html, false, event || new ClipboardEvent("paste"))
440
- }
441
-
442
- /// Run the editor's paste logic with the given plain-text input.
443
- pasteText(text: string, event?: ClipboardEvent) {
444
- return doPaste(this, text, null, true, event || new ClipboardEvent("paste"))
445
- }
446
-
447
- /// Serialize the given slice as it would be if it was copied from
448
- /// this editor. Returns a DOM element that contains a
449
- /// representation of the slice as its children, a textual
450
- /// representation, and the transformed slice (which can be
451
- /// different from the given input due to hooks like
452
- /// [`transformCopied`](#view.EditorProps.transformCopied)).
453
- serializeForClipboard(slice: Slice): {dom: HTMLElement, text: string, slice: Slice} {
454
- return serializeForClipboard(this, slice)
455
- }
456
-
457
- /// Removes the editor from the DOM and destroys all [node
458
- /// views](#view.NodeView).
459
- destroy() {
460
- if (!this.docView) return
461
- destroyInput(this)
462
- this.destroyPluginViews()
463
- if (this.mounted) {
464
- this.docView.update(this.state.doc, [], viewDecorations(this), this)
465
- this.dom.textContent = ""
466
- } else if (this.dom.parentNode) {
467
- this.dom.parentNode.removeChild(this.dom)
468
- }
469
- this.docView.destroy()
470
- ;(this as any).docView = null
471
- clearReusedRange();
472
- }
473
-
474
- /// This is true when the view has been
475
- /// [destroyed](#view.EditorView.destroy) (and thus should not be
476
- /// used anymore).
477
- get isDestroyed() {
478
- return this.docView == null
479
- }
480
-
481
- /// Used for testing.
482
- dispatchEvent(event: Event) {
483
- return dispatchEvent(this, event)
484
- }
485
-
486
- /// Dispatch a transaction. Will call
487
- /// [`dispatchTransaction`](#view.DirectEditorProps.dispatchTransaction)
488
- /// when given, and otherwise defaults to applying the transaction to
489
- /// the current state and calling
490
- /// [`updateState`](#view.EditorView.updateState) with the result.
491
- /// This method is bound to the view instance, so that it can be
492
- /// easily passed around.
493
- dispatch(tr: Transaction) {
494
- let pendingDOMChange = this.domObserver.getDOMChange()
495
- let dispatchTransaction = this._props.dispatchTransaction
496
- if (dispatchTransaction) dispatchTransaction.call(this, tr)
497
- else this.updateState(this.state.apply(tr))
498
- }
499
-
500
- /// @internal
501
- domSelectionRange(): DOMSelectionRange {
502
- let sel = this.domSelection()
503
- if (!sel) return {focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0}
504
- return browser.safari && this.root.nodeType === 11 &&
505
- deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel
506
- }
507
-
508
- /// @internal
509
- domSelection(): DOMSelection | null {
510
- return (this.root as Document).getSelection()
511
- }
512
- }
513
-
514
- function computeDocDeco(view: EditorView) {
515
- let attrs = Object.create(null)
516
- attrs.class = "ProseMirror"
517
- attrs.contenteditable = String(view.editable)
518
-
519
- view.someProp("attributes", value => {
520
- if (typeof value == "function") value = value(view.state)
521
- if (value) for (let attr in value) {
522
- if (attr == "class")
523
- attrs.class += " " + value[attr]
524
- else if (attr == "style")
525
- attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]
526
- else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName")
527
- attrs[attr] = String(value[attr])
528
- }
529
- })
530
- if (!attrs.translate) attrs.translate = "no"
531
-
532
- return [Decoration.node(0, view.state.doc.content.size, attrs)]
533
- }
534
-
535
- function updateCursorWrapper(view: EditorView) {
536
- if (view.markCursor) {
537
- let dom = document.createElement("img")
538
- dom.className = "ProseMirror-separator"
539
- dom.setAttribute("mark-placeholder", "true")
540
- dom.setAttribute("alt", "")
541
- view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.from,
542
- dom, {raw: true, marks: view.markCursor} as any)}
543
- } else {
544
- view.cursorWrapper = null
545
- }
546
- }
547
-
548
- function getEditable(view: EditorView) {
549
- return !view.someProp("editable", value => value(view.state) === false)
550
- }
551
-
552
- function selectionContextChanged(sel1: Selection, sel2: Selection) {
553
- let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head))
554
- return sel1.$anchor.start(depth) != sel2.$anchor.start(depth)
555
- }
556
-
557
- function buildNodeViews(view: EditorView) {
558
- let result: NodeViewSet = Object.create(null)
559
- function add(obj: NodeViewSet) {
560
- for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop))
561
- result[prop] = obj[prop]
562
- }
563
- view.someProp("nodeViews", add)
564
- view.someProp("markViews", add)
565
- return result
566
- }
567
-
568
- function changedNodeViews(a: NodeViewSet, b: NodeViewSet) {
569
- let nA = 0, nB = 0
570
- for (let prop in a) {
571
- if (a[prop] != b[prop]) return true
572
- nA++
573
- }
574
- for (let _ in b) nB++
575
- return nA != nB
576
- }
577
-
578
- function checkStateComponent(plugin: Plugin) {
579
- if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction)
580
- throw new RangeError("Plugins passed directly to the view must not have a state component")
581
- }
582
-
583
- /// The type of function [provided](#view.EditorProps.nodeViews) to
584
- /// create [node views](#view.NodeView).
585
- export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined,
586
- decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView
587
-
588
- /// The function types [used](#view.EditorProps.markViews) to create
589
- /// mark views.
590
- export type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => MarkView
591
-
592
- type NodeViewSet = {[name: string]: NodeViewConstructor | MarkViewConstructor}
593
-
594
- /// Helper type that maps event names to event object types, but
595
- /// includes events that TypeScript's HTMLElementEventMap doesn't know
596
- /// about.
597
- export interface DOMEventMap extends HTMLElementEventMap {
598
- [event: string]: any
599
- }
600
-
601
- /// Props are configuration values that can be passed to an editor view
602
- /// or included in a plugin. This interface lists the supported props.
603
- ///
604
- /// The various event-handling functions may all return `true` to
605
- /// indicate that they handled the given event. The view will then take
606
- /// care to call `preventDefault` on the event, except with
607
- /// `handleDOMEvents`, where the handler itself is responsible for that.
608
- ///
609
- /// How a prop is resolved depends on the prop. Handler functions are
610
- /// called one at a time, starting with the base props and then
611
- /// searching through the plugins (in order of appearance) until one of
612
- /// them returns true. For some props, the first plugin that yields a
613
- /// value gets precedence.
614
- ///
615
- /// The optional type parameter refers to the type of `this` in prop
616
- /// functions, and is used to pass in the plugin type when defining a
617
- /// [plugin](#state.Plugin).
618
- export interface EditorProps<P = any> {
619
- /// Can be an object mapping DOM event type names to functions that
620
- /// handle them. Such functions will be called before any handling
621
- /// ProseMirror does of events fired on the editable DOM element.
622
- /// Contrary to the other event handling props, when returning true
623
- /// from such a function, you are responsible for calling
624
- /// `preventDefault` yourself (or not, if you want to allow the
625
- /// default behavior).
626
- handleDOMEvents?: {
627
- [event in keyof DOMEventMap]?: (this: P, view: EditorView, event: DOMEventMap[event]) => boolean | void
628
- }
629
-
630
- /// Called when the editor receives a `keydown` event.
631
- handleKeyDown?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
632
-
633
- /// Handler for `keypress` events.
634
- handleKeyPress?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
635
-
636
- /// Whenever the user directly input text, this handler is called
637
- /// before the input is applied. If it returns `true`, the default
638
- /// behavior of actually inserting the text is suppressed.
639
- handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void
640
-
641
- /// Called for each node around a click, from the inside out. The
642
- /// `direct` flag will be true for the inner node.
643
- handleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
644
-
645
- /// Called when the editor is clicked, after `handleClickOn` handlers
646
- /// have been called.
647
- handleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
648
-
649
- /// Called for each node around a double click.
650
- handleDoubleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
651
-
652
- /// Called when the editor is double-clicked, after `handleDoubleClickOn`.
653
- handleDoubleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
654
-
655
- /// Called for each node around a triple click.
656
- handleTripleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
657
-
658
- /// Called when the editor is triple-clicked, after `handleTripleClickOn`.
659
- handleTripleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
660
-
661
- /// Can be used to override the behavior of pasting. `slice` is the
662
- /// pasted content parsed by the editor, but you can directly access
663
- /// the event to get at the raw content.
664
- handlePaste?: (this: P, view: EditorView, event: ClipboardEvent, slice: Slice) => boolean | void
665
-
666
- /// Called when something is dropped on the editor. `moved` will be
667
- /// true if this drop moves from the current selection (which should
668
- /// thus be deleted).
669
- handleDrop?: (this: P, view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => boolean | void
670
-
671
- /// Called when the view, after updating its state, tries to scroll
672
- /// the selection into view. A handler function may return false to
673
- /// indicate that it did not handle the scrolling and further
674
- /// handlers or the default behavior should be tried.
675
- handleScrollToSelection?: (this: P, view: EditorView) => boolean
676
-
677
- /// Determines whether an in-editor drag event should copy or move
678
- /// the selection. When not given, the event's `altKey` property is
679
- /// used on macOS, `ctrlKey` on other platforms.
680
- dragCopies?: (event: DragEvent) => boolean
681
-
682
- /// Can be used to override the way a selection is created when
683
- /// reading a DOM selection between the given anchor and head.
684
- createSelectionBetween?: (this: P, view: EditorView, anchor: ResolvedPos, head: ResolvedPos) => Selection | null
685
-
686
- /// The [parser](#model.DOMParser) to use when reading editor changes
687
- /// from the DOM. Defaults to calling
688
- /// [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) on the
689
- /// editor's schema.
690
- domParser?: DOMParser
691
-
692
- /// Can be used to transform pasted HTML text, _before_ it is parsed,
693
- /// for example to clean it up.
694
- transformPastedHTML?: (this: P, html: string, view: EditorView) => string
695
-
696
- /// The [parser](#model.DOMParser) to use when reading content from
697
- /// the clipboard. When not given, the value of the
698
- /// [`domParser`](#view.EditorProps.domParser) prop is used.
699
- clipboardParser?: DOMParser
700
-
701
- /// Transform pasted plain text. The `plain` flag will be true when
702
- /// the text is pasted as plain text.
703
- transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string
704
-
705
- /// A function to parse text from the clipboard into a document
706
- /// slice. Called after
707
- /// [`transformPastedText`](#view.EditorProps.transformPastedText).
708
- /// The default behavior is to split the text into lines, wrap them
709
- /// in `<p>` tags, and call
710
- /// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
711
- /// The `plain` flag will be true when the text is pasted as plain text.
712
- clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
713
-
714
- /// Can be used to transform pasted or dragged-and-dropped content
715
- /// before it is applied to the document.
716
- transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice
717
-
718
- /// Can be used to transform copied or cut content before it is
719
- /// serialized to the clipboard.
720
- transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice
721
-
722
- /// Allows you to pass custom rendering and behavior logic for
723
- /// nodes. Should map node names to constructor functions that
724
- /// produce a [`NodeView`](#view.NodeView) object implementing the
725
- /// node's display behavior. The third argument `getPos` is a
726
- /// function that can be called to get the node's current position,
727
- /// which can be useful when creating transactions to update it.
728
- /// Note that if the node is not in the document, the position
729
- /// returned by this function will be `undefined`.
730
- ///
731
- /// `decorations` is an array of node or inline decorations that are
732
- /// active around the node. They are automatically drawn in the
733
- /// normal way, and you will usually just want to ignore this, but
734
- /// they can also be used as a way to provide context information to
735
- /// the node view without adding it to the document itself.
736
- ///
737
- /// `innerDecorations` holds the decorations for the node's content.
738
- /// You can safely ignore this if your view has no content or a
739
- /// `contentDOM` property, since the editor will draw the decorations
740
- /// on the content. But if you, for example, want to create a nested
741
- /// editor with the content, it may make sense to provide it with the
742
- /// inner decorations.
743
- ///
744
- /// (For backwards compatibility reasons, [mark
745
- /// views](#view.EditorProps.markViews) can also be included in this
746
- /// object.)
747
- nodeViews?: {[node: string]: NodeViewConstructor}
748
-
749
- /// Pass custom mark rendering functions. Note that these cannot
750
- /// provide the kind of dynamic behavior that [node
751
- /// views](#view.NodeView) can—they just provide custom rendering
752
- /// logic. The third argument indicates whether the mark's content
753
- /// is inline.
754
- markViews?: {[mark: string]: MarkViewConstructor}
755
-
756
- /// The DOM serializer to use when putting content onto the
757
- /// clipboard. If not given, the result of
758
- /// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)
759
- /// will be used. This object will only have its
760
- /// [`serializeFragment`](#model.DOMSerializer.serializeFragment)
761
- /// method called, and you may provide an alternative object type
762
- /// implementing a compatible method.
763
- clipboardSerializer?: DOMSerializer
764
-
765
- /// A function that will be called to get the text for the current
766
- /// selection when copying text to the clipboard. By default, the
767
- /// editor will use [`textBetween`](#model.Node.textBetween) on the
768
- /// selected range.
769
- clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string
770
-
771
- /// A set of [document decorations](#view.Decoration) to show in the
772
- /// view.
773
- decorations?: (this: P, state: EditorState) => DecorationSource | null | undefined
774
-
775
- /// When this returns false, the content of the view is not directly
776
- /// editable.
777
- editable?: (this: P, state: EditorState) => boolean
778
-
779
- /// Control the DOM attributes of the editable element. May be either
780
- /// an object or a function going from an editor state to an object.
781
- /// By default, the element will get a class `"ProseMirror"`, and
782
- /// will have its `contentEditable` attribute determined by the
783
- /// [`editable` prop](#view.EditorProps.editable). Additional classes
784
- /// provided here will be added to the class. For other attributes,
785
- /// the value provided first (as in
786
- /// [`someProp`](#view.EditorView.someProp)) will be used.
787
- attributes?: {[name: string]: string} | ((state: EditorState) => {[name: string]: string})
788
-
789
- /// Determines the distance (in pixels) between the cursor and the
790
- /// end of the visible viewport at which point, when scrolling the
791
- /// cursor into view, scrolling takes place. Defaults to 0.
792
- scrollThreshold?: number | {top: number, right: number, bottom: number, left: number}
793
-
794
- /// Determines the extra space (in pixels) that is left above or
795
- /// below the cursor when it is scrolled into view. Defaults to 5.
796
- scrollMargin?: number | {top: number, right: number, bottom: number, left: number}
797
- }
798
-
799
- /// The props object given directly to the editor view supports some
800
- /// fields that can't be used in plugins:
801
- export interface DirectEditorProps extends EditorProps {
802
- /// The current state of the editor.
803
- state: EditorState
804
-
805
- /// A set of plugins to use in the view, applying their [plugin
806
- /// view](#state.PluginSpec.view) and
807
- /// [props](#state.PluginSpec.props). Passing plugins with a state
808
- /// component (a [state field](#state.PluginSpec.state) field or a
809
- /// [transaction](#state.PluginSpec.filterTransaction) filter or
810
- /// appender) will result in an error, since such plugins must be
811
- /// present in the state to work.
812
- plugins?: readonly Plugin[]
813
-
814
- /// The callback over which to send transactions (state updates)
815
- /// produced by the view. If you specify this, you probably want to
816
- /// make sure this ends up calling the view's
817
- /// [`updateState`](#view.EditorView.updateState) method with a new
818
- /// state that has the transaction
819
- /// [applied](#state.EditorState.apply). The callback will be bound to have
820
- /// the view instance as its `this` binding.
821
- dispatchTransaction?: (tr: Transaction) => void
822
- }