This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"libs": ["browser"],
"plugins": {
"node": {},
"complete_strings": {},
"es_modules": {}
}
}

View File

@@ -0,0 +1,104 @@
# How to contribute
- [Getting help](#getting-help)
- [Submitting bug reports](#submitting-bug-reports)
- [Contributing code](#contributing-code)
## Getting help
Community discussion, questions, and informal bug reporting is done on the
[discuss.ProseMirror forum](http://discuss.prosemirror.net).
## Submitting bug reports
Report bugs on the
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
Before reporting a bug, please read these pointers.
- The issue tracker is for *bugs*, not requests for help. Questions
should be asked on the [forum](http://discuss.prosemirror.net).
- Include information about the version of the code that exhibits the
problem. For browser-related issues, include the browser and browser
version on which the problem occurred.
- Mention very precisely what went wrong. "X is broken" is not a good
bug report. What did you expect to happen? What happened instead?
Describe the exact steps a maintainer has to take to make the
problem occur. A screencast can be useful, but is no substitute for
a textual description.
- A great way to make it easy to reproduce your problem, if it can not
be trivially reproduced on the website demos, is to submit a script
that triggers the issue.
## Contributing code
If you want to make a change that involves a significant overhaul of
the code or introduces a user-visible new feature, create an
[RFC](https://github.com/ProseMirror/rfcs/) first with your proposal.
- Make sure you have a [GitHub Account](https://github.com/signup/free)
- Fork the relevant repository
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Create a local checkout of the code. You can use the
[main repository](https://github.com/prosemirror/prosemirror) to
easily check out all core modules.
- Make your changes, and commit them
- Follow the code style of the rest of the project (see below). Run
`npm run lint` (in the main repository checkout) to make sure that
the linter is happy.
- If your changes are easy to test or likely to regress, add tests in
the relevant `test/` directory. Either put them in an existing
`test-*.js` file, if they fit there, or add a new file.
- Make sure all tests pass. Run `npm run test` to verify tests pass
(you will need Node.js v6+).
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to ProseMirror you
- Agree to license the contributed code under the project's [MIT
license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Coding standards
- ES6 syntax, targeting an ES5 runtime (i.e. don't use library
elements added by ES6, don't use ES7/ES.next syntax).
- 2 spaces per indentation level, no tabs.
- No semicolons except when necessary.
- Follow the surrounding code when it comes to spacing, brace
placement, etc.
- Brace-less single-statement bodies are encouraged (whenever they
don't impact readability).
- [getdocs](https://github.com/marijnh/getdocs)-style doc comments
above items that are part of the public API.
- When documenting non-public items, you can put the type after a
single colon, so that getdocs doesn't pick it up and add it to the
API reference.
- The linter (`npm run lint`) complains about unused variables and
functions. Prefix their names with an underscore to muffle it.
- ProseMirror does *not* follow JSHint or JSLint prescribed style.
Patches that try to 'fix' code to pass one of these linters will not
be accepted.

19
resources/app/node_modules/prosemirror-state/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,709 @@
import { Schema, Node, Mark, MarkType, Slice, ResolvedPos } from 'prosemirror-model';
import { Transform, Mappable } from 'prosemirror-transform';
import { EditorProps, EditorView } from 'prosemirror-view';
/**
The type of object passed to
[`EditorState.create`](https://prosemirror.net/docs/ref/#state.EditorState^create).
*/
interface EditorStateConfig {
/**
The schema to use (only relevant if no `doc` is specified).
*/
schema?: Schema;
/**
The starting document. Either this or `schema` _must_ be
provided.
*/
doc?: Node;
/**
A valid selection in the document.
*/
selection?: Selection;
/**
The initial set of [stored marks](https://prosemirror.net/docs/ref/#state.EditorState.storedMarks).
*/
storedMarks?: readonly Mark[] | null;
/**
The plugins that should be active in this state.
*/
plugins?: readonly Plugin[];
}
/**
The state of a ProseMirror editor is represented by an object of
this type. A state is a persistent data structure—it isn't
updated, but rather a new state value is computed from an old one
using the [`apply`](https://prosemirror.net/docs/ref/#state.EditorState.apply) method.
A state holds a number of built-in fields, and plugins can
[define](https://prosemirror.net/docs/ref/#state.PluginSpec.state) additional fields.
*/
declare class EditorState {
/**
The current document.
*/
doc: Node;
/**
The selection.
*/
selection: Selection;
/**
A set of marks to apply to the next input. Will be null when
no explicit marks have been set.
*/
storedMarks: readonly Mark[] | null;
/**
The schema of the state's document.
*/
get schema(): Schema;
/**
The plugins that are active in this state.
*/
get plugins(): readonly Plugin[];
/**
Apply the given transaction to produce a new state.
*/
apply(tr: Transaction): EditorState;
/**
Verbose variant of [`apply`](https://prosemirror.net/docs/ref/#state.EditorState.apply) that
returns the precise transactions that were applied (which might
be influenced by the [transaction
hooks](https://prosemirror.net/docs/ref/#state.PluginSpec.filterTransaction) of
plugins) along with the new state.
*/
applyTransaction(rootTr: Transaction): {
state: EditorState;
transactions: readonly Transaction[];
};
/**
Start a [transaction](https://prosemirror.net/docs/ref/#state.Transaction) from this state.
*/
get tr(): Transaction;
/**
Create a new state.
*/
static create(config: EditorStateConfig): EditorState;
/**
Create a new state based on this one, but with an adjusted set
of active plugins. State fields that exist in both sets of
plugins are kept unchanged. Those that no longer exist are
dropped, and those that are new are initialized using their
[`init`](https://prosemirror.net/docs/ref/#state.StateField.init) method, passing in the new
configuration object..
*/
reconfigure(config: {
/**
New set of active plugins.
*/
plugins?: readonly Plugin[];
}): EditorState;
/**
Serialize this state to JSON. If you want to serialize the state
of plugins, pass an object mapping property names to use in the
resulting JSON object to plugin objects. The argument may also be
a string or number, in which case it is ignored, to support the
way `JSON.stringify` calls `toString` methods.
*/
toJSON(pluginFields?: {
[propName: string]: Plugin;
}): any;
/**
Deserialize a JSON representation of a state. `config` should
have at least a `schema` field, and should contain array of
plugins to initialize the state with. `pluginFields` can be used
to deserialize the state of plugins, by associating plugin
instances with the property names they use in the JSON object.
*/
static fromJSON(config: {
/**
The schema to use.
*/
schema: Schema;
/**
The set of active plugins.
*/
plugins?: readonly Plugin[];
}, json: any, pluginFields?: {
[propName: string]: Plugin;
}): EditorState;
}
/**
This is the type passed to the [`Plugin`](https://prosemirror.net/docs/ref/#state.Plugin)
constructor. It provides a definition for a plugin.
*/
interface PluginSpec<PluginState> {
/**
The [view props](https://prosemirror.net/docs/ref/#view.EditorProps) added by this plugin. Props
that are functions will be bound to have the plugin instance as
their `this` binding.
*/
props?: EditorProps<Plugin<PluginState>>;
/**
Allows a plugin to define a [state field](https://prosemirror.net/docs/ref/#state.StateField), an
extra slot in the state object in which it can keep its own data.
*/
state?: StateField<PluginState>;
/**
Can be used to make this a keyed plugin. You can have only one
plugin with a given key in a given state, but it is possible to
access the plugin's configuration and state through the key,
without having access to the plugin instance object.
*/
key?: PluginKey;
/**
When the plugin needs to interact with the editor view, or
set something up in the DOM, use this field. The function
will be called when the plugin's state is associated with an
editor view.
*/
view?: (view: EditorView) => PluginView;
/**
When present, this will be called before a transaction is
applied by the state, allowing the plugin to cancel it (by
returning false).
*/
filterTransaction?: (tr: Transaction, state: EditorState) => boolean;
/**
Allows the plugin to append another transaction to be applied
after the given array of transactions. When another plugin
appends a transaction after this was called, it is called again
with the new state and new transactions—but only the new
transactions, i.e. it won't be passed transactions that it
already saw.
*/
appendTransaction?: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => Transaction | null | undefined;
/**
Additional properties are allowed on plugin specs, which can be
read via [`Plugin.spec`](https://prosemirror.net/docs/ref/#state.Plugin.spec).
*/
[key: string]: any;
}
/**
A stateful object that can be installed in an editor by a
[plugin](https://prosemirror.net/docs/ref/#state.PluginSpec.view).
*/
declare type PluginView = {
/**
Called whenever the view's state is updated.
*/
update?: (view: EditorView, prevState: EditorState) => void;
/**
Called when the view is destroyed or receives a state
with different plugins.
*/
destroy?: () => void;
};
/**
Plugins bundle functionality that can be added to an editor.
They are part of the [editor state](https://prosemirror.net/docs/ref/#state.EditorState) and
may influence that state and the view that contains it.
*/
declare class Plugin<PluginState = any> {
/**
The plugin's [spec object](https://prosemirror.net/docs/ref/#state.PluginSpec).
*/
readonly spec: PluginSpec<PluginState>;
/**
Create a plugin.
*/
constructor(
/**
The plugin's [spec object](https://prosemirror.net/docs/ref/#state.PluginSpec).
*/
spec: PluginSpec<PluginState>);
/**
The [props](https://prosemirror.net/docs/ref/#view.EditorProps) exported by this plugin.
*/
readonly props: EditorProps<Plugin<PluginState>>;
/**
Extract the plugin's state field from an editor state.
*/
getState(state: EditorState): PluginState | undefined;
}
/**
A plugin spec may provide a state field (under its
[`state`](https://prosemirror.net/docs/ref/#state.PluginSpec.state) property) of this type, which
describes the state it wants to keep. Functions provided here are
always called with the plugin instance as their `this` binding.
*/
interface StateField<T> {
/**
Initialize the value of the field. `config` will be the object
passed to [`EditorState.create`](https://prosemirror.net/docs/ref/#state.EditorState^create). Note
that `instance` is a half-initialized state instance, and will
not have values for plugin fields initialized after this one.
*/
init: (config: EditorStateConfig, instance: EditorState) => T;
/**
Apply the given transaction to this state field, producing a new
field value. Note that the `newState` argument is again a partially
constructed state does not yet contain the state from plugins
coming after this one.
*/
apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T;
/**
Convert this field to JSON. Optional, can be left off to disable
JSON serialization for the field.
*/
toJSON?: (value: T) => any;
/**
Deserialize the JSON representation of this field. Note that the
`state` argument is again a half-initialized state.
*/
fromJSON?: (config: EditorStateConfig, value: any, state: EditorState) => T;
}
/**
A key is used to [tag](https://prosemirror.net/docs/ref/#state.PluginSpec.key) plugins in a way
that makes it possible to find them, given an editor state.
Assigning a key does mean only one plugin of that type can be
active in a state.
*/
declare class PluginKey<PluginState = any> {
/**
Create a plugin key.
*/
constructor(name?: string);
/**
Get the active plugin with this key, if any, from an editor
state.
*/
get(state: EditorState): Plugin<PluginState> | undefined;
/**
Get the plugin's state from an editor state.
*/
getState(state: EditorState): PluginState | undefined;
}
/**
Commands are functions that take a state and a an optional
transaction dispatch function and...
- determine whether they apply to this state
- if not, return false
- if `dispatch` was passed, perform their effect, possibly by
passing a transaction to `dispatch`
- return true
In some cases, the editor view is passed as a third argument.
*/
declare type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean;
/**
An editor state transaction, which can be applied to a state to
create an updated state. Use
[`EditorState.tr`](https://prosemirror.net/docs/ref/#state.EditorState.tr) to create an instance.
Transactions track changes to the document (they are a subclass of
[`Transform`](https://prosemirror.net/docs/ref/#transform.Transform)), but also other state changes,
like selection updates and adjustments of the set of [stored
marks](https://prosemirror.net/docs/ref/#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
transaction represents, so that they can update their [own
state](https://prosemirror.net/docs/ref/#state.StateField) accordingly.
The [editor view](https://prosemirror.net/docs/ref/#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, a `"composition"` property holding an ID identifying the
composition that caused it to transactions caused by composed DOM
input, and a `"uiEvent"` property of that may be `"paste"`,
`"cut"`, or `"drop"`.
*/
declare class Transaction extends Transform {
/**
The timestamp associated with this transaction, in the same
format as `Date.now()`.
*/
time: number;
private curSelection;
private curSelectionFor;
private updated;
private meta;
/**
The stored marks set by this transaction, if any.
*/
storedMarks: readonly Mark[] | null;
/**
The transaction's current selection. This defaults to the editor
selection [mapped](https://prosemirror.net/docs/ref/#state.Selection.map) through the steps in the
transaction, but can be overwritten with
[`setSelection`](https://prosemirror.net/docs/ref/#state.Transaction.setSelection).
*/
get selection(): Selection;
/**
Update the transaction's current selection. Will determine the
selection that the editor gets when the transaction is applied.
*/
setSelection(selection: Selection): this;
/**
Whether the selection was explicitly updated by this transaction.
*/
get selectionSet(): boolean;
/**
Set the current stored marks.
*/
setStoredMarks(marks: readonly Mark[] | null): this;
/**
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: readonly Mark[]): this;
/**
Add a mark to the set of stored marks.
*/
addStoredMark(mark: Mark): this;
/**
Remove a mark or mark type from the set of stored marks.
*/
removeStoredMark(mark: Mark | MarkType): this;
/**
Whether the stored marks were explicitly set for this transaction.
*/
get storedMarksSet(): boolean;
/**
Update the timestamp for the transaction.
*/
setTime(time: number): this;
/**
Replace the current selection with the given slice.
*/
replaceSelection(slice: Slice): this;
/**
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: Node, inheritMarks?: boolean): this;
/**
Delete the selection.
*/
deleteSelection(): this;
/**
Replace the given range, or the selection if no range is given,
with a text node containing the given string.
*/
insertText(text: string, from?: number, to?: number): this;
/**
Store a metadata property in this transaction, keyed either by
name or by plugin.
*/
setMeta(key: string | Plugin | PluginKey, value: any): this;
/**
Retrieve a metadata property for a given name or plugin.
*/
getMeta(key: string | Plugin | PluginKey): any;
/**
Returns true if this transaction doesn't contain any metadata,
and can thus safely be extended.
*/
get isGeneric(): boolean;
/**
Indicate that the editor should scroll the selection into view
when updated to the state produced by this transaction.
*/
scrollIntoView(): this;
/**
True when this transaction has had `scrollIntoView` called on it.
*/
get scrolledIntoView(): boolean;
}
/**
Superclass for editor selections. Every selection type should
extend this. Should not be instantiated directly.
*/
declare abstract class Selection {
/**
The resolved anchor of the selection (the side that stays in
place when the selection is modified).
*/
readonly $anchor: ResolvedPos;
/**
The resolved head of the selection (the side that moves when
the selection is modified).
*/
readonly $head: ResolvedPos;
/**
Initialize a selection with the head and anchor and ranges. If no
ranges are given, constructs a single range across `$anchor` and
`$head`.
*/
constructor(
/**
The resolved anchor of the selection (the side that stays in
place when the selection is modified).
*/
$anchor: ResolvedPos,
/**
The resolved head of the selection (the side that moves when
the selection is modified).
*/
$head: ResolvedPos, ranges?: readonly SelectionRange[]);
/**
The ranges covered by the selection.
*/
ranges: readonly SelectionRange[];
/**
The selection's anchor, as an unresolved position.
*/
get anchor(): number;
/**
The selection's head.
*/
get head(): number;
/**
The lower bound of the selection's main range.
*/
get from(): number;
/**
The upper bound of the selection's main range.
*/
get to(): number;
/**
The resolved lower bound of the selection's main range.
*/
get $from(): ResolvedPos;
/**
The resolved upper bound of the selection's main range.
*/
get $to(): ResolvedPos;
/**
Indicates whether the selection contains any content.
*/
get empty(): boolean;
/**
Test whether the selection is the same as another selection.
*/
abstract eq(selection: Selection): boolean;
/**
Map this selection through a [mappable](https://prosemirror.net/docs/ref/#transform.Mappable)
thing. `doc` should be the new document to which we are mapping.
*/
abstract map(doc: Node, mapping: Mappable): Selection;
/**
Get the content of this selection as a slice.
*/
content(): Slice;
/**
Replace the selection with a slice or, if no slice is given,
delete the selection. Will append to the given transaction.
*/
replace(tr: Transaction, content?: Slice): void;
/**
Replace the selection with the given node, appending the changes
to the given transaction.
*/
replaceWith(tr: Transaction, node: Node): void;
/**
Convert the selection to a JSON representation. When implementing
this for a custom selection class, make sure to give the object a
`type` property whose value matches the ID under which you
[registered](https://prosemirror.net/docs/ref/#state.Selection^jsonID) your class.
*/
abstract toJSON(): any;
/**
Find a valid cursor or leaf node selection starting at the given
position and searching back if `dir` is negative, and forward if
positive. When `textOnly` is true, only consider cursor
selections. Will return null when no valid selection position is
found.
*/
static findFrom($pos: ResolvedPos, dir: number, textOnly?: boolean): Selection | null;
/**
Find a valid cursor or leaf node selection near the given
position. Searches forward first by default, but if `bias` is
negative, it will search backwards first.
*/
static near($pos: ResolvedPos, bias?: number): Selection;
/**
Find the cursor or leaf node selection closest to the start of
the given document. Will return an
[`AllSelection`](https://prosemirror.net/docs/ref/#state.AllSelection) if no valid position
exists.
*/
static atStart(doc: Node): Selection;
/**
Find the cursor or leaf node selection closest to the end of the
given document.
*/
static atEnd(doc: Node): Selection;
/**
Deserialize the JSON representation of a selection. Must be
implemented for custom classes (as a static class method).
*/
static fromJSON(doc: Node, json: any): Selection;
/**
To be able to deserialize selections from JSON, custom selection
classes must register themselves with an ID string, so that they
can be disambiguated. Try to pick something that's unlikely to
clash with classes from other modules.
*/
static jsonID(id: string, selectionClass: {
fromJSON: (doc: Node, json: any) => Selection;
}): {
fromJSON: (doc: Node, json: any) => Selection;
};
/**
Get a [bookmark](https://prosemirror.net/docs/ref/#state.SelectionBookmark) for this selection,
which is a value that can be mapped without having access to a
current document, and later resolved to a real selection for a
given document again. (This is used mostly by the history to
track and restore old selections.) The default implementation of
this method just converts the selection to a text selection and
returns the bookmark for that.
*/
getBookmark(): SelectionBookmark;
/**
Controls whether, when a selection of this type is active in the
browser, the selected range should be visible to the user.
Defaults to `true`.
*/
visible: boolean;
}
/**
A lightweight, document-independent representation of a selection.
You can define a custom bookmark type for a custom selection class
to make the history handle it well.
*/
interface SelectionBookmark {
/**
Map the bookmark through a set of changes.
*/
map: (mapping: Mappable) => SelectionBookmark;
/**
Resolve the bookmark to a real selection again. This may need to
do some error checking and may fall back to a default (usually
[`TextSelection.between`](https://prosemirror.net/docs/ref/#state.TextSelection^between)) if
mapping made the bookmark invalid.
*/
resolve: (doc: Node) => Selection;
}
/**
Represents a selected range in a document.
*/
declare class SelectionRange {
/**
The lower bound of the range.
*/
readonly $from: ResolvedPos;
/**
The upper bound of the range.
*/
readonly $to: ResolvedPos;
/**
Create a range.
*/
constructor(
/**
The lower bound of the range.
*/
$from: ResolvedPos,
/**
The upper bound of the range.
*/
$to: ResolvedPos);
}
/**
A text selection represents a classical editor selection, with a
head (the moving side) and anchor (immobile side), both of which
point into textblock nodes. It can be empty (a regular cursor
position).
*/
declare class TextSelection extends Selection {
/**
Construct a text selection between the given points.
*/
constructor($anchor: ResolvedPos, $head?: ResolvedPos);
/**
Returns a resolved position if this is a cursor selection (an
empty text selection), and null otherwise.
*/
get $cursor(): ResolvedPos | null;
map(doc: Node, mapping: Mappable): Selection;
replace(tr: Transaction, content?: Slice): void;
eq(other: Selection): boolean;
getBookmark(): TextBookmark;
toJSON(): any;
/**
Create a text selection from non-resolved positions.
*/
static create(doc: Node, anchor: number, head?: number): TextSelection;
/**
Return a text selection that spans the given positions or, if
they aren't text positions, find a text selection near them.
`bias` determines whether the method searches forward (default)
or backwards (negative number) first. Will fall back to calling
[`Selection.near`](https://prosemirror.net/docs/ref/#state.Selection^near) when the document
doesn't contain a valid text position.
*/
static between($anchor: ResolvedPos, $head: ResolvedPos, bias?: number): Selection;
}
declare class TextBookmark {
readonly anchor: number;
readonly head: number;
constructor(anchor: number, head: number);
map(mapping: Mappable): TextBookmark;
resolve(doc: Node): Selection;
}
/**
A node selection is a selection that points at a single node. All
nodes marked [selectable](https://prosemirror.net/docs/ref/#model.NodeSpec.selectable) can be the
target of a node selection. In such a selection, `from` and `to`
point directly before and after the selected node, `anchor` equals
`from`, and `head` equals `to`..
*/
declare class NodeSelection extends Selection {
/**
Create a node selection. Does not verify the validity of its
argument.
*/
constructor($pos: ResolvedPos);
/**
The selected node.
*/
node: Node;
map(doc: Node, mapping: Mappable): Selection;
content(): Slice;
eq(other: Selection): boolean;
toJSON(): any;
getBookmark(): NodeBookmark;
/**
Create a node selection from non-resolved positions.
*/
static create(doc: Node, from: number): NodeSelection;
/**
Determines whether the given node may be selected as a node
selection.
*/
static isSelectable(node: Node): boolean;
}
declare class NodeBookmark {
readonly anchor: number;
constructor(anchor: number);
map(mapping: Mappable): TextBookmark | NodeBookmark;
resolve(doc: Node): Selection | NodeSelection;
}
/**
A selection type that represents selecting the whole document
(which can not necessarily be expressed with a text selection, when
there are for example leaf block nodes at the start or end of the
document).
*/
declare class AllSelection extends Selection {
/**
Create an all-selection over the given document.
*/
constructor(doc: Node);
replace(tr: Transaction, content?: Slice): void;
toJSON(): any;
map(doc: Node): AllSelection;
eq(other: Selection): boolean;
getBookmark(): {
map(): any;
resolve(doc: Node): AllSelection;
};
}
export { AllSelection, Command, EditorState, EditorStateConfig, NodeSelection, Plugin, PluginKey, PluginSpec, PluginView, Selection, SelectionBookmark, SelectionRange, StateField, TextSelection, Transaction };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"name": "prosemirror-state",
"version": "1.4.3",
"description": "ProseMirror editor state",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"sideEffects": false,
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijn@haverbeke.berlin",
"web": "http://marijnhaverbeke.nl"
}
],
"repository": {
"type": "git",
"url": "git://github.com/prosemirror/prosemirror-state.git"
},
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
},
"devDependencies": {
"@prosemirror/buildhelper": "^0.1.5",
"prosemirror-test-builder": "^1.0.0"
}
}

View File

@@ -0,0 +1,42 @@
This module implements the state object of a ProseMirror editor, along
with the representation of the selection and the plugin abstraction.
### Editor State
ProseMirror keeps all editor state (the things, basically, that would
be required to create an editor just like the current one) in a single
[object](#state.EditorState). That object is updated (creating a new
state) by applying [transactions](#state.Transaction) to it.
@EditorState
@EditorStateConfig
@Transaction
@Command
### Selection
A ProseMirror selection can be one of several types. This module
defines types for classical [text selections](#state.TextSelection)
(of which cursors are a special case) and [_node_
selections](#state.NodeSelection), where a specific document node is
selected. It is possible to extend the editor with custom selection
types.
@Selection
@TextSelection
@NodeSelection
@AllSelection
@SelectionRange
@SelectionBookmark
### Plugin System
To make it easy to package and enable extra editor functionality,
ProseMirror has a plugin system.
@PluginSpec
@StateField
@PluginView
@Plugin
@PluginKey

View File

@@ -0,0 +1,7 @@
export {Selection, SelectionRange, TextSelection, NodeSelection, AllSelection, SelectionBookmark} from "./selection"
export {Transaction, Command} from "./transaction"
export {EditorState, EditorStateConfig} from "./state"
export {Plugin, PluginKey, PluginSpec, StateField, PluginView} from "./plugin"

View File

@@ -0,0 +1,142 @@
import {type EditorView, type EditorProps} from "prosemirror-view"
import {EditorState, EditorStateConfig} from "./state"
import {Transaction} from "./transaction"
/// This is the type passed to the [`Plugin`](#state.Plugin)
/// constructor. It provides a definition for a plugin.
export interface PluginSpec<PluginState> {
/// The [view props](#view.EditorProps) added by this plugin. Props
/// that are functions will be bound to have the plugin instance as
/// their `this` binding.
props?: EditorProps<Plugin<PluginState>>
/// Allows a plugin to define a [state field](#state.StateField), an
/// extra slot in the state object in which it can keep its own data.
state?: StateField<PluginState>
/// Can be used to make this a keyed plugin. You can have only one
/// plugin with a given key in a given state, but it is possible to
/// access the plugin's configuration and state through the key,
/// without having access to the plugin instance object.
key?: PluginKey
/// When the plugin needs to interact with the editor view, or
/// set something up in the DOM, use this field. The function
/// will be called when the plugin's state is associated with an
/// editor view.
view?: (view: EditorView) => PluginView
/// When present, this will be called before a transaction is
/// applied by the state, allowing the plugin to cancel it (by
/// returning false).
filterTransaction?: (tr: Transaction, state: EditorState) => boolean
/// Allows the plugin to append another transaction to be applied
/// after the given array of transactions. When another plugin
/// appends a transaction after this was called, it is called again
/// with the new state and new transactions—but only the new
/// transactions, i.e. it won't be passed transactions that it
/// already saw.
appendTransaction?: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => Transaction | null | undefined
/// Additional properties are allowed on plugin specs, which can be
/// read via [`Plugin.spec`](#state.Plugin.spec).
[key: string]: any
}
/// A stateful object that can be installed in an editor by a
/// [plugin](#state.PluginSpec.view).
export type PluginView = {
/// Called whenever the view's state is updated.
update?: (view: EditorView, prevState: EditorState) => void
/// Called when the view is destroyed or receives a state
/// with different plugins.
destroy?: () => void
}
function bindProps(obj: {[prop: string]: any}, self: any, target: {[prop: string]: any}) {
for (let prop in obj) {
let val = obj[prop]
if (val instanceof Function) val = val.bind(self)
else if (prop == "handleDOMEvents") val = bindProps(val, self, {})
target[prop] = val
}
return target
}
/// Plugins bundle functionality that can be added to an editor.
/// They are part of the [editor state](#state.EditorState) and
/// may influence that state and the view that contains it.
export class Plugin<PluginState = any> {
/// Create a plugin.
constructor(
/// The plugin's [spec object](#state.PluginSpec).
readonly spec: PluginSpec<PluginState>
) {
if (spec.props) bindProps(spec.props, this, this.props)
this.key = spec.key ? spec.key.key : createKey("plugin")
}
/// The [props](#view.EditorProps) exported by this plugin.
readonly props: EditorProps<Plugin<PluginState>> = {}
/// @internal
key: string
/// Extract the plugin's state field from an editor state.
getState(state: EditorState): PluginState | undefined { return (state as any)[this.key] }
}
/// A plugin spec may provide a state field (under its
/// [`state`](#state.PluginSpec.state) property) of this type, which
/// describes the state it wants to keep. Functions provided here are
/// always called with the plugin instance as their `this` binding.
export interface StateField<T> {
/// Initialize the value of the field. `config` will be the object
/// passed to [`EditorState.create`](#state.EditorState^create). Note
/// that `instance` is a half-initialized state instance, and will
/// not have values for plugin fields initialized after this one.
init: (config: EditorStateConfig, instance: EditorState) => T
/// Apply the given transaction to this state field, producing a new
/// field value. Note that the `newState` argument is again a partially
/// constructed state does not yet contain the state from plugins
/// coming after this one.
apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T
/// Convert this field to JSON. Optional, can be left off to disable
/// JSON serialization for the field.
toJSON?: (value: T) => any
/// Deserialize the JSON representation of this field. Note that the
/// `state` argument is again a half-initialized state.
fromJSON?: (config: EditorStateConfig, value: any, state: EditorState) => T
}
const keys = Object.create(null)
function createKey(name: string) {
if (name in keys) return name + "$" + ++keys[name]
keys[name] = 0
return name + "$"
}
/// A key is used to [tag](#state.PluginSpec.key) plugins in a way
/// that makes it possible to find them, given an editor state.
/// Assigning a key does mean only one plugin of that type can be
/// active in a state.
export class PluginKey<PluginState = any> {
/// @internal
key: string
/// Create a plugin key.
constructor(name = "key") { this.key = createKey(name) }
/// Get the active plugin with this key, if any, from an editor
/// state.
get(state: EditorState): Plugin<PluginState> | undefined { return state.config.pluginsByKey[this.key] }
/// Get the plugin's state from an editor state.
getState(state: EditorState): PluginState | undefined { return (state as any)[this.key] }
}

View File

@@ -0,0 +1,462 @@
import {Slice, Fragment, ResolvedPos, Node} from "prosemirror-model"
import {ReplaceStep, ReplaceAroundStep, Mappable} from "prosemirror-transform"
import {Transaction} from "./transaction"
const classesById = Object.create(null)
/// Superclass for editor selections. Every selection type should
/// extend this. Should not be instantiated directly.
export abstract class Selection {
/// Initialize a selection with the head and anchor and ranges. If no
/// ranges are given, constructs a single range across `$anchor` and
/// `$head`.
constructor(
/// The resolved anchor of the selection (the side that stays in
/// place when the selection is modified).
readonly $anchor: ResolvedPos,
/// The resolved head of the selection (the side that moves when
/// the selection is modified).
readonly $head: ResolvedPos,
ranges?: readonly SelectionRange[]
) {
this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))]
}
/// The ranges covered by the selection.
ranges: readonly SelectionRange[]
/// The selection's anchor, as an unresolved position.
get anchor() { return this.$anchor.pos }
/// The selection's head.
get head() { return this.$head.pos }
/// The lower bound of the selection's main range.
get from() { return this.$from.pos }
/// The upper bound of the selection's main range.
get to() { return this.$to.pos }
/// The resolved lower bound of the selection's main range.
get $from() {
return this.ranges[0].$from
}
/// The resolved upper bound of the selection's main range.
get $to() {
return this.ranges[0].$to
}
/// Indicates whether the selection contains any content.
get empty(): boolean {
let ranges = this.ranges
for (let i = 0; i < ranges.length; i++)
if (ranges[i].$from.pos != ranges[i].$to.pos) return false
return true
}
/// Test whether the selection is the same as another selection.
abstract eq(selection: Selection): boolean
/// Map this selection through a [mappable](#transform.Mappable)
/// thing. `doc` should be the new document to which we are mapping.
abstract map(doc: Node, mapping: Mappable): Selection
/// Get the content of this selection as a slice.
content() {
return this.$from.doc.slice(this.from, this.to, true)
}
/// Replace the selection with a slice or, if no slice is given,
/// delete the selection. Will append to the given transaction.
replace(tr: Transaction, content = Slice.empty) {
// Put the new selection at the position after the inserted
// content. When that ended in an inline node, search backwards,
// to get the position after that node. If not, search forward.
let lastNode = content.content.lastChild, lastParent = null
for (let i = 0; i < content.openEnd; i++) {
lastParent = lastNode!
lastNode = lastNode!.lastChild
}
let mapFrom = tr.steps.length, ranges = this.ranges
for (let i = 0; i < ranges.length; i++) {
let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom)
tr.replaceRange(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content)
if (i == 0)
selectionToInsertionEnd(tr, mapFrom, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1)
}
}
/// Replace the selection with the given node, appending the changes
/// to the given transaction.
replaceWith(tr: Transaction, node: Node) {
let mapFrom = tr.steps.length, ranges = this.ranges
for (let i = 0; i < ranges.length; i++) {
let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom)
let from = mapping.map($from.pos), to = mapping.map($to.pos)
if (i) {
tr.deleteRange(from, to)
} else {
tr.replaceRangeWith(from, to, node)
selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1)
}
}
}
/// Convert the selection to a JSON representation. When implementing
/// this for a custom selection class, make sure to give the object a
/// `type` property whose value matches the ID under which you
/// [registered](#state.Selection^jsonID) your class.
abstract toJSON(): any
/// Find a valid cursor or leaf node selection starting at the given
/// position and searching back if `dir` is negative, and forward if
/// positive. When `textOnly` is true, only consider cursor
/// selections. Will return null when no valid selection position is
/// found.
static findFrom($pos: ResolvedPos, dir: number, textOnly: boolean = false): Selection | null {
let inner = $pos.parent.inlineContent ? new TextSelection($pos)
: findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly)
if (inner) return inner
for (let depth = $pos.depth - 1; depth >= 0; depth--) {
let found = dir < 0
? findSelectionIn($pos.node(0), $pos.node(depth), $pos.before(depth + 1), $pos.index(depth), dir, textOnly)
: findSelectionIn($pos.node(0), $pos.node(depth), $pos.after(depth + 1), $pos.index(depth) + 1, dir, textOnly)
if (found) return found
}
return null
}
/// Find a valid cursor or leaf node selection near the given
/// position. Searches forward first by default, but if `bias` is
/// negative, it will search backwards first.
static near($pos: ResolvedPos, bias = 1): Selection {
return this.findFrom($pos, bias) || this.findFrom($pos, -bias) || new AllSelection($pos.node(0))
}
/// Find the cursor or leaf node selection closest to the start of
/// the given document. Will return an
/// [`AllSelection`](#state.AllSelection) if no valid position
/// exists.
static atStart(doc: Node): Selection {
return findSelectionIn(doc, doc, 0, 0, 1) || new AllSelection(doc)
}
/// Find the cursor or leaf node selection closest to the end of the
/// given document.
static atEnd(doc: Node): Selection {
return findSelectionIn(doc, doc, doc.content.size, doc.childCount, -1) || new AllSelection(doc)
}
/// Deserialize the JSON representation of a selection. Must be
/// implemented for custom classes (as a static class method).
static fromJSON(doc: Node, json: any): Selection {
if (!json || !json.type) throw new RangeError("Invalid input for Selection.fromJSON")
let cls = classesById[json.type]
if (!cls) throw new RangeError(`No selection type ${json.type} defined`)
return cls.fromJSON(doc, json)
}
/// To be able to deserialize selections from JSON, custom selection
/// classes must register themselves with an ID string, so that they
/// can be disambiguated. Try to pick something that's unlikely to
/// clash with classes from other modules.
static jsonID(id: string, selectionClass: {fromJSON: (doc: Node, json: any) => Selection}) {
if (id in classesById) throw new RangeError("Duplicate use of selection JSON ID " + id)
classesById[id] = selectionClass
;(selectionClass as any).prototype.jsonID = id
return selectionClass
}
/// Get a [bookmark](#state.SelectionBookmark) for this selection,
/// which is a value that can be mapped without having access to a
/// current document, and later resolved to a real selection for a
/// given document again. (This is used mostly by the history to
/// track and restore old selections.) The default implementation of
/// this method just converts the selection to a text selection and
/// returns the bookmark for that.
getBookmark(): SelectionBookmark {
return TextSelection.between(this.$anchor, this.$head).getBookmark()
}
/// Controls whether, when a selection of this type is active in the
/// browser, the selected range should be visible to the user.
/// Defaults to `true`.
visible!: boolean
}
Selection.prototype.visible = true
/// A lightweight, document-independent representation of a selection.
/// You can define a custom bookmark type for a custom selection class
/// to make the history handle it well.
export interface SelectionBookmark {
/// Map the bookmark through a set of changes.
map: (mapping: Mappable) => SelectionBookmark
/// Resolve the bookmark to a real selection again. This may need to
/// do some error checking and may fall back to a default (usually
/// [`TextSelection.between`](#state.TextSelection^between)) if
/// mapping made the bookmark invalid.
resolve: (doc: Node) => Selection
}
/// Represents a selected range in a document.
export class SelectionRange {
/// Create a range.
constructor(
/// The lower bound of the range.
readonly $from: ResolvedPos,
/// The upper bound of the range.
readonly $to: ResolvedPos
) {}
}
let warnedAboutTextSelection = false
function checkTextSelection($pos: ResolvedPos) {
if (!warnedAboutTextSelection && !$pos.parent.inlineContent) {
warnedAboutTextSelection = true
console["warn"]("TextSelection endpoint not pointing into a node with inline content (" + $pos.parent.type.name + ")")
}
}
/// A text selection represents a classical editor selection, with a
/// head (the moving side) and anchor (immobile side), both of which
/// point into textblock nodes. It can be empty (a regular cursor
/// position).
export class TextSelection extends Selection {
/// Construct a text selection between the given points.
constructor($anchor: ResolvedPos, $head = $anchor) {
checkTextSelection($anchor)
checkTextSelection($head)
super($anchor, $head)
}
/// Returns a resolved position if this is a cursor selection (an
/// empty text selection), and null otherwise.
get $cursor() { return this.$anchor.pos == this.$head.pos ? this.$head : null }
map(doc: Node, mapping: Mappable): Selection {
let $head = doc.resolve(mapping.map(this.head))
if (!$head.parent.inlineContent) return Selection.near($head)
let $anchor = doc.resolve(mapping.map(this.anchor))
return new TextSelection($anchor.parent.inlineContent ? $anchor : $head, $head)
}
replace(tr: Transaction, content = Slice.empty) {
super.replace(tr, content)
if (content == Slice.empty) {
let marks = this.$from.marksAcross(this.$to)
if (marks) tr.ensureMarks(marks)
}
}
eq(other: Selection): boolean {
return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head
}
getBookmark() {
return new TextBookmark(this.anchor, this.head)
}
toJSON(): any {
return {type: "text", anchor: this.anchor, head: this.head}
}
/// @internal
static fromJSON(doc: Node, json: any) {
if (typeof json.anchor != "number" || typeof json.head != "number")
throw new RangeError("Invalid input for TextSelection.fromJSON")
return new TextSelection(doc.resolve(json.anchor), doc.resolve(json.head))
}
/// Create a text selection from non-resolved positions.
static create(doc: Node, anchor: number, head = anchor) {
let $anchor = doc.resolve(anchor)
return new this($anchor, head == anchor ? $anchor : doc.resolve(head))
}
/// Return a text selection that spans the given positions or, if
/// they aren't text positions, find a text selection near them.
/// `bias` determines whether the method searches forward (default)
/// or backwards (negative number) first. Will fall back to calling
/// [`Selection.near`](#state.Selection^near) when the document
/// doesn't contain a valid text position.
static between($anchor: ResolvedPos, $head: ResolvedPos, bias?: number): Selection {
let dPos = $anchor.pos - $head.pos
if (!bias || dPos) bias = dPos >= 0 ? 1 : -1
if (!$head.parent.inlineContent) {
let found = Selection.findFrom($head, bias, true) || Selection.findFrom($head, -bias, true)
if (found) $head = found.$head
else return Selection.near($head, bias)
}
if (!$anchor.parent.inlineContent) {
if (dPos == 0) {
$anchor = $head
} else {
$anchor = (Selection.findFrom($anchor, -bias, true) || Selection.findFrom($anchor, bias, true))!.$anchor
if (($anchor.pos < $head.pos) != (dPos < 0)) $anchor = $head
}
}
return new TextSelection($anchor, $head)
}
}
Selection.jsonID("text", TextSelection)
class TextBookmark {
constructor(readonly anchor: number, readonly head: number) {}
map(mapping: Mappable) {
return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head))
}
resolve(doc: Node) {
return TextSelection.between(doc.resolve(this.anchor), doc.resolve(this.head))
}
}
/// A node selection is a selection that points at a single node. All
/// nodes marked [selectable](#model.NodeSpec.selectable) can be the
/// target of a node selection. In such a selection, `from` and `to`
/// point directly before and after the selected node, `anchor` equals
/// `from`, and `head` equals `to`..
export class NodeSelection extends Selection {
/// Create a node selection. Does not verify the validity of its
/// argument.
constructor($pos: ResolvedPos) {
let node = $pos.nodeAfter!
let $end = $pos.node(0).resolve($pos.pos + node.nodeSize)
super($pos, $end)
this.node = node
}
/// The selected node.
node: Node
map(doc: Node, mapping: Mappable): Selection {
let {deleted, pos} = mapping.mapResult(this.anchor)
let $pos = doc.resolve(pos)
if (deleted) return Selection.near($pos)
return new NodeSelection($pos)
}
content() {
return new Slice(Fragment.from(this.node), 0, 0)
}
eq(other: Selection): boolean {
return other instanceof NodeSelection && other.anchor == this.anchor
}
toJSON(): any {
return {type: "node", anchor: this.anchor}
}
getBookmark() { return new NodeBookmark(this.anchor) }
/// @internal
static fromJSON(doc: Node, json: any) {
if (typeof json.anchor != "number")
throw new RangeError("Invalid input for NodeSelection.fromJSON")
return new NodeSelection(doc.resolve(json.anchor))
}
/// Create a node selection from non-resolved positions.
static create(doc: Node, from: number) {
return new NodeSelection(doc.resolve(from))
}
/// Determines whether the given node may be selected as a node
/// selection.
static isSelectable(node: Node) {
return !node.isText && node.type.spec.selectable !== false
}
}
NodeSelection.prototype.visible = false
Selection.jsonID("node", NodeSelection)
class NodeBookmark {
constructor(readonly anchor: number) {}
map(mapping: Mappable) {
let {deleted, pos} = mapping.mapResult(this.anchor)
return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos)
}
resolve(doc: Node) {
let $pos = doc.resolve(this.anchor), node = $pos.nodeAfter
if (node && NodeSelection.isSelectable(node)) return new NodeSelection($pos)
return Selection.near($pos)
}
}
/// A selection type that represents selecting the whole document
/// (which can not necessarily be expressed with a text selection, when
/// there are for example leaf block nodes at the start or end of the
/// document).
export class AllSelection extends Selection {
/// Create an all-selection over the given document.
constructor(doc: Node) {
super(doc.resolve(0), doc.resolve(doc.content.size))
}
replace(tr: Transaction, content = Slice.empty) {
if (content == Slice.empty) {
tr.delete(0, tr.doc.content.size)
let sel = Selection.atStart(tr.doc)
if (!sel.eq(tr.selection)) tr.setSelection(sel)
} else {
super.replace(tr, content)
}
}
toJSON(): any { return {type: "all"} }
/// @internal
static fromJSON(doc: Node) { return new AllSelection(doc) }
map(doc: Node) { return new AllSelection(doc) }
eq(other: Selection) { return other instanceof AllSelection }
getBookmark() { return AllBookmark }
}
Selection.jsonID("all", AllSelection)
const AllBookmark = {
map() { return this },
resolve(doc: Node) { return new AllSelection(doc) }
}
// FIXME we'll need some awareness of text direction when scanning for selections
// Try to find a selection inside the given node. `pos` points at the
// position where the search starts. When `text` is true, only return
// text selections.
function findSelectionIn(doc: Node, node: Node, pos: number, index: number, dir: number, text = false): Selection | null {
if (node.inlineContent) return TextSelection.create(doc, pos)
for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) {
let child = node.child(i)
if (!child.isAtom) {
let inner = findSelectionIn(doc, child, pos + dir, dir < 0 ? child.childCount : 0, dir, text)
if (inner) return inner
} else if (!text && NodeSelection.isSelectable(child)) {
return NodeSelection.create(doc, pos - (dir < 0 ? child.nodeSize : 0))
}
pos += child.nodeSize * dir
}
return null
}
function selectionToInsertionEnd(tr: Transaction, startLen: number, bias: number) {
let last = tr.steps.length - 1
if (last < startLen) return
let step = tr.steps[last]
if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) return
let map = tr.mapping.maps[last], end: number | undefined
map.forEach((_from, _to, _newFrom, newTo) => { if (end == null) end = newTo })
tr.setSelection(Selection.near(tr.doc.resolve(end!), bias))
}

