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