tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }