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