View File

@@ -0,0 +1,266 @@
import {Node, Mark, Schema} from "prosemirror-model"
import {Selection, TextSelection} from "./selection"
import {Transaction} from "./transaction"
import {Plugin, StateField} from "./plugin"
function bind<T extends Function>(f: T, self: any): T {
return !self || !f ? f : f.bind(self)
}
class FieldDesc<T> {
init: (config: EditorStateConfig, instance: EditorState) => T
apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T
constructor(readonly name: string, desc: StateField<any>, self?: any) {
this.init = bind(desc.init, self)
this.apply = bind(desc.apply, self)
}
}
const baseFields = [
new FieldDesc<Node>("doc", {
init(config) { return config.doc || config.schema!.topNodeType.createAndFill() },
apply(tr) { return tr.doc }
}),
new FieldDesc<Selection>("selection", {
init(config, instance) { return config.selection || Selection.atStart(instance.doc) },
apply(tr) { return tr.selection }
}),
new FieldDesc<readonly Mark[] | null>("storedMarks", {
init(config) { return config.storedMarks || null },
apply(tr, _marks, _old, state) { return (state.selection as TextSelection).$cursor ? tr.storedMarks : null }
}),
new FieldDesc<number>("scrollToSelection", {
init() { return 0 },
apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev }
})
]
// Object wrapping the part of a state object that stays the same
// across transactions. Stored in the state's `config` property.
class Configuration {
fields: FieldDesc<any>[]
plugins: Plugin[] = []
pluginsByKey: {[key: string]: Plugin} = Object.create(null)
constructor(readonly schema: Schema, plugins?: readonly Plugin[]) {
this.fields = baseFields.slice()
if (plugins) plugins.forEach(plugin => {
if (this.pluginsByKey[plugin.key])
throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")")
this.plugins.push(plugin)
this.pluginsByKey[plugin.key] = plugin
if (plugin.spec.state)
this.fields.push(new FieldDesc<any>(plugin.key, plugin.spec.state, plugin))
})
}
}
/// The type of object passed to
/// [`EditorState.create`](#state.EditorState^create).
export interface EditorStateConfig {
/// The schema to use (only relevant if no `doc` is specified).
schema?: Schema
/// The starting document. Either this or `schema` _must_ be
/// provided.
doc?: Node
/// A valid selection in the document.
selection?: Selection
/// The initial set of [stored marks](#state.EditorState.storedMarks).
storedMarks?: readonly Mark[] | null
/// The plugins that should be active in this state.
plugins?: readonly Plugin[]
}
/// The state of a ProseMirror editor is represented by an object of
/// this type. A state is a persistent data structure—it isn't
/// updated, but rather a new state value is computed from an old one
/// using the [`apply`](#state.EditorState.apply) method.
///
/// A state holds a number of built-in fields, and plugins can
/// [define](#state.PluginSpec.state) additional fields.
export class EditorState {
/// @internal
constructor(
/// @internal
readonly config: Configuration
) {}
/// The current document.
doc!: Node
/// The selection.
selection!: Selection
/// A set of marks to apply to the next input. Will be null when
/// no explicit marks have been set.
storedMarks!: readonly Mark[] | null
/// The schema of the state's document.
get schema(): Schema {
return this.config.schema
}
/// The plugins that are active in this state.
get plugins(): readonly Plugin[] {
return this.config.plugins
}
/// Apply the given transaction to produce a new state.
apply(tr: Transaction): EditorState {
return this.applyTransaction(tr).state
}
/// @internal
filterTransaction(tr: Transaction, ignore = -1) {
for (let i = 0; i < this.config.plugins.length; i++) if (i != ignore) {
let plugin = this.config.plugins[i]
if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this))
return false
}
return true
}
/// Verbose variant of [`apply`](#state.EditorState.apply) that
/// returns the precise transactions that were applied (which might
/// be influenced by the [transaction
/// hooks](#state.PluginSpec.filterTransaction) of
/// plugins) along with the new state.
applyTransaction(rootTr: Transaction): {state: EditorState, transactions: readonly Transaction[]} {
if (!this.filterTransaction(rootTr)) return {state: this, transactions: []}
let trs = [rootTr], newState = this.applyInner(rootTr), seen = null
// This loop repeatedly gives plugins a chance to respond to
// transactions as new transactions are added, making sure to only
// pass the transactions the plugin did not see before.
outer: for (;;) {
let haveNew = false
for (let i = 0; i < this.config.plugins.length; i++) {
let plugin = this.config.plugins[i]
if (plugin.spec.appendTransaction) {
let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this
let tr = n < trs.length &&
plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState)
if (tr && newState.filterTransaction(tr, i)) {
tr.setMeta("appendedTransaction", rootTr)
if (!seen) {
seen = []
for (let j = 0; j < this.config.plugins.length; j++)
seen.push(j < i ? {state: newState, n: trs.length} : {state: this, n: 0})
}
trs.push(tr)
newState = newState.applyInner(tr)
haveNew = true
}
if (seen) seen[i] = {state: newState, n: trs.length}
}
}
if (!haveNew) return {state: newState, transactions: trs}
}
}
/// @internal
applyInner(tr: Transaction) {
if (!tr.before.eq(this.doc)) throw new RangeError("Applying a mismatched transaction")
let newInstance = new EditorState(this.config), fields = this.config.fields
for (let i = 0; i < fields.length; i++) {
let field = fields[i]
;(newInstance as any)[field.name] = field.apply(tr, (this as any)[field.name], this, newInstance)
}
return newInstance
}
/// Start a [transaction](#state.Transaction) from this state.
get tr(): Transaction { return new Transaction(this) }
/// Create a new state.
static create(config: EditorStateConfig) {
let $config = new Configuration(config.doc ? config.doc.type.schema : config.schema!, config.plugins)
let instance = new EditorState($config)
for (let i = 0; i < $config.fields.length; i++)
(instance as any)[$config.fields[i].name] = $config.fields[i].init(config, instance)
return instance
}
/// Create a new state based on this one, but with an adjusted set
/// of active plugins. State fields that exist in both sets of
/// plugins are kept unchanged. Those that no longer exist are
/// dropped, and those that are new are initialized using their
/// [`init`](#state.StateField.init) method, passing in the new
/// configuration object..
reconfigure(config: {
/// New set of active plugins.
plugins?: readonly Plugin[]
}) {
let $config = new Configuration(this.schema, config.plugins)
let fields = $config.fields, instance = new EditorState($config)
for (let i = 0; i < fields.length; i++) {
let name = fields[i].name
;(instance as any)[name] = this.hasOwnProperty(name) ? (this as any)[name] : fields[i].init(config, instance)
}
return instance
}
/// Serialize this state to JSON. If you want to serialize the state
/// of plugins, pass an object mapping property names to use in the
/// resulting JSON object to plugin objects. The argument may also be
/// a string or number, in which case it is ignored, to support the
/// way `JSON.stringify` calls `toString` methods.
toJSON(pluginFields?: {[propName: string]: Plugin}): any {
let result: any = {doc: this.doc.toJSON(), selection: this.selection.toJSON()}
if (this.storedMarks) result.storedMarks = this.storedMarks.map(m => m.toJSON())
if (pluginFields && typeof pluginFields == 'object') for (let prop in pluginFields) {
if (prop == "doc" || prop == "selection")
throw new RangeError("The JSON fields `doc` and `selection` are reserved")
let plugin = pluginFields[prop], state = plugin.spec.state
if (state && state.toJSON) result[prop] = state.toJSON.call(plugin, (this as any)[plugin.key])
}
return result
}
/// Deserialize a JSON representation of a state. `config` should
/// have at least a `schema` field, and should contain array of
/// plugins to initialize the state with. `pluginFields` can be used
/// to deserialize the state of plugins, by associating plugin
/// instances with the property names they use in the JSON object.
static fromJSON(config: {
/// The schema to use.
schema: Schema
/// The set of active plugins.
plugins?: readonly Plugin[]
}, json: any, pluginFields?: {[propName: string]: Plugin}) {
if (!json) throw new RangeError("Invalid input for EditorState.fromJSON")
if (!config.schema) throw new RangeError("Required config field 'schema' missing")
let $config = new Configuration(config.schema, config.plugins)
let instance = new EditorState($config)
$config.fields.forEach(field => {
if (field.name == "doc") {
instance.doc = Node.fromJSON(config.schema, json.doc)
} else if (field.name == "selection") {
instance.selection = Selection.fromJSON(instance.doc, json.selection)
} else if (field.name == "storedMarks") {
if (json.storedMarks) instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON)
} else {
if (pluginFields) for (let prop in pluginFields) {
let plugin = pluginFields[prop], state = plugin.spec.state
if (plugin.key == field.name && state && state.fromJSON &&
Object.prototype.hasOwnProperty.call(json, prop)) {
// This field belongs to a plugin mapped to a JSON field, read it from there.
;(instance as any)[field.name] = state.fromJSON.call(plugin, config, json[prop], instance)
return
}
}
;(instance as any)[field.name] = field.init(config, instance)
}
})
return instance
}
}

