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