tor-browser

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

transaction.ts (7932B)


      1 import {Transform, Step} from "prosemirror-transform"
      2 import {Mark, MarkType, Node, Slice} from "prosemirror-model"
      3 import {type EditorView} from "prosemirror-view"
      4 import {Selection} from "./selection"
      5 import {Plugin, PluginKey} from "./plugin"
      6 import {EditorState} from "./state"
      7 
      8 /// Commands are functions that take a state and a an optional
      9 /// transaction dispatch function and...
     10 ///
     11 ///  - determine whether they apply to this state
     12 ///  - if not, return false
     13 ///  - if `dispatch` was passed, perform their effect, possibly by
     14 ///    passing a transaction to `dispatch`
     15 ///  - return true
     16 ///
     17 /// In some cases, the editor view is passed as a third argument.
     18 export type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
     19 
     20 const UPDATED_SEL = 1, UPDATED_MARKS = 2, UPDATED_SCROLL = 4
     21 
     22 /// An editor state transaction, which can be applied to a state to
     23 /// create an updated state. Use
     24 /// [`EditorState.tr`](#state.EditorState.tr) to create an instance.
     25 ///
     26 /// Transactions track changes to the document (they are a subclass of
     27 /// [`Transform`](#transform.Transform)), but also other state changes,
     28 /// like selection updates and adjustments of the set of [stored
     29 /// marks](#state.EditorState.storedMarks). In addition, you can store
     30 /// metadata properties in a transaction, which are extra pieces of
     31 /// information that client code or plugins can use to describe what a
     32 /// transaction represents, so that they can update their [own
     33 /// state](#state.StateField) accordingly.
     34 ///
     35 /// The [editor view](#view.EditorView) uses a few metadata
     36 /// properties: it will attach a property `"pointer"` with the value
     37 /// `true` to selection transactions directly caused by mouse or touch
     38 /// input, a `"composition"` property holding an ID identifying the
     39 /// composition that caused it to transactions caused by composed DOM
     40 /// input, and a `"uiEvent"` property of that may be `"paste"`,
     41 /// `"cut"`, or `"drop"`.
     42 export class Transaction extends Transform {
     43  /// The timestamp associated with this transaction, in the same
     44  /// format as `Date.now()`.
     45  time: number
     46 
     47  private curSelection: Selection
     48  // The step count for which the current selection is valid.
     49  private curSelectionFor = 0
     50  // Bitfield to track which aspects of the state were updated by
     51  // this transaction.
     52  private updated = 0
     53  // Object used to store metadata properties for the transaction.
     54  private meta: {[name: string]: any} = Object.create(null)
     55 
     56  /// The stored marks set by this transaction, if any.
     57  storedMarks: readonly Mark[] | null
     58 
     59  /// @internal
     60  constructor(state: EditorState) {
     61    super(state.doc)
     62    this.time = Date.now()
     63    this.curSelection = state.selection
     64    this.storedMarks = state.storedMarks
     65  }
     66 
     67  /// The transaction's current selection. This defaults to the editor
     68  /// selection [mapped](#state.Selection.map) through the steps in the
     69  /// transaction, but can be overwritten with
     70  /// [`setSelection`](#state.Transaction.setSelection).
     71  get selection(): Selection {
     72    if (this.curSelectionFor < this.steps.length) {
     73      this.curSelection = this.curSelection.map(this.doc, this.mapping.slice(this.curSelectionFor))
     74      this.curSelectionFor = this.steps.length
     75    }
     76    return this.curSelection
     77  }
     78 
     79  /// Update the transaction's current selection. Will determine the
     80  /// selection that the editor gets when the transaction is applied.
     81  setSelection(selection: Selection): this {
     82    if (selection.$from.doc != this.doc)
     83      throw new RangeError("Selection passed to setSelection must point at the current document")
     84    this.curSelection = selection
     85    this.curSelectionFor = this.steps.length
     86    this.updated = (this.updated | UPDATED_SEL) & ~UPDATED_MARKS
     87    this.storedMarks = null
     88    return this
     89  }
     90 
     91  /// Whether the selection was explicitly updated by this transaction.
     92  get selectionSet() {
     93    return (this.updated & UPDATED_SEL) > 0
     94  }
     95 
     96  /// Set the current stored marks.
     97  setStoredMarks(marks: readonly Mark[] | null): this {
     98    this.storedMarks = marks
     99    this.updated |= UPDATED_MARKS
    100    return this
    101  }
    102 
    103  /// Make sure the current stored marks or, if that is null, the marks
    104  /// at the selection, match the given set of marks. Does nothing if
    105  /// this is already the case.
    106  ensureMarks(marks: readonly Mark[]): this {
    107    if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks))
    108      this.setStoredMarks(marks)
    109    return this
    110  }
    111 
    112  /// Add a mark to the set of stored marks.
    113  addStoredMark(mark: Mark): this {
    114    return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks()))
    115  }
    116 
    117  /// Remove a mark or mark type from the set of stored marks.
    118  removeStoredMark(mark: Mark | MarkType): this {
    119    return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks()))
    120  }
    121 
    122  /// Whether the stored marks were explicitly set for this transaction.
    123  get storedMarksSet() {
    124    return (this.updated & UPDATED_MARKS) > 0
    125  }
    126 
    127  /// @internal
    128  addStep(step: Step, doc: Node) {
    129    super.addStep(step, doc)
    130    this.updated = this.updated & ~UPDATED_MARKS
    131    this.storedMarks = null
    132  }
    133 
    134  /// Update the timestamp for the transaction.
    135  setTime(time: number): this {
    136    this.time = time
    137    return this
    138  }
    139 
    140  /// Replace the current selection with the given slice.
    141  replaceSelection(slice: Slice): this {
    142    this.selection.replace(this, slice)
    143    return this
    144  }
    145 
    146  /// Replace the selection with the given node. When `inheritMarks` is
    147  /// true and the content is inline, it inherits the marks from the
    148  /// place where it is inserted.
    149  replaceSelectionWith(node: Node, inheritMarks = true): this {
    150    let selection = this.selection
    151    if (inheritMarks)
    152      node = node.mark(this.storedMarks || (selection.empty ? selection.$from.marks() : (selection.$from.marksAcross(selection.$to) || Mark.none)))
    153    selection.replaceWith(this, node)
    154    return this
    155  }
    156 
    157  /// Delete the selection.
    158  deleteSelection(): this {
    159    this.selection.replace(this)
    160    return this
    161  }
    162 
    163  /// Replace the given range, or the selection if no range is given,
    164  /// with a text node containing the given string.
    165  insertText(text: string, from?: number, to?: number): this {
    166    let schema = this.doc.type.schema
    167    if (from == null) {
    168      if (!text) return this.deleteSelection()
    169      return this.replaceSelectionWith(schema.text(text), true)
    170    } else {
    171      if (to == null) to = from
    172      if (!text) return this.deleteRange(from, to)
    173      let marks = this.storedMarks
    174      if (!marks) {
    175        let $from = this.doc.resolve(from)
    176        marks = to == from ? $from.marks() : $from.marksAcross(this.doc.resolve(to))
    177      }
    178      this.replaceRangeWith(from, to, schema.text(text, marks))
    179      if (!this.selection.empty && this.selection.to == from + text.length)
    180        this.setSelection(Selection.near(this.selection.$to))
    181      return this
    182    }
    183  }
    184 
    185  /// Store a metadata property in this transaction, keyed either by
    186  /// name or by plugin.
    187  setMeta(key: string | Plugin | PluginKey, value: any): this {
    188    this.meta[typeof key == "string" ? key : key.key] = value
    189    return this
    190  }
    191 
    192  /// Retrieve a metadata property for a given name or plugin.
    193  getMeta(key: string | Plugin | PluginKey) {
    194    return this.meta[typeof key == "string" ? key : key.key]
    195  }
    196 
    197  /// Returns true if this transaction doesn't contain any metadata,
    198  /// and can thus safely be extended.
    199  get isGeneric() {
    200    for (let _ in this.meta) return false
    201    return true
    202  }
    203 
    204  /// Indicate that the editor should scroll the selection into view
    205  /// when updated to the state produced by this transaction.
    206  scrollIntoView(): this {
    207    this.updated |= UPDATED_SCROLL
    208    return this
    209  }
    210 
    211  /// True when this transaction has had `scrollIntoView` called on it.
    212  get scrolledIntoView() {
    213    return (this.updated & UPDATED_SCROLL) > 0
    214  }
    215 }