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 }