View File

@@ -0,0 +1,215 @@
import {Transform, Step} from "prosemirror-transform"
import {Mark, MarkType, Node, Slice} from "prosemirror-model"
import {type EditorView} from "prosemirror-view"
import {Selection} from "./selection"
import {Plugin, PluginKey} from "./plugin"
import {EditorState} from "./state"
/// Commands are functions that take a state and a an optional
/// transaction dispatch function and...
///
/// - determine whether they apply to this state
/// - if not, return false
/// - if `dispatch` was passed, perform their effect, possibly by
/// passing a transaction to `dispatch`
/// - return true
///
/// In some cases, the editor view is passed as a third argument.
export type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
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
/// transaction 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, a `"composition"` property holding an ID identifying the
/// composition that caused it to transactions caused by composed DOM
/// input, and a `"uiEvent"` property of that may be `"paste"`,
/// `"cut"`, or `"drop"`.
export class Transaction extends Transform {
/// The timestamp associated with this transaction, in the same
/// format as `Date.now()`.
time: number
private curSelection: Selection
// The step count for which the current selection is valid.
private curSelectionFor = 0
// Bitfield to track which aspects of the state were updated by
// this transaction.
private updated = 0
// Object used to store metadata properties for the transaction.
private meta: {[name: string]: any} = Object.create(null)
/// The stored marks set by this transaction, if any.
storedMarks: readonly Mark[] | null
/// @internal
constructor(state: EditorState) {
super(state.doc)
this.time = Date.now()
this.curSelection = state.selection
this.storedMarks = state.storedMarks
}
/// 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(): 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
}
/// Update the transaction's current selection. Will determine the
/// selection that the editor gets when the transaction is applied.
setSelection(selection: Selection): this {
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
}
/// Whether the selection was explicitly updated by this transaction.
get selectionSet() {
return (this.updated & UPDATED_SEL) > 0
}
/// Set the current stored marks.
setStoredMarks(marks: readonly Mark[] | null): this {
this.storedMarks = marks
this.updated |= UPDATED_MARKS
return this
}
/// 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: readonly Mark[]): this {
if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks))
this.setStoredMarks(marks)
return this
}
/// Add a mark to the set of stored marks.
addStoredMark(mark: Mark): this {
return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks()))
}
/// Remove a mark or mark type from the set of stored marks.
removeStoredMark(mark: Mark | MarkType): this {
return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks()))
}
/// Whether the stored marks were explicitly set for this transaction.
get storedMarksSet() {
return (this.updated & UPDATED_MARKS) > 0
}
/// @internal
addStep(step: Step, doc: Node) {
super.addStep(step, doc)
this.updated = this.updated & ~UPDATED_MARKS
this.storedMarks = null
}
/// Update the timestamp for the transaction.
setTime(time: number): this {
this.time = time
return this
}
/// Replace the current selection with the given slice.
replaceSelection(slice: Slice): this {
this.selection.replace(this, slice)
return this
}
/// 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: Node, inheritMarks = true): this {
let selection = this.selection
if (inheritMarks)
node = node.mark(this.storedMarks || (selection.empty ? selection.$from.marks() : (selection.$from.marksAcross(selection.$to) || Mark.none)))
selection.replaceWith(this, node)
return this
}
/// Delete the selection.
deleteSelection(): this {
this.selection.replace(this)
return this
}
/// Replace the given range, or the selection if no range is given,
/// with a text node containing the given string.
insertText(text: string, from?: number, to?: number): this {
let schema = this.doc.type.schema
if (from == null) {
if (!text) return this.deleteSelection()
return this.replaceSelectionWith(schema.text(text), true)
} else {
if (to == null) to = from
to = to == null ? from : to
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
}
}
/// Store a metadata property in this transaction, keyed either by
/// name or by plugin.
setMeta(key: string | Plugin | PluginKey, value: any): this {
this.meta[typeof key == "string" ? key : key.key] = value
return this
}
/// Retrieve a metadata property for a given name or plugin.
getMeta(key: string | Plugin | PluginKey) {
return this.meta[typeof key == "string" ? key : key.key]
}
/// 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
}
/// Indicate that the editor should scroll the selection into view
/// when updated to the state produced by this transaction.
scrollIntoView(): this {
this.updated |= UPDATED_SCROLL
return this
}
/// True when this transaction has had `scrollIntoView` called on it.
get scrolledIntoView() {
return (this.updated & UPDATED_SCROLL) > 0
}
}