@depup/prosemirror-state 1.4.4-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/state.ts ADDED
@@ -0,0 +1,266 @@
1
+ import {Node, Mark, Schema} from "prosemirror-model"
2
+
3
+ import {Selection, TextSelection} from "./selection"
4
+ import {Transaction} from "./transaction"
5
+ import {Plugin, StateField} from "./plugin"
6
+
7
+ function bind<T extends Function>(f: T, self: any): T {
8
+ return !self || !f ? f : f.bind(self)
9
+ }
10
+
11
+ class FieldDesc<T> {
12
+ init: (config: EditorStateConfig, instance: EditorState) => T
13
+ apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T
14
+
15
+ constructor(readonly name: string, desc: StateField<any>, self?: any) {
16
+ this.init = bind(desc.init, self)
17
+ this.apply = bind(desc.apply, self)
18
+ }
19
+ }
20
+
21
+ const baseFields = [
22
+ new FieldDesc<Node>("doc", {
23
+ init(config) { return config.doc || config.schema!.topNodeType.createAndFill() },
24
+ apply(tr) { return tr.doc }
25
+ }),
26
+
27
+ new FieldDesc<Selection>("selection", {
28
+ init(config, instance) { return config.selection || Selection.atStart(instance.doc) },
29
+ apply(tr) { return tr.selection }
30
+ }),
31
+
32
+ new FieldDesc<readonly Mark[] | null>("storedMarks", {
33
+ init(config) { return config.storedMarks || null },
34
+ apply(tr, _marks, _old, state) { return (state.selection as TextSelection).$cursor ? tr.storedMarks : null }
35
+ }),
36
+
37
+ new FieldDesc<number>("scrollToSelection", {
38
+ init() { return 0 },
39
+ apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev }
40
+ })
41
+ ]
42
+
43
+ // Object wrapping the part of a state object that stays the same
44
+ // across transactions. Stored in the state's `config` property.
45
+ class Configuration {
46
+ fields: FieldDesc<any>[]
47
+ plugins: Plugin[] = []
48
+ pluginsByKey: {[key: string]: Plugin} = Object.create(null)
49
+
50
+ constructor(readonly schema: Schema, plugins?: readonly Plugin[]) {
51
+ this.fields = baseFields.slice()
52
+ if (plugins) plugins.forEach(plugin => {
53
+ if (this.pluginsByKey[plugin.key])
54
+ throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")")
55
+ this.plugins.push(plugin)
56
+ this.pluginsByKey[plugin.key] = plugin
57
+ if (plugin.spec.state)
58
+ this.fields.push(new FieldDesc<any>(plugin.key, plugin.spec.state, plugin))
59
+ })
60
+ }
61
+ }
62
+
63
+ /// The type of object passed to
64
+ /// [`EditorState.create`](#state.EditorState^create).
65
+ export interface EditorStateConfig {
66
+ /// The schema to use (only relevant if no `doc` is specified).
67
+ schema?: Schema
68
+
69
+ /// The starting document. Either this or `schema` _must_ be
70
+ /// provided.
71
+ doc?: Node
72
+
73
+ /// A valid selection in the document.
74
+ selection?: Selection
75
+
76
+ /// The initial set of [stored marks](#state.EditorState.storedMarks).
77
+ storedMarks?: readonly Mark[] | null
78
+
79
+ /// The plugins that should be active in this state.
80
+ plugins?: readonly Plugin[]
81
+ }
82
+
83
+ /// The state of a ProseMirror editor is represented by an object of
84
+ /// this type. A state is a persistent data structure—it isn't
85
+ /// updated, but rather a new state value is computed from an old one
86
+ /// using the [`apply`](#state.EditorState.apply) method.
87
+ ///
88
+ /// A state holds a number of built-in fields, and plugins can
89
+ /// [define](#state.PluginSpec.state) additional fields.
90
+ export class EditorState {
91
+ /// @internal
92
+ constructor(
93
+ /// @internal
94
+ readonly config: Configuration
95
+ ) {}
96
+
97
+ /// The current document.
98
+ declare doc: Node
99
+
100
+ /// The selection.
101
+ declare selection: Selection
102
+
103
+ /// A set of marks to apply to the next input. Will be null when
104
+ /// no explicit marks have been set.
105
+ declare storedMarks: readonly Mark[] | null
106
+
107
+ /// The schema of the state's document.
108
+ get schema(): Schema {
109
+ return this.config.schema
110
+ }
111
+
112
+ /// The plugins that are active in this state.
113
+ get plugins(): readonly Plugin[] {
114
+ return this.config.plugins
115
+ }
116
+
117
+ /// Apply the given transaction to produce a new state.
118
+ apply(tr: Transaction): EditorState {
119
+ return this.applyTransaction(tr).state
120
+ }
121
+
122
+ /// @internal
123
+ filterTransaction(tr: Transaction, ignore = -1) {
124
+ for (let i = 0; i < this.config.plugins.length; i++) if (i != ignore) {
125
+ let plugin = this.config.plugins[i]
126
+ if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this))
127
+ return false
128
+ }
129
+ return true
130
+ }
131
+
132
+ /// Verbose variant of [`apply`](#state.EditorState.apply) that
133
+ /// returns the precise transactions that were applied (which might
134
+ /// be influenced by the [transaction
135
+ /// hooks](#state.PluginSpec.filterTransaction) of
136
+ /// plugins) along with the new state.
137
+ applyTransaction(rootTr: Transaction): {state: EditorState, transactions: readonly Transaction[]} {
138
+ if (!this.filterTransaction(rootTr)) return {state: this, transactions: []}
139
+
140
+ let trs = [rootTr], newState = this.applyInner(rootTr), seen = null
141
+ // This loop repeatedly gives plugins a chance to respond to
142
+ // transactions as new transactions are added, making sure to only
143
+ // pass the transactions the plugin did not see before.
144
+ for (;;) {
145
+ let haveNew = false
146
+ for (let i = 0; i < this.config.plugins.length; i++) {
147
+ let plugin = this.config.plugins[i]
148
+ if (plugin.spec.appendTransaction) {
149
+ let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this
150
+ let tr = n < trs.length &&
151
+ plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState)
152
+ if (tr && newState.filterTransaction(tr, i)) {
153
+ tr.setMeta("appendedTransaction", rootTr)
154
+ if (!seen) {
155
+ seen = []
156
+ for (let j = 0; j < this.config.plugins.length; j++)
157
+ seen.push(j < i ? {state: newState, n: trs.length} : {state: this, n: 0})
158
+ }
159
+ trs.push(tr)
160
+ newState = newState.applyInner(tr)
161
+ haveNew = true
162
+ }
163
+ if (seen) seen[i] = {state: newState, n: trs.length}
164
+ }
165
+ }
166
+ if (!haveNew) return {state: newState, transactions: trs}
167
+ }
168
+ }
169
+
170
+ /// @internal
171
+ applyInner(tr: Transaction) {
172
+ if (!tr.before.eq(this.doc)) throw new RangeError("Applying a mismatched transaction")
173
+ let newInstance = new EditorState(this.config), fields = this.config.fields
174
+ for (let i = 0; i < fields.length; i++) {
175
+ let field = fields[i]
176
+ ;(newInstance as any)[field.name] = field.apply(tr, (this as any)[field.name], this, newInstance)
177
+ }
178
+ return newInstance
179
+ }
180
+
181
+ /// Accessor that constructs and returns a new [transaction](#state.Transaction) from this state.
182
+ get tr(): Transaction { return new Transaction(this) }
183
+
184
+ /// Create a new state.
185
+ static create(config: EditorStateConfig) {
186
+ let $config = new Configuration(config.doc ? config.doc.type.schema : config.schema!, config.plugins)
187
+ let instance = new EditorState($config)
188
+ for (let i = 0; i < $config.fields.length; i++)
189
+ (instance as any)[$config.fields[i].name] = $config.fields[i].init(config, instance)
190
+ return instance
191
+ }
192
+
193
+ /// Create a new state based on this one, but with an adjusted set
194
+ /// of active plugins. State fields that exist in both sets of
195
+ /// plugins are kept unchanged. Those that no longer exist are
196
+ /// dropped, and those that are new are initialized using their
197
+ /// [`init`](#state.StateField.init) method, passing in the new
198
+ /// configuration object..
199
+ reconfigure(config: {
200
+ /// New set of active plugins.
201
+ plugins?: readonly Plugin[]
202
+ }) {
203
+ let $config = new Configuration(this.schema, config.plugins)
204
+ let fields = $config.fields, instance = new EditorState($config)
205
+ for (let i = 0; i < fields.length; i++) {
206
+ let name = fields[i].name
207
+ ;(instance as any)[name] = this.hasOwnProperty(name) ? (this as any)[name] : fields[i].init(config, instance)
208
+ }
209
+ return instance
210
+ }
211
+
212
+ /// Serialize this state to JSON. If you want to serialize the state
213
+ /// of plugins, pass an object mapping property names to use in the
214
+ /// resulting JSON object to plugin objects. The argument may also be
215
+ /// a string or number, in which case it is ignored, to support the
216
+ /// way `JSON.stringify` calls `toString` methods.
217
+ toJSON(pluginFields?: {[propName: string]: Plugin}): any {
218
+ let result: any = {doc: this.doc.toJSON(), selection: this.selection.toJSON()}
219
+ if (this.storedMarks) result.storedMarks = this.storedMarks.map(m => m.toJSON())
220
+ if (pluginFields && typeof pluginFields == 'object') for (let prop in pluginFields) {
221
+ if (prop == "doc" || prop == "selection")
222
+ throw new RangeError("The JSON fields `doc` and `selection` are reserved")
223
+ let plugin = pluginFields[prop], state = plugin.spec.state
224
+ if (state && state.toJSON) result[prop] = state.toJSON.call(plugin, (this as any)[plugin.key])
225
+ }
226
+ return result
227
+ }
228
+
229
+ /// Deserialize a JSON representation of a state. `config` should
230
+ /// have at least a `schema` field, and should contain array of
231
+ /// plugins to initialize the state with. `pluginFields` can be used
232
+ /// to deserialize the state of plugins, by associating plugin
233
+ /// instances with the property names they use in the JSON object.
234
+ static fromJSON(config: {
235
+ /// The schema to use.
236
+ schema: Schema
237
+ /// The set of active plugins.
238
+ plugins?: readonly Plugin[]
239
+ }, json: any, pluginFields?: {[propName: string]: Plugin}) {
240
+ if (!json) throw new RangeError("Invalid input for EditorState.fromJSON")
241
+ if (!config.schema) throw new RangeError("Required config field 'schema' missing")
242
+ let $config = new Configuration(config.schema, config.plugins)
243
+ let instance = new EditorState($config)
244
+ $config.fields.forEach(field => {
245
+ if (field.name == "doc") {
246
+ instance.doc = Node.fromJSON(config.schema, json.doc)
247
+ } else if (field.name == "selection") {
248
+ instance.selection = Selection.fromJSON(instance.doc, json.selection)
249
+ } else if (field.name == "storedMarks") {
250
+ if (json.storedMarks) instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON)
251
+ } else {
252
+ if (pluginFields) for (let prop in pluginFields) {
253
+ let plugin = pluginFields[prop], state = plugin.spec.state
254
+ if (plugin.key == field.name && state && state.fromJSON &&
255
+ Object.prototype.hasOwnProperty.call(json, prop)) {
256
+ // This field belongs to a plugin mapped to a JSON field, read it from there.
257
+ ;(instance as any)[field.name] = state.fromJSON.call(plugin, config, json[prop], instance)
258
+ return
259
+ }
260
+ }
261
+ ;(instance as any)[field.name] = field.init(config, instance)
262
+ }
263
+ })
264
+ return instance
265
+ }
266
+ }
@@ -0,0 +1,215 @@
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
+ }