pax_global_header00006660000000000000000000000064140020231250014477gustar00rootroot0000000000000052 comment=389bb702247283b1d4da2656f46c0b1bcadf89e1 prosemirror-state-1.3.4/000077500000000000000000000000001400202312500152055ustar00rootroot00000000000000prosemirror-state-1.3.4/.gitignore000066400000000000000000000000731400202312500171750ustar00rootroot00000000000000/node_modules .tern-port /dist /notes.txt webpack.config.jsprosemirror-state-1.3.4/.npmignore000066400000000000000000000000371400202312500172040ustar00rootroot00000000000000/node_modules .tern-port /test prosemirror-state-1.3.4/.tern-project000066400000000000000000000001571400202312500176250ustar00rootroot00000000000000{ "libs": ["browser"], "plugins": { "node": {}, "complete_strings": {}, "es_modules": {} } } prosemirror-state-1.3.4/CHANGELOG.md000066400000000000000000000332561400202312500170270ustar00rootroot00000000000000## 1.3.4 (2021-01-20) ### Bug fixes Remove (broken, poorly conceptualized) `schema` option to `EditorState.reconfigure`. ## 1.3.3 (2020-03-18) ### Bug fixes When deleting a whole-document-selection causes default content to be generated, make sure the selection is moved to the start of the document. ## 1.3.2 (2019-11-20) ### Bug fixes Rename ES module files to use a .js extension, since Webpack gets confused by .mjs ## 1.3.1 (2019-11-19) ### Bug fixes The file referred to in the package's `module` field now is compiled down to ES5. ## 1.3.0 (2019-11-08) ### New features Add a `module` field to package json file. ## 1.2.4 (2019-08-12) ### Bug fixes `Transaction.setSelection` now immediately checks whether the given selection points into the proper document, rather than silently accepting an invalid selection. ## 1.2.3 (2019-05-08) ### Bug fixes The [`insertText`](https://prosemirror.net/docs/ref/#state.Transaction.insertText) method now collapses the selection to the end of the inserted text, even when given explicit start/end positions. ## 1.2.2 (2018-07-23) ### Bug fixes The `"appendedTransaction"` meta property on appended transactions now points to the root transaction instead of at the transaction itself, which it accidentally did before. ## 1.2.1 (2018-07-02) ### Bug fixes Fixes a bug in the default implementation of `Selection.getBookmark`. ## 1.2.0 (2018-04-05) ### New features [`EditorState.create`](https://prosemirror.net/docs/ref/#state.EditorState^create) now accepts a `storedMark` option to set the state's stored marks. [`EditorState.toJSON`](https://prosemirror.net/docs/ref/#state.EditorState.toJSON) and [`fromJSON`](https://prosemirror.net/docs/ref/#state.EditorState^fromJSON) persist the set of stored marks, when available. ## 1.1.1 (2018-03-15) ### Bug fixes Throw errors, rather than constructing invalid objects, when deserializing from invalid JSON data. ## 1.1.0 (2018-01-22) ### New features [`EditorState.toJSON`](https://prosemirror.net/docs/ref/#state.EditorState.toJSON) now accepts a string or number as argument to support the way `JSON.stringify` can call it. ## 1.0.2 (2017-12-13) ### Bug fixes Fix issue where a character might be selected after overwriting across block nodes. Make sure `replaceSelectionWith` doesn't needlessly copy unmarked nodes. ## 1.0.1 (2017-11-01) ### Bug fixes Typing over marked text now properly makes the new text inherit the old text's marks. ## 0.21.0 (2017-05-03) ### Breaking changes [`Selection.atStart`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.Selection^atStart), and [`atEnd`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.Selection^atEnd) no longer take a second `textOnly` parameter. ### New features [`Selection.near`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.Selection^near), [`atStart`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.Selection^atStart), and [`atEnd`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.Selection^atEnd) will now fall back to returning an [`AllSelection`](https://prosemirror.net/docs/ref/version/0.21.0.html#state.AllSelection) when unable to find a valid selection. This removes the (undocumented) requirement that documents always contain a valid selection position (though you'll probably still want to maintain this for practical UI reasons). ## 0.20.0 (2017-04-03) ### Breaking changes [`Selection.near`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection^near) no longer accepts a `textOnly` parameter. ### Bug fixes [`TextSelection.between`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.TextSelection^between) may now return a node selection when the document does not contain a valid cursor position. ### New features [`Selection`](https://prosemirror.net/docs/ref/version/0.20.0.html#model.Selection) objects now implement a [`content`](https://prosemirror.net/docs/ref/version/0.20.0.html#model.Selection.content) method that returns their content. This is used to determine what ends up on the clipboard when the selection is copied or dragged. Selections may now specify multiple [ranges](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection.ranges) that they cover, to generalize to more types of selections. The [`Selection`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection) superclass constructor takes an array of [ranges](https://prosemirror.net/docs/ref/version/0.20.0.html#state.SelectionRange) as optional third argument. Selections gained new methods [`replace`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection.replace) and [`replaceWith`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection.replaceWith) to provide subclasses more control over how selections of that type respond to being deleted or overwritten. Selections have a new method [`getBookmark`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.Selection.getBookmark) that custom selection classes can implement to allow the undo history to accurately store and restore them. The new selection class [`AllSelection`](https://prosemirror.net/docs/ref/version/0.20.0.html#state.AllSelection) can be used to select the entire document. ## 0.19.1 (2017-03-17) ### Bug fixes Fix an issue where `ensureMarks` would fail to reset the marks to the empty set when turning off the last mark. ## 0.19.0 (2017-03-16) ### Breaking changes `Selection.between` is now called [`TextSelection.between`](state.TextSelection^between), and only returns text selections. The JSON representation of selections changed. [`fromJSON`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Selection^fromJSON) will continue to understand the old representation, but if your own code touches the JSON data, you'll have to adjust it. All `Selection` objects now have [`$head`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Selection.$head)/[`$anchor`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Selection.$anchor) properties, so those can no longer be used to recognize text selections (use [`$cursor`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.TextSelection.$cursor) or `instanceof`). ### New features It is now possible to write your own [`Selection`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Selection) subclasses and set the editor selection to an instance of them (provided you implement all required methods and register them with [`Selection.jsonID`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Selection^jsonID)). Text selections now have a [`$cursor`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.TextSelection.$cursor) getter which returns a position only if this is a cursor selection. The new [`Transaction.ensureMarks`](https://prosemirror.net/docs/ref/version/0.19.0.html#state.Transaction.ensureMarks) method makes it easier to ensure given set of active marks without needlessly setting `storedMarks`. ## 0.18.0 (2017-02-24) ### Breaking changes Plugin objects now store their spec under a [`spec`](https://prosemirror.net/docs/ref/version/0.18.0.html#state.PluginSpec.spec) instead of an `options` property. The `options` property still works with a warning in this release. ## 0.17.1 (2017-02-08) ### Bug fixes [`Transaction.scrolledIntoView`](https://prosemirror.net/docs/ref/version/0.17.0.html##state.Transaction.scrolledIntoView) no longer always returns true. [`Selection.near`](https://prosemirror.net/docs/ref/version/0.17.0.html#state.Selection^neard) now takes a third `textOnly` argument, as the docs already claimed. ## 0.17.0 (2017-01-05) ### Breaking changes The way state is updated was changed. Instead of applying an action (a raw object with a `type` property), it is now done by [applying](https://prosemirror.net/docs/ref/version/0.17.0.html#state.EditorState.apply) a [`Transaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#state.Transaction). The `EditorTransform` class was renamed [`Transaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#state.Transaction), and extended to allow changing the set of stored marks and attaching custom metadata. ### New features Plugins now accept a [`filterTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#state.Plugin.constructor^options.filterTransaction) option that can be used to filter out transactions as they come in. Plugins also got an [`appendTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#state.Plugin.constructor^options.appendTransaction) option making it possible to follow up transactions with another transaction. ## 0.16.0 (2016-12-23) ### New features Plugins now take a [`view` option](https://prosemirror.net/docs/ref/version/0.16.0.html#state.Plugin.constructor^options.view) that can be used to interact with the [editor view](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorView). ## 0.15.0 (2016-12-10) ### Breaking changes Selection actions no longer scroll the new selection into view by default (they never were supposed to, but due to a bug they did). Add a `scrollIntoView` property to the action to get this behavior. ## 0.14.0 (2016-11-28) ### New features [Selection actions](https://prosemirror.net/docs/ref/version/0.14.0.html#state.SelectionAction) now have a `time` field and an (optional) `origin` field. ## 0.13.0 (2016-11-11) ### Breaking changes [`EditorTransform.replaceSelection`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.EditorTransform.replaceSelection) now takes a [slice](https://prosemirror.net/docs/ref/version/0.13.0.html#model.Slice), no longer a node. The new [`replaceSelectionWith`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.EditorTransform.replaceSelectionWith) method should be used to replace the selection with a node. Until the next release, calling it the old way will still work and emit a warning. ### Bug fixes The documentation for [`applyAction`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.StateField.applyAction) now actually reflects the arguments this method is given. ### New features A state field's [`applyAction`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.StateField.applyAction) method is now passed the previous state as 4th argument, so that it has access to the new doc and selection. [`EditorTransform.replaceSelection`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.EditorTransform.replaceSelection) now accepts a slice (or, as before, as a node), and uses a revised algorithm, relying on the [`defining`](https://prosemirror.net/docs/ref/version/0.13.0.html#model.NodeSpec.defining) node flag. The [`TextSelection`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.TextSelection) and [`NodeSelection`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.NodeSelection) classes now have a static [`create`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.TextSelection^create) convenience method for creating selections from unresolved positions. Allow [transform actions](https://prosemirror.net/docs/ref/version/0.13.0.html#state.TransformAction) to be extended during dispatch using [`extendTransformAction`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.extendTransformAction). Introduce [`sealed`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.TransformAction.sealed) flag to indicate when this is not safe. A new utility function [`NodeSelection.isSelectable`](https://prosemirror.net/docs/ref/version/0.13.0.html#state.NodeSelection.isSelectable) can be used to test whether a node can be the target of a node selection. ## 0.12.0 (2016-10-21) ### Breaking changes The interace to [`EditorState.toJSON`](https://prosemirror.net/docs/ref/version/0.12.0.html#state.EditorState.toJSON) and [`EditorState.fromJSON`](https://prosemirror.net/docs/ref/version/0.12.0.html#state.EditorState.fromJSON) has changed. The way plugins declare their [state field](https://prosemirror.net/docs/ref/version/0.12.0.html#state.Plugin.constructor.options.state) has changed. Only one state field per plugin is supported, and state fields no longer have hard-coded names. [`Plugin.getState`](https://prosemirror.net/docs/ref/version/0.12.0.html#state.Plugin.getState) is the way to access plugin state now. Plugin dependencies are no longer supported. `Plugin.reconfigure` is gone. Plugins are now always created with [`new Plugin`](https://prosemirror.net/docs/ref/version/0.12.0.html#state.Plugin.constructor). Plugins no longer have a `config` field. ### Bug fixes Node selections are now properly dropped when mapped over a change that replaces their nodes. ### New features [Plugin keys](https://prosemirror.net/docs/ref/version/0.12.0.html#state.PluginKey) can now be used to find plugins by identity. [Transform actions](https://prosemirror.net/docs/ref/version/0.12.0.html#state.TransformAction) now have a `time` field containing the timestamp when the change was made. ## 0.11.0 (2016-09-21) ### Breaking changes New module inheriting the [`Selection`](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Selection) and [`EditorTransform`](https://prosemirror.net/docs/ref/version/0.11.0.html#state.EditorTransform) abstraction, along with the persistent [state](https://prosemirror.net/docs/ref/version/0.11.0.html#state.EditorState) value that is now separate from the display logic, and the [plugin](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Plugin) system. `Selection.findAtStart`/`End` was renamed to [`Selection.atStart`](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Selection^atStart)/[`End`](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Selection^atEnd), and `Selection.findNear` to [`Selection.near`](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Selection^near). prosemirror-state-1.3.4/CONTRIBUTING.md000066400000000000000000000075161400202312500174470ustar00rootroot00000000000000# 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. prosemirror-state-1.3.4/LICENSE000066400000000000000000000021131400202312500162070ustar00rootroot00000000000000Copyright (C) 2015-2017 by Marijn Haverbeke 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. prosemirror-state-1.3.4/README.md000066400000000000000000000026411400202312500164670ustar00rootroot00000000000000# prosemirror-state [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-state/blob/master/CHANGELOG.md) ] This is a [core module](https://prosemirror.net/docs/ref/#state) of [ProseMirror](https://prosemirror.net). ProseMirror is a well-behaved rich semantic content editor based on contentEditable, with support for collaborative editing and custom document schemas. This [module](https://prosemirror.net/docs/ref/#state) implements the editor state, which tracks the current document and selection, and managed plugins. The [project page](https://prosemirror.net) has more information, a number of [examples](https://prosemirror.net/examples/) and the [documentation](https://prosemirror.net/docs/). This code is released under an [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). There's a [forum](http://discuss.prosemirror.net) for general discussion and support requests, and the [Github bug tracker](https://github.com/prosemirror/prosemirror/issues) is the place to report issues. We aim to be an inclusive, welcoming community. To make that explicit, we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. prosemirror-state-1.3.4/package.json000066400000000000000000000015221400202312500174730ustar00rootroot00000000000000{ "name": "prosemirror-state", "version": "1.3.4", "description": "ProseMirror editor state", "main": "dist/index.js", "module": "dist/index.es.js", "license": "MIT", "maintainers": [ { "name": "Marijn Haverbeke", "email": "marijnh@gmail.com", "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" }, "devDependencies": { "ist": "^1.0.0", "mocha": "^3.0.2", "prosemirror-test-builder": "^1.0.0", "rollup": "^2.26.3", "@rollup/plugin-buble": "^0.21.3" }, "scripts": { "test": "mocha test/test-*.js", "build": "rollup -c", "watch": "rollup -c -w", "prepare": "npm run build" } } prosemirror-state-1.3.4/rollup.config.js000066400000000000000000000005121400202312500203220ustar00rootroot00000000000000module.exports = { input: './src/index.js', output: [{ file: 'dist/index.js', format: 'cjs', sourcemap: true }, { file: 'dist/index.es.js', format: 'es', sourcemap: true }], plugins: [require('@rollup/plugin-buble')()], external(id) { return id[0] != "." && !require("path").isAbsolute(id) } } prosemirror-state-1.3.4/src/000077500000000000000000000000001400202312500157745ustar00rootroot00000000000000prosemirror-state-1.3.4/src/README.md000066400000000000000000000020371400202312500172550ustar00rootroot00000000000000This 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 @Transaction ### 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 @Plugin @PluginKey prosemirror-state-1.3.4/src/index.js000066400000000000000000000003361400202312500174430ustar00rootroot00000000000000export {Selection, SelectionRange, TextSelection, NodeSelection, AllSelection} from "./selection" export {Transaction} from "./transaction" export {EditorState} from "./state" export {Plugin, PluginKey} from "./plugin" prosemirror-state-1.3.4/src/plugin.js000066400000000000000000000120671400202312500176360ustar00rootroot00000000000000// PluginSpec:: interface // // This is the type passed to the [`Plugin`](#state.Plugin) // constructor. It provides a definition for a plugin. // // props:: ?EditorProps // 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. // // state:: ?StateField // 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. // // key:: ?PluginKey // 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. // // view:: ?(EditorView) → Object // 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. // // return::- // Should return an object with the following optional // properties: // // update:: ?(view: EditorView, prevState: EditorState) // Called whenever the view's state is updated. // // destroy:: ?() // Called when the view is destroyed or receives a state // with different plugins. // // filterTransaction:: ?(Transaction, EditorState) → bool // When present, this will be called before a transaction is // applied by the state, allowing the plugin to cancel it (by // returning false). // // appendTransaction:: ?(transactions: [Transaction], oldState: EditorState, newState: EditorState) → ?Transaction // 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. function bindProps(obj, self, target) { 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 { // :: (PluginSpec) // Create a plugin. constructor(spec) { // :: EditorProps // The [props](#view.EditorProps) exported by this plugin. this.props = {} if (spec.props) bindProps(spec.props, this, this.props) // :: Object // The plugin's [spec object](#state.PluginSpec). this.spec = spec this.key = spec.key ? spec.key.key : createKey("plugin") } // :: (EditorState) → any // Extract the plugin's state field from an editor state. getState(state) { return state[this.key] } } // StateField:: interface // 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. // // init:: (config: Object, instance: EditorState) → 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. // // apply:: (tr: Transaction, value: T, oldState: EditorState, newState: 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. // // toJSON:: ?(value: T) → * // Convert this field to JSON. Optional, can be left off to disable // JSON serialization for the field. // // fromJSON:: ?(config: Object, value: *, state: EditorState) → T // Deserialize the JSON representation of this field. Note that the // `state` argument is again a half-initialized state. const keys = Object.create(null) function createKey(name) { 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 { // :: (?string) // Create a plugin key. constructor(name = "key") { this.key = createKey(name) } // :: (EditorState) → ?Plugin // Get the active plugin with this key, if any, from an editor // state. get(state) { return state.config.pluginsByKey[this.key] } // :: (EditorState) → ?any // Get the plugin's state from an editor state. getState(state) { return state[this.key] } } prosemirror-state-1.3.4/src/selection.js000066400000000000000000000405741400202312500203310ustar00rootroot00000000000000import {Slice, Fragment} from "prosemirror-model" import {ReplaceStep, ReplaceAroundStep} from "prosemirror-transform" const classesById = Object.create(null) // ::- Superclass for editor selections. Every selection type should // extend this. Should not be instantiated directly. export class Selection { // :: (ResolvedPos, ResolvedPos, ?[SelectionRange]) // Initialize a selection with the head and anchor and ranges. If no // ranges are given, constructs a single range across `$anchor` and // `$head`. constructor($anchor, $head, ranges) { // :: [SelectionRange] // The ranges covered by the selection. this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))] // :: ResolvedPos // The resolved anchor of the selection (the side that stays in // place when the selection is modified). this.$anchor = $anchor // :: ResolvedPos // The resolved head of the selection (the side that moves when // the selection is modified). this.$head = $head } // :: number // The selection's anchor, as an unresolved position. get anchor() { return this.$anchor.pos } // :: number // The selection's head. get head() { return this.$head.pos } // :: number // The lower bound of the selection's main range. get from() { return this.$from.pos } // :: number // The upper bound of the selection's main range. get to() { return this.$to.pos } // :: ResolvedPos // The resolved lower bound of the selection's main range. get $from() { return this.ranges[0].$from } // :: ResolvedPos // The resolved upper bound of the selection's main range. get $to() { return this.ranges[0].$to } // :: bool // Indicates whether the selection contains any content. get empty() { 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 } // eq:: (Selection) → bool // Test whether the selection is the same as another selection. // map:: (doc: Node, mapping: Mappable) → Selection // Map this selection through a [mappable](#transform.Mappable) thing. `doc` // should be the new document to which we are mapping. // :: () → Slice // Get the content of this selection as a slice. content() { return this.$from.node(0).slice(this.from, this.to, true) } // :: (Transaction, ?Slice) // Replace the selection with a slice or, if no slice is given, // delete the selection. Will append to the given transaction. replace(tr, 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) } } // :: (Transaction, Node) // Replace the selection with the given node, appending the changes // to the given transaction. replaceWith(tr, 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) } } } // toJSON:: () → Object // 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. // :: (ResolvedPos, number, ?bool) → ?Selection // 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, dir, textOnly) { 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 } } // :: (ResolvedPos, ?number) → Selection // 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, bias = 1) { return this.findFrom($pos, bias) || this.findFrom($pos, -bias) || new AllSelection($pos.node(0)) } // :: (Node) → Selection // 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) { return findSelectionIn(doc, doc, 0, 0, 1) || new AllSelection(doc) } // :: (Node) → Selection // Find the cursor or leaf node selection closest to the end of the // given document. static atEnd(doc) { return findSelectionIn(doc, doc, doc.content.size, doc.childCount, -1) || new AllSelection(doc) } // :: (Node, Object) → Selection // Deserialize the JSON representation of a selection. Must be // implemented for custom classes (as a static class method). static fromJSON(doc, json) { 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) } // :: (string, constructor) // 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, selectionClass) { if (id in classesById) throw new RangeError("Duplicate use of selection JSON ID " + id) classesById[id] = selectionClass selectionClass.prototype.jsonID = id return selectionClass } // :: () → SelectionBookmark // 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() { return TextSelection.between(this.$anchor, this.$head).getBookmark() } } // :: bool // 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`. Selection.prototype.visible = true // SelectionBookmark:: interface // 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. // // map:: (mapping: Mapping) → SelectionBookmark // Map the bookmark through a set of changes. // // resolve:: (doc: Node) → Selection // 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. // ::- Represents a selected range in a document. export class SelectionRange { // :: (ResolvedPos, ResolvedPos) constructor($from, $to) { // :: ResolvedPos // The lower bound of the range. this.$from = $from // :: ResolvedPos // The upper bound of the range. this.$to = $to } } // ::- 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 { // :: (ResolvedPos, ?ResolvedPos) // Construct a text selection between the given points. constructor($anchor, $head = $anchor) { super($anchor, $head) } // :: ?ResolvedPos // 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, mapping) { 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, 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) { return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head } getBookmark() { return new TextBookmark(this.anchor, this.head) } toJSON() { return {type: "text", anchor: this.anchor, head: this.head} } static fromJSON(doc, json) { 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)) } // :: (Node, number, ?number) → TextSelection // Create a text selection from non-resolved positions. static create(doc, anchor, head = anchor) { let $anchor = doc.resolve(anchor) return new this($anchor, head == anchor ? $anchor : doc.resolve(head)) } // :: (ResolvedPos, ResolvedPos, ?number) → Selection // 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, $head, bias) { 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(anchor, head) { this.anchor = anchor this.head = head } map(mapping) { return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head)) } resolve(doc) { 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 { // :: (ResolvedPos) // Create a node selection. Does not verify the validity of its // argument. constructor($pos) { let node = $pos.nodeAfter let $end = $pos.node(0).resolve($pos.pos + node.nodeSize) super($pos, $end) // :: Node The selected node. this.node = node } map(doc, mapping) { 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) { return other instanceof NodeSelection && other.anchor == this.anchor } toJSON() { return {type: "node", anchor: this.anchor} } getBookmark() { return new NodeBookmark(this.anchor) } static fromJSON(doc, json) { if (typeof json.anchor != "number") throw new RangeError("Invalid input for NodeSelection.fromJSON") return new NodeSelection(doc.resolve(json.anchor)) } // :: (Node, number) → NodeSelection // Create a node selection from non-resolved positions. static create(doc, from) { return new this(doc.resolve(from)) } // :: (Node) → bool // Determines whether the given node may be selected as a node // selection. static isSelectable(node) { return !node.isText && node.type.spec.selectable !== false } } NodeSelection.prototype.visible = false Selection.jsonID("node", NodeSelection) class NodeBookmark { constructor(anchor) { this.anchor = anchor } map(mapping) { let {deleted, pos} = mapping.mapResult(this.anchor) return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos) } resolve(doc) { 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 { // :: (Node) // Create an all-selection over the given document. constructor(doc) { super(doc.resolve(0), doc.resolve(doc.content.size)) } replace(tr, 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() { return {type: "all"} } static fromJSON(doc) { return new AllSelection(doc) } map(doc) { return new AllSelection(doc) } eq(other) { return other instanceof AllSelection } getBookmark() { return AllBookmark } } Selection.jsonID("all", AllSelection) const AllBookmark = { map() { return this }, resolve(doc) { 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, pos, index, dir, text) { 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 } } function selectionToInsertionEnd(tr, startLen, bias) { 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 map.forEach((_from, _to, _newFrom, newTo) => { if (end == null) end = newTo }) tr.setSelection(Selection.near(tr.doc.resolve(end), bias)) } prosemirror-state-1.3.4/src/state.js000066400000000000000000000246521400202312500174630ustar00rootroot00000000000000import {Node} from "prosemirror-model" import {Selection} from "./selection" import {Transaction} from "./transaction" function bind(f, self) { return !self || !f ? f : f.bind(self) } class FieldDesc { constructor(name, desc, self) { this.name = name this.init = bind(desc.init, self) this.apply = bind(desc.apply, self) } } const baseFields = [ new FieldDesc("doc", { init(config) { return config.doc || config.schema.topNodeType.createAndFill() }, apply(tr) { return tr.doc } }), new FieldDesc("selection", { init(config, instance) { return config.selection || Selection.atStart(instance.doc) }, apply(tr) { return tr.selection } }), new FieldDesc("storedMarks", { init(config) { return config.storedMarks || null }, apply(tr, _marks, _old, state) { return state.selection.$cursor ? tr.storedMarks : null } }), new FieldDesc("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 { constructor(schema, plugins) { this.schema = schema this.fields = baseFields.concat() this.plugins = [] this.pluginsByKey = Object.create(null) 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(plugin.key, plugin.spec.state, 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 { constructor(config) { this.config = config } // doc:: Node // The current document. // selection:: Selection // The selection. // storedMarks:: ?[Mark] // A set of marks to apply to the next input. Will be null when // no explicit marks have been set. // :: Schema // The schema of the state's document. get schema() { return this.config.schema } // :: [Plugin] // The plugins that are active in this state. get plugins() { return this.config.plugins } // :: (Transaction) → EditorState // Apply the given transaction to produce a new state. apply(tr) { return this.applyTransaction(tr).state } // : (Transaction) → bool filterTransaction(tr, 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 } // :: (Transaction) → {state: EditorState, transactions: [Transaction]} // 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) { 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} } } // : (Transaction) → EditorState applyInner(tr) { 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[field.name] = field.apply(tr, this[field.name], this, newInstance) } for (let i = 0; i < applyListeners.length; i++) applyListeners[i](this, tr, newInstance) return newInstance } // :: Transaction // Start a [transaction](#state.Transaction) from this state. get tr() { return new Transaction(this) } // :: (Object) → EditorState // Create a new state. // // config::- Configuration options. Must contain `schema` or `doc` (or both). // // schema:: ?Schema // The schema to use (only relevant if no `doc` is specified). // // doc:: ?Node // The starting document. // // selection:: ?Selection // A valid selection in the document. // // storedMarks:: ?[Mark] // The initial set of [stored marks](#state.EditorState.storedMarks). // // plugins:: ?[Plugin] // The plugins that should be active in this state. static create(config) { 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[$config.fields[i].name] = $config.fields[i].init(config, instance) return instance } // :: (Object) → 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`](#state.StateField.init) method, passing in the new // configuration object.. // // config::- configuration options // // plugins:: [Plugin] // New set of active plugins. reconfigure(config) { 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[name] = this.hasOwnProperty(name) ? this[name] : fields[i].init(config, instance) } return instance } // :: (?union, string, number>) → Object // 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) { let result = {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[plugin.key]) } return result } // :: (Object, Object, ?Object) → EditorState // 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. // // config::- configuration options // // schema:: Schema // The schema to use. // // plugins:: ?[Plugin] // The set of active plugins. static fromJSON(config, json, pluginFields) { 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[field.name] = state.fromJSON.call(plugin, config, json[prop], instance) return } } instance[field.name] = field.init(config, instance) } }) return instance } // Kludge to allow the view to track mappings between different // instances of a state. // // FIXME this is no longer needed as of prosemirror-view 1.9.0, // though due to backwards-compat we should probably keep it around // for a while (if only as a no-op) static addApplyListener(f) { applyListeners.push(f) } static removeApplyListener(f) { let found = applyListeners.indexOf(f) if (found > -1) applyListeners.splice(found, 1) } } const applyListeners = [] prosemirror-state-1.3.4/src/transaction.js000066400000000000000000000157251400202312500206710ustar00rootroot00000000000000import {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) → 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, 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) → 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 } } prosemirror-state-1.3.4/test/000077500000000000000000000000001400202312500161645ustar00rootroot00000000000000prosemirror-state-1.3.4/test/state.js000066400000000000000000000024531400202312500176460ustar00rootroot00000000000000const {EditorState, Selection, TextSelection, NodeSelection} = require("..") // Wrapper object to make writing state tests easier. function selFor(doc) { let a = doc.tag.a if (a != null) { let $a = doc.resolve(a) if ($a.parent.inlineContent) return new TextSelection($a, doc.tag.b != null ? doc.resolve(doc.tag.b) : undefined) else return new NodeSelection($a) } return Selection.atStart(doc) } exports.selFor = selFor exports.TestState = class TestState { constructor(config) { if (!config.selection && config.doc) config.selection = selFor(config.doc) this.state = EditorState.create(config) } apply(tr) { this.state = this.state.apply(tr) } command(cmd) { cmd(this.state, tr => this.apply(tr)) } type(text) { this.apply(this.tr.insertText(text)) } deleteSelection() { this.apply(this.state.tr.deleteSelection()) } textSel(anchor, head) { let sel = TextSelection.create(this.state.doc, anchor, head) this.state = this.state.apply(this.state.tr.setSelection(sel)) } nodeSel(pos) { let sel = NodeSelection.create(this.state.doc, pos) this.state = this.state.apply(this.state.tr.setSelection(sel)) } get doc() { return this.state.doc } get selection() { return this.state.selection } get tr() { return this.state.tr } } prosemirror-state-1.3.4/test/test-selection.js000066400000000000000000000167351400202312500215000ustar00rootroot00000000000000const {TextSelection} = require("..") const {schema, eq, doc, blockquote, pre, p, li, ul, img, em, a, br, hr} = require("prosemirror-test-builder") const {TestState} = require("./state") const ist = require("ist") describe("Selection", () => { it("should follow changes", () => { let state = new TestState({doc: doc(p("hi")), schema}) state.apply(state.tr.insertText("xy", 1)) ist(state.selection.head, 3) ist(state.selection.anchor, 3) state.apply(state.tr.insertText("zq", 1)) ist(state.selection.head, 5) ist(state.selection.anchor, 5) state.apply(state.tr.insertText("uv", 7)) ist(state.selection.head, 5) ist(state.selection.anchor, 5) }) it("should move after inserted content", () => { let state = new TestState({doc: doc(p("hi")), schema}) state.textSel(2, 3) state.apply(state.tr.insertText("o")) ist(state.selection.head, 3) ist(state.selection.anchor, 3) }) it("moves after an inserted leaf node", () => { let state = new TestState({doc: doc(p("foobar")), schema}) state.textSel(4) state.apply(state.tr.replaceSelectionWith(schema.node("horizontal_rule"))) ist(state.doc, doc(p("foo"), hr, p("bar")), eq) ist(state.selection.head, 7) state.textSel(10) state.apply(state.tr.replaceSelectionWith(schema.node("horizontal_rule"))) ist(state.doc, doc(p("foo"), hr, p("bar"), hr), eq) ist(state.selection.from, 11) }) it("allows typing over a leaf node", () => { let state = new TestState({doc: doc(p("a"), "", hr, p("b")), schema}) state.nodeSel(3) state.apply(state.tr.replaceSelectionWith(schema.text("x"))) ist(state.doc, doc(p("a"), p("x"), p("b")), eq) ist(state.selection.head, 5) ist(state.selection.anchor, 5) }) it("allows deleting a selected block", () => { let state = new TestState({doc: doc(p("foo"), ul(li(p("bar")), li(p("baz")), li(p("quux")))), schema}) state.nodeSel(0) state.deleteSelection() ist(state.doc, doc(ul(li(p("bar")), li(p("baz")), li(p("quux")))), eq) ist(state.selection.head, 3) state.nodeSel(2) state.deleteSelection() ist(state.doc, doc(ul(li(p("baz")), li(p("quux")))), eq) ist(state.selection.head, 3) state.nodeSel(9) state.deleteSelection() ist(state.doc, doc(ul(li(p("baz")))), eq) ist(state.selection.head, 6) state.nodeSel(0) state.deleteSelection() ist(state.doc, doc(p()), eq) }) it("preserves the marks of a deleted selection", () => { let state = new TestState({doc: doc(p("foo", em("bar"), "baz"))}) state.deleteSelection() ist(state.state.storedMarks.length, 1) }) it("doesn't preserve non-inclusive marks of a deleted selection", () => { let state = new TestState({doc: doc(p("foo", a(em("bar")), "baz"))}) state.deleteSelection() ist(state.state.storedMarks.length, 1) }) it("doesn't preserve marks when deleting a selection at the end of a block", () => { let state = new TestState({doc: doc(p("foo", em("bar")), p("baz"))}) state.deleteSelection() ist(!state.state.storedMarks) }) it("drops non-inclusive marks at the end of a deleted span when appropriate", () => { let state = new TestState({doc: doc(p("foo", a("ba", em("r")), "baz"))}) state.deleteSelection() ist(state.state.storedMarks.map(x => x.type.name).join(), "em") }) it("keeps non-inclusive marks when still inside them", () => { let state = new TestState({doc: doc(p("foo", a("b", em("a"), "r"), "baz"))}) state.deleteSelection() ist(state.state.storedMarks.length, 2) }) it("preserves marks when typing over marked text", () => { let state = new TestState({doc: doc(p("foo ", em("bar"), " baz"))}) state.apply(state.tr.insertText("quux")) ist(state.doc, doc(p("foo ", em("quux"), " baz")), eq) state.apply(state.tr.insertText("bar", 5, 9)) ist(state.doc, doc(p("foo ", em("bar"), " baz")), eq) }) it("allows deleting a leaf", () => { let state = new TestState({doc: doc(p("a"), hr, hr, p("b")), schema}) state.nodeSel(3) state.deleteSelection() ist(state.doc, doc(p("a"), hr, p("b")), eq) ist(state.selection.from, 3) state.deleteSelection() ist(state.doc, doc(p("a"), p("b")), eq) ist(state.selection.head, 4) }) it("properly handles deleting the selection", () => { let state = new TestState({doc: doc(p("foo", img, "bar"), blockquote(p("hi")), p("ay")), schema}) state.nodeSel(4) state.apply(state.tr.deleteSelection()) ist(state.doc, doc(p("foobar"), blockquote(p("hi")), p("ay")), eq) ist(state.selection.head, 4) state.nodeSel(9) state.apply(state.tr.deleteSelection()) ist(state.doc, doc(p("foobar"), p("ay")), eq) ist(state.selection.from, 9) state.nodeSel(8) state.apply(state.tr.deleteSelection()) ist(state.doc, doc(p("foobar")), eq) ist(state.selection.from, 7) }) it("can replace inline selections", () => { let state = new TestState({doc: doc(p("foo", img, "bar", img, "baz")), schema}) state.nodeSel(4) state.apply(state.tr.replaceSelectionWith(schema.node("hard_break"))) ist(state.doc, doc(p("foo", br, "bar", img, "baz")), eq) ist(state.selection.head, 5) ist(state.selection.empty) state.nodeSel(8) state.apply(state.tr.insertText("abc")) ist(state.doc, doc(p("foo", br, "barabcbaz")), eq) ist(state.selection.head, 11) ist(state.selection.empty) state.nodeSel(0) state.apply(state.tr.insertText("xyz")) ist(state.doc, doc(p("xyz")), eq) }) it("can replace a block selection", () => { let state = new TestState({doc: doc(p("abc"), hr, hr, blockquote(p("ow"))), schema}) state.nodeSel(5) state.apply(state.tr.replaceSelectionWith(schema.node("code_block"))) ist(state.doc, doc(p("abc"), pre(), hr, blockquote(p("ow"))), eq) ist(state.selection.from, 7) state.nodeSel(8) state.apply(state.tr.replaceSelectionWith(schema.node("paragraph"))) ist(state.doc, doc(p("abc"), pre(), hr, p()), eq) ist(state.selection.from, 9) }) it("puts the cursor after the inserted text when inserting a list item", () => { let state = new TestState({doc: doc(p("abc"))}) let source = doc(ul(li(p("def")))) state.apply(state.tr.replaceSelection(source.slice(source.tag.a, source.tag.b, true))) ist(state.selection.from, 6) }) }) describe("TextSelection.between", () => { it("uses arguments when possible", () => { let d = doc(p("foo")) let s = TextSelection.between(d.resolve(d.tag.b), d.resolve(d.tag.a)) ist(s.anchor, d.tag.b) ist(s.head, d.tag.a) }) it("will adjust when necessary", () => { let d = doc("", p("foo")) let s = TextSelection.between(d.resolve(d.tag.a), d.resolve(d.tag.a)) ist(s.anchor, 1) }) it("uses bias when adjusting", () => { let d = doc(p("foo"), "", p("bar")), pos = d.resolve(d.tag.a) let sUp = TextSelection.between(pos, pos, -1) ist(sUp.anchor, 4) let sDown = TextSelection.between(pos, pos, 1) ist(sDown.anchor, 6) }) it("will fall back to a node selection", () => { let d = doc(hr, "") let s = TextSelection.between(d.resolve(d.tag.a), d.resolve(d.tag.a)) ist(s.node, d.firstChild) }) it("will collapse towards the other argument", () => { let d = doc("", p("foo"), "") let s = TextSelection.between(d.resolve(d.tag.a), d.resolve(d.tag.b)) ist(s.anchor, 1) ist(s.head, 4) s = TextSelection.between(d.resolve(d.tag.b), d.resolve(d.tag.a)) ist(s.anchor, 4) ist(s.head, 1) }) }) prosemirror-state-1.3.4/test/test-state.js000066400000000000000000000124421400202312500206220ustar00rootroot00000000000000const {EditorState, TextSelection, Plugin, PluginKey} = require("..") const {schema, eq, doc, p} = require("prosemirror-test-builder") const ist = require("ist") const messageCountKey = new PluginKey("messageCount") const messageCountPlugin = new Plugin({ key: messageCountKey, state: { init() { return 0 }, apply(_, count) { return count + 1 }, toJSON(count) { return count }, fromJSON(_, count) { return count } }, props: { testProp() { return this } } }) const transactionPlugin = new Plugin({ filterTransaction(tr) { return !tr.getMeta("filtered") }, appendTransaction(trs, _, state) { let last = trs[trs.length - 1] if (last && last.getMeta("append")) return state.tr.insertText("A") } }) describe("State", () => { it("creates a default doc", () => { let state = EditorState.create({schema}) ist(state.doc, doc(p()), eq) }) it("creates a default selection", () => { let state = EditorState.create({doc: doc(p("foo"))}) ist(state.selection.from, 1) ist(state.selection.to, 1) }) it("applies transform transactions", () => { let state = EditorState.create({schema}) let newState = state.apply(state.tr.insertText("hi")) ist(state.doc, doc(p()), eq) ist(newState.doc, doc(p("hi")), eq) ist(newState.selection.from, 3) }) it("supports plugin fields", () => { let state = EditorState.create({plugins: [messageCountPlugin], schema}) let newState = state.apply(state.tr).apply(state.tr) ist(messageCountPlugin.getState(state), 0) ist(messageCountPlugin.getState(newState), 2) }) it("can be serialized to JSON", () => { let state = EditorState.create({plugins: [messageCountPlugin], doc: doc(p("ok"))}) state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 3))) let pluginProps = {count: messageCountPlugin} let expected = {doc: {type: "doc", content: [{type: "paragraph", content: [{type: "text", text: "ok"}]}]}, selection: {type: "text", anchor: 3, head: 3}, count: 1} let json = state.toJSON(pluginProps) ist(JSON.stringify(json), JSON.stringify(expected)) let copy = EditorState.fromJSON({plugins: [messageCountPlugin], schema}, json, pluginProps) ist(copy.doc, state.doc, eq) ist(copy.selection.from, 3) ist(messageCountPlugin.getState(copy), 1) let limitedJSON = state.toJSON() ist(limitedJSON.doc) ist(limitedJSON.messageCount$, undefined) let deserialized = EditorState.fromJSON({plugins: [messageCountPlugin], schema}, limitedJSON) ist(messageCountPlugin.getState(deserialized), 0) }) it("supports specifying and persisting storedMarks", () => { let state = EditorState.create({doc: doc(p("ok")), storedMarks: [schema.mark("em")]}) ist(state.storedMarks.length, 1) let copy = EditorState.fromJSON({schema}, state.toJSON()) ist(copy.storedMarks.length, 1) }) it("supports reconfiguration", () => { let state = EditorState.create({plugins: [messageCountPlugin], schema}) ist(messageCountPlugin.getState(state), 0) let without = state.reconfigure({}) ist(messageCountPlugin.getState(without), undefined) ist(without.plugins.length, 0) ist(without.doc, doc(p()), eq) let reAdd = without.reconfigure({plugins: [messageCountPlugin]}) ist(messageCountPlugin.getState(reAdd), 0) ist(reAdd.plugins.length, 1) }) it("allows plugins to filter transactions", () => { let state = EditorState.create({plugins: [transactionPlugin], schema}) let applied = state.applyTransaction(state.tr.insertText("X")) ist(applied.state.doc, doc(p("X")), eq) ist(applied.transactions.length, 1) applied = state.applyTransaction(state.tr.insertText("Y").setMeta("filtered", true)) ist(applied.state, state) ist(applied.transactions.length, 0) }) it("allows plugins to append transactions", () => { let state = EditorState.create({plugins: [transactionPlugin], schema}) let applied = state.applyTransaction(state.tr.insertText("X").setMeta("append", true)) ist(applied.state.doc, doc(p("XA")), eq) ist(applied.transactions.length, 2) }) it("stores a reference to a root transaction for appended transactions", () => { let state = EditorState.create({schema, plugins: [new Plugin({ appendTransaction: (_trs, _oldState, newState) => newState.tr.insertText("Y") })]}) let {transactions} = state.applyTransaction(state.tr.insertText("X")) ist(transactions.length, 2) ist(transactions[1].getMeta("appendedTransaction"), transactions[0]) }) it("supports JSON.stringify toJSON arguments", () => { let someObject = { someKey: EditorState.create({schema}) } ist(JSON.stringify(someObject).length > 0) }) }) describe("Plugin", () => { it("calls prop functions bound to the plugin", () => { ist(messageCountPlugin.props.testProp(), messageCountPlugin) }) it("can be found by key", () => { let state = EditorState.create({plugins: [messageCountPlugin], schema}) ist(messageCountKey.get(state), messageCountPlugin) ist(messageCountKey.getState(state), 0) }) it("generates new keys", () => { let p1 = new Plugin({}), p2 = new Plugin({}) ist(p1.key != p2.key) let k1 = new PluginKey("foo"), k2 = new PluginKey("foo") ist(k1.key != k2.key) }) })