prosemirror-view 1.33.0 → 1.33.2

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