` tag in a schema-basic code block) no longer break the editor.
## 0.17.2 (2017-01-16)
### Bug fixes
Call custom click handlers before applying select-node behavior for a ctrl/cmd-click.
Fix failure to apply DOM changes that start at document position 0.
## 0.17.1 (2017-01-07)
### Bug fixes
Fix issue where a document update that left the selection in the same place sometimes led to an incorrect DOM selection.
Make sure [`EditorView.focus`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorView.focus) doesn't cause the browser to scroll the top of the editor into view.
## 0.17.0 (2017-01-05)
### Breaking changes
The `handleDOMEvent` prop has been dropped in favor of the [`handleDOMEvents`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.handleDOMEvents) (plural) prop.
The `onChange` prop has been replaced by a [`dispatchTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.dispatchTransaction) prop (which takes a transaction instead of an action).
### New features
Added support for a [`handleDOMEvents` prop](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.handleDOMEvents), which allows you to provide handler functions per DOM event, and works even for events that the editor doesn't normally add a handler for.
Add view method [`dispatch`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorView.dispatch), which provides a convenient way to dispatch transactions.
The [`dispatchTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.dispatchTransaction) (used to be `onAction`) prop is now optional, and will default to simply applying the transaction to the current view state.
[Widget decorations](https://prosemirror.net/docs/ref/version/0.17.0.html#view.Decoration.widget) now accept an option `associative` which can be used to configure on which side of content inserted at their position they end up.
Typing immediately after deleting text now preserves the marks of the deleted text.
Transactions that update the selection because of mouse or touch input now get a metadata property `pointer` with the value `true`.
## 0.16.0 (2016-12-23)
### Bug fixes
Solve problem where setting a node selection would trigger a DOM read, leading to the selection being reset.
## 0.16.0 (2016-12-23)
### Breaking changes
The `spellcheck`, `label`, and `class` props are now replaced by an [`attributes` prop](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.attributes).
### Bug fixes
Ignoring/aborting an action should no longer lead to the DOM being stuck in an outdated state.
Typing at the end of a textblock which ends in a non-text node now actually works.
DOM nodes for leaf document nodes are now set as non-editable to prevent various issues such as stray cursors inside of them and Firefox adding image resize controls.
Inserting a node no longer causes nodes of the same type after it to be neednessly redrawn.
### New features
Add a new editor prop [`editable`](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.editable) which controls whether the editor's `contentEditable` behavior is enabled.
Plugins and props can now set any DOM attribute on the outer editor node using the [`attributes` prop](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.attributes).
Node view constructors and update methods now have access to the node's wrapping decorations, which can be used to pass information to a node view without encoding it in the document.
Attributes added or removed by node and inline [decorations](https://prosemirror.net/docs/ref/version/0.16.0.html#view.Decoration) no longer cause the nodes inside of them to be fully redrawn, making node views more stable and allowing CSS transitions to be used.
## 0.15.2 (2016-12-10)
### Bug fixes
The native selection is now appropriately hidden when there is a node selection.
## 0.15.1 (2016-12-10)
### Bug fixes
Fix DOM parsing for decorated text nodes.
## 0.15.0 (2016-12-10)
### Breaking changes
The editor view no longer wraps its editable DOM element in a wrapper element. The `ProseMirror` CSS class now applies directly to the editable element. The `ProseMirror-content` CSS class is still present for ease of upgrading but will be dropped in the next release.
The editor view no longer draws a drop cursor when dragging content over the editor. The new [`prosemirror-dropcursor`](https://github.com/prosemirror/prosemirror-dropcursor) module implements this as a plugin.
### Bug fixes
Simple typing and backspacing now gets handled by the browser without ProseMirror redrawing the touched nodes, making spell-checking and various platform-specific input tricks (long-press on OS X, double space on iOS) work in the editor.
Improve tracking of DOM nodes that have been touched by user changes, so that [`updateState`](https://prosemirror.net/docs/ref/version/0.15.0.html#view.EditorView.updateState) can reliably fix them.
Changes to the document that happen while dragging editor content no longer break moving of the content.
Adding or removing a mark directly in the DOM (for example with the bold/italic buttons in iOS' context menu) now produces mark steps, rather than replace steps.
Pressing backspace at the start of a paragraph on Android now allows key handlers for backspace to fire.
Toggling a mark when there is no selection now works better on mobile platforms.
### New features
Introduces an [`endOfTextblock`](https://prosemirror.net/docs/ref/version/0.15.0.html#view.EditorView.endOfTextblock) method on views, which can be used to find out in a bidi- and layout-aware way whether the selection is on the edge of a textblock.
## 0.14.4 (2016-12-02)
### Bug fixes
Fix issue where node decorations would stick around in the DOM after the decoration was removed.
Setting or removing a node selection in an unfocused editor now properly updates the DOM to show that selection.
## 0.14.2 (2016-11-30)
### Bug fixes
FIX: Avoid unneeded selection resets which sometimes confused browsers.
## 0.14.2 (2016-11-29)
### Bug fixes
Fix a bug where inverted selections weren't created in the DOM correctly.
## 0.14.1 (2016-11-29)
### Bug fixes
Restores previously broken kludge that allows the cursor to appear after non-text content at the end of a line.
## 0.14.0 (2016-11-28)
### Breaking changes
Wrapping decorations are now created using the [`nodeName`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.DecorationAttrs.nodeName) property. The `wrapper` property is no longer supported.
The `onUnmountDOM` prop is no longer supported (use a node view with a [`destroy`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.NodeView.destroy) method instead).
The `domSerializer` prop is no longer supported. Use [node views](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.nodeViews) to configure editor-specific node representations.
### New features
Widget decorations can now be given a [`key`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.Decoration.widget^options.key) property to prevent unneccesary redraws.
The `EditorView` class now has a [`destroy`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorView.destroy) method for cleaning up.
The [`handleClickOn`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.handleClickOn) prop and friends now receive a `direct` boolean argument that indicates whether the node was clicked directly.
[Widget decorations](https://prosemirror.net/docs/ref/version/0.14.0.html#view.Decoration^widget) now support a `stopEvent` option that can be used to control which DOM events that pass through them should be ignored by the editor view.
You can now [specify](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.nodeViews) custom [node views](https://prosemirror.net/docs/ref/version/0.14.0.html#view.NodeView) for an editor view, which give you control over the way node of a given type are represented in the DOM. See the related [RFC](https://discuss.prosemirror.net/t/rfc-node-views-to-manage-the-representation-of-nodes/463).
## 0.13.2 (2016-11-15)
### Bug fixes
Fixes an issue where widget decorations in the middle of text nodes would sometimes disappear.
## 0.13.1 (2016-11-15)
### Bug fixes
Fixes event handler crash (and subsequent bad default behavior) when pasting some types of external HTML into an editor.
## 0.13.0 (2016-11-11)
### Breaking changes
Selecting nodes on OS X is now done with cmd-leftclick rather than ctrl-leftclick.
### Bug fixes
Pasting text into a code block will now insert the raw text.
Widget decorations at the start or end of a textblock no longer block horizontal cursor motion through them.
Widget nodes at the end of textblocks are now reliably drawn during display updates.
### New features
[`DecorationSet.map`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.DecorationSet.map) now takes an options object which allows you to specify an `onRemove` callback to be notified when remapping drops decorations.
The [`transformPastedHTML`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.EditorProps.transformPastedHTML) and [`transformPastedText`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.EditorProps.transformPastedText) props were (re-)added, and can be used to clean up pasted content.
## 0.12.2 (2016-11-02)
### Bug fixes
Inline decorations that span across an empty textblock no longer crash the display drawing code.
## 0.12.1 (2016-11-01)
### Bug fixes
Use a separate document to parse pasted HTML to better protect
against cross-site scripting attacks.
Specifying multiple classes in a decoration now actually works.
Ignore empty inline decorations when building a decoration set.
## 0.12.0 (2016-10-21)
### Breaking changes
The return value of
[`EditorView.posAtCoords`](https://prosemirror.net/docs/ref/version/0.12.0.html#view.EditorView.posAtCoords) changed to
contain an `inside` property pointing at the innermost node that the
coordinates are inside of. (Note that the docs for this method were
wrong in the previous release.)
### Bug fixes
Reduce reliance on shift-state tracking to minimize damage when
it gets out of sync.
Fix bug that'd produce bogus document positions for DOM positions
inside non-document nodes.
Don't treat fast ctrl-clicks as double or triple clicks.
### New features
Implement [decorations](https://prosemirror.net/docs/ref/version/0.12.0.html#view.Decoration), a way to
influence the way the document is drawn. Add the [`decorations`
prop](https://prosemirror.net/docs/ref/version/0.12.0.html#view.EditorProps.decorations) to specify them.
## 0.11.2 (2016-10-04)
### Bug fixes
Pass actual event object to [`handleDOMEvent`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.handleDOMEvent), rather than just its name.
Fix display corruption caused by using the wrong state as previous version during IME.
## 0.11.0 (2016-09-21)
### Breaking changes
Moved into a separate module from the old `edit` submodule. Completely
new approach to managing the editor's DOM representation and input.
Event handlers and options are now replaced by
[props](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps). The view's state is now 'shallow',
represented entirely by a set of props, one of which holds an editor
state value from the [state](https://prosemirror.net/docs/ref/version/0.11.0.html#state) module.
When the user interacts with the editor, it will pass an
[action](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Action) to its
[`onAction`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.onAction) prop, which is responsible
for triggering an view update.
The `markRange` system was dropped, to be replaced in the next release
by a 'decoration' system.
There is no keymap support in the view module anymore. Use a
[keymap](https://prosemirror.net/docs/ref/version/0.11.0.html#keymap) plugin for that.
The undo [history](https://prosemirror.net/docs/ref/version/0.11.0.html#history) is now a separate plugin.
CSS needed by the editor is no longer injected implicitly into the
page. Instead, you should arrange for the `style/prosemirror.css` file
to be loaded into your page.
### New features
The DOM [parser](https://prosemirror.net/docs/ref/version/0.11.0.html#model.DOMParser) and
[serializer](https://prosemirror.net/docs/ref/version/0.11.0.html#model.DOMSerializer) used to interact with the visible
DOM and the clipboard can now be customized through
[props](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps).
You can now provide a catch-all DOM
[event handler](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.handleDOMEvent) to get a first
chance at handling DOM events.
The [`onUnmountDOM`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.onUnmountDOM) can be used to
be notified when a piece of the document DOM is thrown away (in case
cleanup is needed).
prosemirror-view-1.23.13/CONTRIBUTING.md 0000664 0000000 0000000 00000007516 14232025710 0017452 0 ustar 00root root 0000000 0000000 # 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-view-1.23.13/LICENSE 0000664 0000000 0000000 00000002113 14232025710 0016212 0 ustar 00root root 0000000 0000000 Copyright (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-view-1.23.13/README.md 0000664 0000000 0000000 00000002637 14232025710 0016477 0 ustar 00root root 0000000 0000000 # prosemirror-view
[ [**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-view/blob/master/CHANGELOG.md) ]
This is a [core module](https://prosemirror.net/docs/ref/#view) 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/#view) exports the editor
view, which renders the current document in the browser, and handles
user events.
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-view-1.23.13/package.json 0000664 0000000 0000000 00000002024 14232025710 0017474 0 ustar 00root root 0000000 0000000 {
"name": "prosemirror-view",
"version": "1.23.13",
"description": "ProseMirror's view component",
"main": "dist/index.js",
"module": "dist/index.es.js",
"style": "style/prosemirror.css",
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijnh@gmail.com",
"web": "http://marijnhaverbeke.nl"
}
],
"repository": {
"type": "git",
"url": "git://github.com/prosemirror/prosemirror-view.git"
},
"dependencies": {
"prosemirror-model": "^1.16.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
},
"devDependencies": {
"ist": "^1.0.0",
"mocha": "^9.1.2",
"moduleserve": "^0.7.0",
"prosemirror-test-builder": "^1.0.0",
"rollup": "^2.26.3",
"@rollup/plugin-buble": "^0.21.3"
},
"scripts": {
"test": "FIXME autorun browser tests",
"test-server": "node test/link-mocha-css.js && moduleserve test --port 8090",
"build": "rollup -c",
"watch": "rollup -c -w",
"prepare": "npm run build"
}
}
prosemirror-view-1.23.13/rollup.config.js 0000664 0000000 0000000 00000000512 14232025710 0020325 0 ustar 00root root 0000000 0000000 module.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-view-1.23.13/src/ 0000775 0000000 0000000 00000000000 14232025710 0015777 5 ustar 00root root 0000000 0000000 prosemirror-view-1.23.13/src/README.md 0000664 0000000 0000000 00000000721 14232025710 0017256 0 ustar 00root root 0000000 0000000 ProseMirror's view module displays a given [editor
state](#state.EditorState) in the DOM, and handles user events.
Make sure you load `style/prosemirror.css` as a stylesheet when using
this module.
@EditorView
### Props
@EditorProps
@DirectEditorProps
@NodeView
### Decorations
Decorations make it possible to influence the way the document is
drawn, without actually changing the document.
@Decoration
@DecorationAttrs
@DecorationSet
@DecorationSource
prosemirror-view-1.23.13/src/browser.js 0000664 0000000 0000000 00000002446 14232025710 0020026 0 ustar 00root root 0000000 0000000 const result = {}
export default result
if (typeof navigator != "undefined" && typeof document != "undefined") {
const ie_edge = /Edge\/(\d+)/.exec(navigator.userAgent)
const ie_upto10 = /MSIE \d/.test(navigator.userAgent)
const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent)
let ie = result.ie = !!(ie_upto10 || ie_11up || ie_edge)
result.ie_version = ie_upto10 ? document.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : null
result.gecko = !ie && /gecko\/(\d+)/i.test(navigator.userAgent)
result.gecko_version = result.gecko && +(/Firefox\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1]
let chrome = !ie && /Chrome\/(\d+)/.exec(navigator.userAgent)
result.chrome = !!chrome
result.chrome_version = chrome && +chrome[1]
// Is true for both iOS and iPadOS for convenience
result.safari = !ie && /Apple Computer/.test(navigator.vendor)
result.ios = result.safari && (/Mobile\/\w+/.test(navigator.userAgent) || navigator.maxTouchPoints > 2)
result.mac = result.ios || /Mac/.test(navigator.platform)
result.android = /Android \d/.test(navigator.userAgent)
result.webkit = "webkitFontSmoothing" in document.documentElement.style
result.webkit_version = result.webkit && +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1]
}
prosemirror-view-1.23.13/src/capturekeys.js 0000664 0000000 0000000 00000023250 14232025710 0020676 0 ustar 00root root 0000000 0000000 import {Selection, NodeSelection, TextSelection, AllSelection} from "prosemirror-state"
import browser from "./browser"
import {domIndex, selectionCollapsed} from "./dom"
import {selectionToDOM} from "./selection"
function moveSelectionBlock(state, dir) {
let {$anchor, $head} = state.selection
let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head)
let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null
return $start && Selection.findFrom($start, dir)
}
function apply(view, sel) {
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView())
return true
}
function selectHorizontally(view, dir, mods) {
let sel = view.state.selection
if (sel instanceof TextSelection) {
if (!sel.empty || mods.indexOf("s") > -1) {
return false
} else if (view.endOfTextblock(dir > 0 ? "right" : "left")) {
let next = moveSelectionBlock(view.state, dir)
if (next && (next instanceof NodeSelection)) return apply(view, next)
return false
} else if (!(browser.mac && mods.indexOf("m") > -1)) {
let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc
if (!node || node.isText) return false
let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos
if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM)) return false
if (NodeSelection.isSelectable(node)) {
return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head))
} else if (browser.webkit) {
// Chrome and Safari will introduce extra pointless cursor
// positions around inline uneditable nodes, so we have to
// take over and move the cursor past them (#937)
return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize)))
} else {
return false
}
}
} else if (sel instanceof NodeSelection && sel.node.isInline) {
return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from))
} else {
let next = moveSelectionBlock(view.state, dir)
if (next) return apply(view, next)
return false
}
}
function nodeLen(node) {
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length
}
function isIgnorable(dom) {
let desc = dom.pmViewDesc
return desc && desc.size == 0 && (dom.nextSibling || dom.nodeName != "BR")
}
// Make sure the cursor isn't directly after one or more ignored
// nodes, which will confuse the browser's cursor motion logic.
function skipIgnoredNodesLeft(view) {
let sel = view.root.getSelection()
let node = sel.focusNode, offset = sel.focusOffset
if (!node) return
let moveNode, moveOffset, force = false
// Gecko will do odd things when the selection is directly in front
// of a non-editable node, so in that case, move it into the next
// node if possible. Issue prosemirror/prosemirror#832.
if (browser.gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset])) force = true
for (;;) {
if (offset > 0) {
if (node.nodeType != 1) {
break
} else {
let before = node.childNodes[offset - 1]
if (isIgnorable(before)) {
moveNode = node
moveOffset = --offset
} else if (before.nodeType == 3) {
node = before
offset = node.nodeValue.length
} else break
}
} else if (isBlockNode(node)) {
break
} else {
let prev = node.previousSibling
while (prev && isIgnorable(prev)) {
moveNode = node.parentNode
moveOffset = domIndex(prev)
prev = prev.previousSibling
}
if (!prev) {
node = node.parentNode
if (node == view.dom) break
offset = 0
} else {
node = prev
offset = nodeLen(node)
}
}
}
if (force) setSelFocus(view, sel, node, offset)
else if (moveNode) setSelFocus(view, sel, moveNode, moveOffset)
}
// Make sure the cursor isn't directly before one or more ignored
// nodes.
function skipIgnoredNodesRight(view) {
let sel = view.root.getSelection()
let node = sel.focusNode, offset = sel.focusOffset
if (!node) return
let len = nodeLen(node)
let moveNode, moveOffset
for (;;) {
if (offset < len) {
if (node.nodeType != 1) break
let after = node.childNodes[offset]
if (isIgnorable(after)) {
moveNode = node
moveOffset = ++offset
}
else break
} else if (isBlockNode(node)) {
break
} else {
let next = node.nextSibling
while (next && isIgnorable(next)) {
moveNode = next.parentNode
moveOffset = domIndex(next) + 1
next = next.nextSibling
}
if (!next) {
node = node.parentNode
if (node == view.dom) break
offset = len = 0
} else {
node = next
offset = 0
len = nodeLen(node)
}
}
}
if (moveNode) setSelFocus(view, sel, moveNode, moveOffset)
}
function isBlockNode(dom) {
let desc = dom.pmViewDesc
return desc && desc.node && desc.node.isBlock
}
function setSelFocus(view, sel, node, offset) {
if (selectionCollapsed(sel)) {
let range = document.createRange()
range.setEnd(node, offset)
range.setStart(node, offset)
sel.removeAllRanges()
sel.addRange(range)
} else if (sel.extend) {
sel.extend(node, offset)
}
view.domObserver.setCurSelection()
let {state} = view
// If no state update ends up happening, reset the selection.
setTimeout(() => {
if (view.state == state) selectionToDOM(view)
}, 50)
}
// : (EditorState, number)
// Check whether vertical selection motion would involve node
// selections. If so, apply it (if not, the result is left to the
// browser)
function selectVertically(view, dir, mods) {
let sel = view.state.selection
if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1) return false
if (browser.mac && mods.indexOf("m") > -1) return false
let {$from, $to} = sel
if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) {
let next = moveSelectionBlock(view.state, dir)
if (next && (next instanceof NodeSelection))
return apply(view, next)
}
if (!$from.parent.inlineContent) {
let side = dir < 0 ? $from : $to
let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir)
return beyond ? apply(view, beyond) : false
}
return false
}
function stopNativeHorizontalDelete(view, dir) {
if (!(view.state.selection instanceof TextSelection)) return true
let {$head, $anchor, empty} = view.state.selection
if (!$head.sameParent($anchor)) return true
if (!empty) return false
if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) return true
let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter)
if (nextNode && !nextNode.isText) {
let tr = view.state.tr
if (dir < 0) tr.delete($head.pos - nextNode.nodeSize, $head.pos)
else tr.delete($head.pos, $head.pos + nextNode.nodeSize)
view.dispatch(tr)
return true
}
return false
}
function switchEditable(view, node, state) {
view.domObserver.stop()
node.contentEditable = state
view.domObserver.start()
}
// Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821
// In which Safari (and at some point in the past, Chrome) does really
// wrong things when the down arrow is pressed when the cursor is
// directly at the start of a textblock and has an uneditable node
// after it
function safariDownArrowBug(view) {
if (!browser.safari || view.state.selection.$head.parentOffset > 0) return
let {focusNode, focusOffset} = view.root.getSelection()
if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
focusNode.firstChild && focusNode.firstChild.contentEditable == "false") {
let child = focusNode.firstChild
switchEditable(view, child, true)
setTimeout(() => switchEditable(view, child, false), 20)
}
}
// A backdrop key mapping used to make sure we always suppress keys
// that have a dangerous default effect, even if the commands they are
// bound to return false, and to make sure that cursor-motion keys
// find a cursor (as opposed to a node selection) when pressed. For
// cursor-motion keys, the code in the handlers also takes care of
// block selections.
function getMods(event) {
let result = ""
if (event.ctrlKey) result += "c"
if (event.metaKey) result += "m"
if (event.altKey) result += "a"
if (event.shiftKey) result += "s"
return result
}
export function captureKeyDown(view, event) {
let code = event.keyCode, mods = getMods(event)
if (code == 8 || (browser.mac && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac
return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodesLeft(view)
} else if (code == 46 || (browser.mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac
return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodesRight(view)
} else if (code == 13 || code == 27) { // Enter, Esc
return true
} else if (code == 37) { // Left arrow
return selectHorizontally(view, -1, mods) || skipIgnoredNodesLeft(view)
} else if (code == 39) { // Right arrow
return selectHorizontally(view, 1, mods) || skipIgnoredNodesRight(view)
} else if (code == 38) { // Up arrow
return selectVertically(view, -1, mods) || skipIgnoredNodesLeft(view)
} else if (code == 40) { // Down arrow
return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodesRight(view)
} else if (mods == (browser.mac ? "m" : "c") &&
(code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz]
return true
}
return false
}
prosemirror-view-1.23.13/src/clipboard.js 0000664 0000000 0000000 00000023660 14232025710 0020303 0 ustar 00root root 0000000 0000000 import {Slice, Fragment, DOMParser, DOMSerializer} from "prosemirror-model"
import browser from "./browser"
export function serializeForClipboard(view, slice) {
let context = [], {content, openStart, openEnd} = slice
while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild.childCount == 1) {
openStart--
openEnd--
let node = content.firstChild
context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null)
content = node.content
}
let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema)
let doc = detachedDoc(), wrap = doc.createElement("div")
wrap.appendChild(serializer.serializeFragment(content, {document: doc}))
let firstChild = wrap.firstChild, needsWrap
while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
for (let i = needsWrap.length - 1; i >= 0; i--) {
let wrapper = doc.createElement(needsWrap[i])
while (wrap.firstChild) wrapper.appendChild(wrap.firstChild)
wrap.appendChild(wrapper)
if (needsWrap[i] != "tbody") {
openStart++
openEnd++
}
}
firstChild = wrap.firstChild
}
if (firstChild && firstChild.nodeType == 1)
firstChild.setAttribute("data-pm-slice", `${openStart} ${openEnd} ${JSON.stringify(context)}`)
let text = view.someProp("clipboardTextSerializer", f => f(slice)) ||
slice.content.textBetween(0, slice.content.size, "\n\n")
return {dom: wrap, text}
}
// : (EditorView, string, string, ?bool, ResolvedPos) → ?Slice
// Read a slice of content from the clipboard (or drop data).
export function parseFromClipboard(view, text, html, plainText, $context) {
let dom, inCode = $context.parent.type.spec.code, slice
if (!html && !text) return null
let asText = text && (plainText || inCode || !html)
if (asText) {
view.someProp("transformPastedText", f => { text = f(text, inCode || plainText) })
if (inCode) return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty
let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText))
if (parsed) {
slice = parsed
} else {
let marks = $context.marks()
let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema)
dom = document.createElement("div")
text.split(/(?:\r\n?|\n)+/).forEach(block => {
let p = dom.appendChild(document.createElement("p"))
if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks)))
})
}
} else {
view.someProp("transformPastedHTML", f => { html = f(html) })
dom = readHTML(html)
if (browser.webkit) restoreReplacedSpaces(dom)
}
let contextNode = dom && dom.querySelector("[data-pm-slice]")
let sliceData = contextNode && /^(\d+) (\d+) (.*)/.exec(contextNode.getAttribute("data-pm-slice"))
if (!slice) {
let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema)
slice = parser.parseSlice(dom, {
preserveWhitespace: !!(asText || sliceData),
context: $context,
ruleFromNode(dom) {
if (dom.nodeName == "BR" && !dom.nextSibling &&
dom.parentNode && !inlineParents.test(dom.parentNode.nodeName)) return {ignore: true}
}
})
}
if (sliceData) {
slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[3])
} else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent
slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true)
if (slice.openStart || slice.openEnd) {
let openStart = 0, openEnd = 0
for (let node = slice.content.firstChild; openStart < slice.openStart && !node.type.spec.isolating;
openStart++, node = node.firstChild) {}
for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node.type.spec.isolating;
openEnd++, node = node.lastChild) {}
slice = closeSlice(slice, openStart, openEnd)
}
}
view.someProp("transformPasted", f => { slice = f(slice) })
return slice
}
const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i
// Takes a slice parsed with parseSlice, which means there hasn't been
// any content-expression checking done on the top nodes, tries to
// find a parent node in the current context that might fit the nodes,
// and if successful, rebuilds the slice so that it fits into that parent.
//
// This addresses the problem that Transform.replace expects a
// coherent slice, and will fail to place a set of siblings that don't
// fit anywhere in the schema.
function normalizeSiblings(fragment, $context) {
if (fragment.childCount < 2) return fragment
for (let d = $context.depth; d >= 0; d--) {
let parent = $context.node(d)
let match = parent.contentMatchAt($context.index(d))
let lastWrap, result = []
fragment.forEach(node => {
if (!result) return
let wrap = match.findWrapping(node.type), inLast
if (!wrap) return result = null
if (inLast = result.length && lastWrap.length && addToSibling(wrap, lastWrap, node, result[result.length - 1], 0)) {
result[result.length - 1] = inLast
} else {
if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap.length)
let wrapped = withWrappers(node, wrap)
result.push(wrapped)
match = match.matchType(wrapped.type, wrapped.attrs)
lastWrap = wrap
}
})
if (result) return Fragment.from(result)
}
return fragment
}
function withWrappers(node, wrap, from = 0) {
for (let i = wrap.length - 1; i >= from; i--)
node = wrap[i].create(null, Fragment.from(node))
return node
}
// Used to group adjacent nodes wrapped in similar parents by
// normalizeSiblings into the same parent node
function addToSibling(wrap, lastWrap, node, sibling, depth) {
if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) {
let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild, depth + 1)
if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner))
let match = sibling.contentMatchAt(sibling.childCount)
if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1]))
return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))))
}
}
function closeRight(node, depth) {
if (depth == 0) return node
let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild, depth - 1))
let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)
return node.copy(fragment.append(fill))
}
function closeRange(fragment, side, from, to, depth, openEnd) {
let node = side < 0 ? fragment.firstChild : fragment.lastChild, inner = node.content
if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd)
if (depth >= from)
inner = side < 0 ? node.contentMatchAt(0).fillBefore(inner, fragment.childCount > 1 || openEnd <= depth).append(inner)
: inner.append(node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true))
return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner))
}
function closeSlice(slice, openStart, openEnd) {
if (openStart < slice.openStart)
slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd)
if (openEnd < slice.openEnd)
slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd)
return slice
}
// Trick from jQuery -- some elements must be wrapped in other
// elements for innerHTML to work. I.e. if you do `div.innerHTML =
// ".. "` the table cells are ignored.
const wrapMap = {
thead: ["table"],
tbody: ["table"],
tfoot: ["table"],
caption: ["table"],
colgroup: ["table"],
col: ["table", "colgroup"],
tr: ["table", "tbody"],
td: ["table", "tbody", "tr"],
th: ["table", "tbody", "tr"]
}
let _detachedDoc = null
function detachedDoc() {
return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"))
}
function readHTML(html) {
let metas = /^(\s*]*>)*/.exec(html)
if (metas) html = html.slice(metas[0].length)
let elt = detachedDoc().createElement("div")
let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap
if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "" + n + ">").reverse().join("")
elt.innerHTML = html
if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt
return elt
}
// Webkit browsers do some hard-to-predict replacement of regular
// spaces with non-breaking spaces when putting content on the
// clipboard. This tries to convert such non-breaking spaces (which
// will be wrapped in a plain span on Chrome, a span with class
// Apple-converted-space on Safari) back to regular spaces.
function restoreReplacedSpaces(dom) {
let nodes = dom.querySelectorAll(browser.chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space")
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode)
node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node)
}
}
function addContext(slice, context) {
if (!slice.size) return slice
let schema = slice.content.firstChild.type.schema, array
try { array = JSON.parse(context) }
catch(e) { return slice }
let {content, openStart, openEnd} = slice
for (let i = array.length - 2; i >= 0; i -= 2) {
let type = schema.nodes[array[i]]
if (!type || type.hasRequiredAttrs()) break
content = Fragment.from(type.create(array[i + 1], content))
openStart++; openEnd++
}
return new Slice(content, openStart, openEnd)
}
prosemirror-view-1.23.13/src/decoration.js 0000664 0000000 0000000 00000063121 14232025710 0020467 0 ustar 00root root 0000000 0000000 function compareObjs(a, b) {
if (a == b) return true
for (let p in a) if (a[p] !== b[p]) return false
for (let p in b) if (!(p in a)) return false
return true
}
class WidgetType {
constructor(toDOM, spec) {
this.spec = spec || noSpec
this.side = this.spec.side || 0
this.toDOM = toDOM
}
map(mapping, span, offset, oldOffset) {
let {pos, deleted} = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1)
return deleted ? null : new Decoration(pos - offset, pos - offset, this)
}
valid() { return true }
eq(other) {
return this == other ||
(other instanceof WidgetType &&
(this.spec.key && this.spec.key == other.spec.key ||
this.toDOM == other.toDOM && compareObjs(this.spec, other.spec)))
}
destroy(node) {
if (this.spec.destroy) this.spec.destroy(node)
}
}
class InlineType {
constructor(attrs, spec) {
this.spec = spec || noSpec
this.attrs = attrs
}
map(mapping, span, offset, oldOffset) {
let from = mapping.map(span.from + oldOffset, this.spec.inclusiveStart ? -1 : 1) - offset
let to = mapping.map(span.to + oldOffset, this.spec.inclusiveEnd ? 1 : -1) - offset
return from >= to ? null : new Decoration(from, to, this)
}
valid(_, span) { return span.from < span.to }
eq(other) {
return this == other ||
(other instanceof InlineType && compareObjs(this.attrs, other.attrs) &&
compareObjs(this.spec, other.spec))
}
static is(span) { return span.type instanceof InlineType }
}
class NodeType {
constructor(attrs, spec) {
this.spec = spec || noSpec
this.attrs = attrs
}
map(mapping, span, offset, oldOffset) {
let from = mapping.mapResult(span.from + oldOffset, 1)
if (from.deleted) return null
let to = mapping.mapResult(span.to + oldOffset, -1)
if (to.deleted || to.pos <= from.pos) return null
return new Decoration(from.pos - offset, to.pos - offset, this)
}
valid(node, span) {
let {index, offset} = node.content.findIndex(span.from), child
return offset == span.from && !(child = node.child(index)).isText && offset + child.nodeSize == span.to
}
eq(other) {
return this == other ||
(other instanceof NodeType && compareObjs(this.attrs, other.attrs) &&
compareObjs(this.spec, other.spec))
}
}
// ::- Decoration objects can be provided to the view through the
// [`decorations` prop](#view.EditorProps.decorations). They come in
// several variants—see the static members of this class for details.
export class Decoration {
constructor(from, to, type) {
// :: number
// The start position of the decoration.
this.from = from
// :: number
// The end position. Will be the same as `from` for [widget
// decorations](#view.Decoration^widget).
this.to = to
this.type = type
}
copy(from, to) {
return new Decoration(from, to, this.type)
}
eq(other, offset = 0) {
return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to
}
map(mapping, offset, oldOffset) {
return this.type.map(mapping, this, offset, oldOffset)
}
// :: (number, union<(view: EditorView, getPos: () → number) → dom.Node, dom.Node>, ?Object) → Decoration
// Creates a widget decoration, which is a DOM node that's shown in
// the document at the given position. It is recommended that you
// delay rendering the widget by passing a function that will be
// called when the widget is actually drawn in a view, but you can
// also directly pass a DOM node. `getPos` can be used to find the
// widget's current document position.
//
// spec::- These options are supported:
//
// side:: ?number
// Controls which side of the document position this widget is
// associated with. When negative, it is drawn before a cursor
// at its position, and content inserted at that position ends
// up after the widget. When zero (the default) or positive, the
// widget is drawn after the cursor and content inserted there
// ends up before the widget.
//
// When there are multiple widgets at a given position, their
// `side` values determine the order in which they appear. Those
// with lower values appear first. The ordering of widgets with
// the same `side` value is unspecified.
//
// When `marks` is null, `side` also determines the marks that
// the widget is wrapped in—those of the node before when
// negative, those of the node after when positive.
//
// marks:: ?[Mark]
// The precise set of marks to draw around the widget.
//
// stopEvent:: ?(event: dom.Event) → bool
// Can be used to control which DOM events, when they bubble out
// of this widget, the editor view should ignore.
//
// ignoreSelection:: ?bool
// When set (defaults to false), selection changes inside the
// widget are ignored, and don't cause ProseMirror to try and
// re-sync the selection with its selection state.
//
// key:: ?string
// When comparing decorations of this type (in order to decide
// whether it needs to be redrawn), ProseMirror will by default
// compare the widget DOM node by identity. If you pass a key,
// that key will be compared instead, which can be useful when
// you generate decorations on the fly and don't want to store
// and reuse DOM nodes. Make sure that any widgets with the same
// key are interchangeable—if widgets differ in, for example,
// the behavior of some event handler, they should get
// different keys.
//
// destroy:: ?(node: dom.Node)
// Called when the widget decoration is removed as a result of
// mapping
static widget(pos, toDOM, spec) {
return new Decoration(pos, pos, new WidgetType(toDOM, spec))
}
// :: (number, number, DecorationAttrs, ?Object) → Decoration
// Creates an inline decoration, which adds the given attributes to
// each inline node between `from` and `to`.
//
// spec::- These options are recognized:
//
// inclusiveStart:: ?bool
// Determines how the left side of the decoration is
// [mapped](#transform.Position_Mapping) when content is
// inserted directly at that position. By default, the decoration
// won't include the new content, but you can set this to `true`
// to make it inclusive.
//
// inclusiveEnd:: ?bool
// Determines how the right side of the decoration is mapped.
// See
// [`inclusiveStart`](#view.Decoration^inline^spec.inclusiveStart).
static inline(from, to, attrs, spec) {
return new Decoration(from, to, new InlineType(attrs, spec))
}
// :: (number, number, DecorationAttrs, ?Object) → Decoration
// Creates a node decoration. `from` and `to` should point precisely
// before and after a node in the document. That node, and only that
// node, will receive the given attributes.
//
// spec::-
//
// Optional information to store with the decoration. It
// is also used when comparing decorators for equality.
static node(from, to, attrs, spec) {
return new Decoration(from, to, new NodeType(attrs, spec))
}
// :: Object
// The spec provided when creating this decoration. Can be useful
// if you've stored extra information in that object.
get spec() { return this.type.spec }
get inline() { return this.type instanceof InlineType }
}
// DecorationAttrs:: interface
// A set of attributes to add to a decorated node. Most properties
// simply directly correspond to DOM attributes of the same name,
// which will be set to the property's value. These are exceptions:
//
// class:: ?string
// A CSS class name or a space-separated set of class names to be
// _added_ to the classes that the node already had.
//
// style:: ?string
// A string of CSS to be _added_ to the node's existing `style` property.
//
// nodeName:: ?string
// When non-null, the target node is wrapped in a DOM element of
// this type (and the other attributes are applied to this element).
const none = [], noSpec = {}
// :: class extends DecorationSource
// A collection of [decorations](#view.Decoration), organized in
// such a way that the drawing algorithm can efficiently use and
// compare them. This is a persistent data structure—it is not
// modified, updates create a new value.
export class DecorationSet {
constructor(local, children) {
this.local = local && local.length ? local : none
this.children = children && children.length ? children : none
}
// :: (Node, [Decoration]) → DecorationSet
// Create a set of decorations, using the structure of the given
// document.
static create(doc, decorations) {
return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty
}
// :: (?number, ?number, ?(spec: Object) → bool) → [Decoration]
// Find all decorations in this set which touch the given range
// (including decorations that start or end directly at the
// boundaries) and match the given predicate on their spec. When
// `start` and `end` are omitted, all decorations in the set are
// considered. When `predicate` isn't given, all decorations are
// assumed to match.
find(start, end, predicate) {
let result = []
this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate)
return result
}
findInner(start, end, result, offset, predicate) {
for (let i = 0; i < this.local.length; i++) {
let span = this.local[i]
if (span.from <= end && span.to >= start && (!predicate || predicate(span.spec)))
result.push(span.copy(span.from + offset, span.to + offset))
}
for (let i = 0; i < this.children.length; i += 3) {
if (this.children[i] < end && this.children[i + 1] > start) {
let childOff = this.children[i] + 1
this.children[i + 2].findInner(start - childOff, end - childOff, result, offset + childOff, predicate)
}
}
}
// :: (Mapping, Node, ?Object) → DecorationSet
// Map the set of decorations in response to a change in the
// document.
//
// options::- An optional set of options.
//
// onRemove:: ?(decorationSpec: Object)
// When given, this function will be called for each decoration
// that gets dropped as a result of the mapping, passing the
// spec of that decoration.
map(mapping, doc, options) {
if (this == empty || mapping.maps.length == 0) return this
return this.mapInner(mapping, doc, 0, 0, options || noSpec)
}
mapInner(mapping, node, offset, oldOffset, options) {
let newLocal
for (let i = 0; i < this.local.length; i++) {
let mapped = this.local[i].map(mapping, offset, oldOffset)
if (mapped && mapped.type.valid(node, mapped)) (newLocal || (newLocal = [])).push(mapped)
else if (options.onRemove) options.onRemove(this.local[i].spec)
}
if (this.children.length)
return mapChildren(this.children, newLocal, mapping, node, offset, oldOffset, options)
else
return newLocal ? new DecorationSet(newLocal.sort(byPos)) : empty
}
// :: (Node, [Decoration]) → DecorationSet
// Add the given array of decorations to the ones in the set,
// producing a new set. Needs access to the current document to
// create the appropriate tree structure.
add(doc, decorations) {
if (!decorations.length) return this
if (this == empty) return DecorationSet.create(doc, decorations)
return this.addInner(doc, decorations, 0)
}
addInner(doc, decorations, offset) {
let children, childIndex = 0
doc.forEach((childNode, childOffset) => {
let baseOffset = childOffset + offset, found
if (!(found = takeSpansForNode(decorations, childNode, baseOffset))) return
if (!children) children = this.children.slice()
while (childIndex < children.length && children[childIndex] < childOffset) childIndex += 3
if (children[childIndex] == childOffset)
children[childIndex + 2] = children[childIndex + 2].addInner(childNode, found, baseOffset + 1)
else
children.splice(childIndex, 0, childOffset, childOffset + childNode.nodeSize, buildTree(found, childNode, baseOffset + 1, noSpec))
childIndex += 3
})
let local = moveSpans(childIndex ? withoutNulls(decorations) : decorations, -offset)
for (let i = 0; i < local.length; i++) if (!local[i].type.valid(doc, local[i])) local.splice(i--, 1)
return new DecorationSet(local.length ? this.local.concat(local).sort(byPos) : this.local,
children || this.children)
}
// :: ([Decoration]) → DecorationSet
// Create a new set that contains the decorations in this set, minus
// the ones in the given array.
remove(decorations) {
if (decorations.length == 0 || this == empty) return this
return this.removeInner(decorations, 0)
}
removeInner(decorations, offset) {
let children = this.children, local = this.local
for (let i = 0; i < children.length; i += 3) {
let found, from = children[i] + offset, to = children[i + 1] + offset
for (let j = 0, span; j < decorations.length; j++) if (span = decorations[j]) {
if (span.from > from && span.to < to) {
decorations[j] = null
;(found || (found = [])).push(span)
}
}
if (!found) continue
if (children == this.children) children = this.children.slice()
let removed = children[i + 2].removeInner(found, from + 1)
if (removed != empty) {
children[i + 2] = removed
} else {
children.splice(i, 3)
i -= 3
}
}
if (local.length) for (let i = 0, span; i < decorations.length; i++) if (span = decorations[i]) {
for (let j = 0; j < local.length; j++) if (local[j].eq(span, offset)) {
if (local == this.local) local = this.local.slice()
local.splice(j--, 1)
}
}
if (children == this.children && local == this.local) return this
return local.length || children.length ? new DecorationSet(local, children) : empty
}
forChild(offset, node) {
if (this == empty) return this
if (node.isLeaf) return DecorationSet.empty
let child, local
for (let i = 0; i < this.children.length; i += 3) if (this.children[i] >= offset) {
if (this.children[i] == offset) child = this.children[i + 2]
break
}
let start = offset + 1, end = start + node.content.size
for (let i = 0; i < this.local.length; i++) {
let dec = this.local[i]
if (dec.from < end && dec.to > start && (dec.type instanceof InlineType)) {
let from = Math.max(start, dec.from) - start, to = Math.min(end, dec.to) - start
if (from < to) (local || (local = [])).push(dec.copy(from, to))
}
}
if (local) {
let localSet = new DecorationSet(local.sort(byPos))
return child ? new DecorationGroup([localSet, child]) : localSet
}
return child || empty
}
eq(other) {
if (this == other) return true
if (!(other instanceof DecorationSet) ||
this.local.length != other.local.length ||
this.children.length != other.children.length) return false
for (let i = 0; i < this.local.length; i++)
if (!this.local[i].eq(other.local[i])) return false
for (let i = 0; i < this.children.length; i += 3)
if (this.children[i] != other.children[i] ||
this.children[i + 1] != other.children[i + 1] ||
!this.children[i + 2].eq(other.children[i + 2])) return false
return true
}
locals(node) {
return removeOverlap(this.localsInner(node))
}
localsInner(node) {
if (this == empty) return none
if (node.inlineContent || !this.local.some(InlineType.is)) return this.local
let result = []
for (let i = 0; i < this.local.length; i++) {
if (!(this.local[i].type instanceof InlineType))
result.push(this.local[i])
}
return result
}
}
// DecorationSource:: interface
// An object that can [provide](#view.EditorProps.decorations)
// decorations. Implemented by [`DecorationSet`](#view.DecorationSet),
// and passed to [node views](#view.EditorProps.nodeViews).
//
// map:: (Mapping, Node) → DecorationSource
// Map the set of decorations in response to a change in the
// document.
const empty = new DecorationSet()
// :: DecorationSet
// The empty set of decorations.
DecorationSet.empty = empty
DecorationSet.removeOverlap = removeOverlap
// :- An abstraction that allows the code dealing with decorations to
// treat multiple DecorationSet objects as if it were a single object
// with (a subset of) the same interface.
class DecorationGroup {
constructor(members) {
this.members = members
}
map(mapping, doc) {
const mappedDecos = this.members.map(
member => member.map(mapping, doc, noSpec)
)
return DecorationGroup.from(mappedDecos)
}
forChild(offset, child) {
if (child.isLeaf) return DecorationSet.empty
let found = []
for (let i = 0; i < this.members.length; i++) {
let result = this.members[i].forChild(offset, child)
if (result == empty) continue
if (result instanceof DecorationGroup) found = found.concat(result.members)
else found.push(result)
}
return DecorationGroup.from(found)
}
eq(other) {
if (!(other instanceof DecorationGroup) ||
other.members.length != this.members.length) return false
for (let i = 0; i < this.members.length; i++)
if (!this.members[i].eq(other.members[i])) return false
return true
}
locals(node) {
let result, sorted = true
for (let i = 0; i < this.members.length; i++) {
let locals = this.members[i].localsInner(node)
if (!locals.length) continue
if (!result) {
result = locals
} else {
if (sorted) {
result = result.slice()
sorted = false
}
for (let j = 0; j < locals.length; j++) result.push(locals[j])
}
}
return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none
}
// : ([DecorationSet]) → union
// Create a group for the given array of decoration sets, or return
// a single set when possible.
static from(members) {
switch (members.length) {
case 0: return empty
case 1: return members[0]
default: return new DecorationGroup(members)
}
}
}
function mapChildren(oldChildren, newLocal, mapping, node, offset, oldOffset, options) {
let children = oldChildren.slice()
// Mark the children that are directly touched by changes, and
// move those that are after the changes.
let shift = (oldStart, oldEnd, newStart, newEnd) => {
for (let i = 0; i < children.length; i += 3) {
let end = children[i + 1], dSize
if (end < 0 || oldStart > end + oldOffset) continue
let start = children[i] + oldOffset
if (oldEnd >= start) {
children[i + 1] = oldStart <= start ? -2 : -1
} else if (newStart >= offset && (dSize = (newEnd - newStart) - (oldEnd - oldStart))) {
children[i] += dSize
children[i + 1] += dSize
}
}
}
for (let i = 0; i < mapping.maps.length; i++) mapping.maps[i].forEach(shift)
// Find the child nodes that still correspond to a single node,
// recursively call mapInner on them and update their positions.
let mustRebuild = false
for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) { // Touched nodes
if (children[i + 1] == -2) {
mustRebuild = true
children[i + 1] = -1
continue
}
let from = mapping.map(oldChildren[i] + oldOffset), fromLocal = from - offset
if (fromLocal < 0 || fromLocal >= node.content.size) {
mustRebuild = true
continue
}
// Must read oldChildren because children was tagged with -1
let to = mapping.map(oldChildren[i + 1] + oldOffset, -1), toLocal = to - offset
let {index, offset: childOffset} = node.content.findIndex(fromLocal)
let childNode = node.maybeChild(index)
if (childNode && childOffset == fromLocal && childOffset + childNode.nodeSize == toLocal) {
let mapped = children[i + 2].mapInner(mapping, childNode, from + 1, oldChildren[i] + oldOffset + 1, options)
if (mapped != empty) {
children[i] = fromLocal
children[i + 1] = toLocal
children[i + 2] = mapped
} else {
children[i + 1] = -2
mustRebuild = true
}
} else {
mustRebuild = true
}
}
// Remaining children must be collected and rebuilt into the appropriate structure
if (mustRebuild) {
let decorations = mapAndGatherRemainingDecorations(children, oldChildren, newLocal || [], mapping,
offset, oldOffset, options)
let built = buildTree(decorations, node, 0, options)
newLocal = built.local
for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) {
children.splice(i, 3)
i -= 3
}
for (let i = 0, j = 0; i < built.children.length; i += 3) {
let from = built.children[i]
while (j < children.length && children[j] < from) j += 3
children.splice(j, 0, built.children[i], built.children[i + 1], built.children[i + 2])
}
}
return new DecorationSet(newLocal && newLocal.sort(byPos), children)
}
function moveSpans(spans, offset) {
if (!offset || !spans.length) return spans
let result = []
for (let i = 0; i < spans.length; i++) {
let span = spans[i]
result.push(new Decoration(span.from + offset, span.to + offset, span.type))
}
return result
}
function mapAndGatherRemainingDecorations(children, oldChildren, decorations, mapping, offset, oldOffset, options) {
// Gather all decorations from the remaining marked children
function gather(set, oldOffset) {
for (let i = 0; i < set.local.length; i++) {
let mapped = set.local[i].map(mapping, offset, oldOffset)
if (mapped) decorations.push(mapped)
else if (options.onRemove) options.onRemove(set.local[i].spec)
}
for (let i = 0; i < set.children.length; i += 3)
gather(set.children[i + 2], set.children[i] + oldOffset + 1)
}
for (let i = 0; i < children.length; i += 3) if (children[i + 1] == -1)
gather(children[i + 2], oldChildren[i] + oldOffset + 1)
return decorations
}
function takeSpansForNode(spans, node, offset) {
if (node.isLeaf) return null
let end = offset + node.nodeSize, found = null
for (let i = 0, span; i < spans.length; i++) {
if ((span = spans[i]) && span.from > offset && span.to < end) {
;(found || (found = [])).push(span)
spans[i] = null
}
}
return found
}
function withoutNulls(array) {
let result = []
for (let i = 0; i < array.length; i++)
if (array[i] != null) result.push(array[i])
return result
}
// : ([Decoration], Node, number) → DecorationSet
// Build up a tree that corresponds to a set of decorations. `offset`
// is a base offset that should be subtracted from the `from` and `to`
// positions in the spans (so that we don't have to allocate new spans
// for recursive calls).
function buildTree(spans, node, offset, options) {
let children = [], hasNulls = false
node.forEach((childNode, localStart) => {
let found = takeSpansForNode(spans, childNode, localStart + offset)
if (found) {
hasNulls = true
let subtree = buildTree(found, childNode, offset + localStart + 1, options)
if (subtree != empty)
children.push(localStart, localStart + childNode.nodeSize, subtree)
}
})
let locals = moveSpans(hasNulls ? withoutNulls(spans) : spans, -offset).sort(byPos)
for (let i = 0; i < locals.length; i++) if (!locals[i].type.valid(node, locals[i])) {
if (options.onRemove) options.onRemove(locals[i].spec)
locals.splice(i--, 1)
}
return locals.length || children.length ? new DecorationSet(locals, children) : empty
}
// : (Decoration, Decoration) → number
// Used to sort decorations so that ones with a low start position
// come first, and within a set with the same start position, those
// with an smaller end position come first.
function byPos(a, b) {
return a.from - b.from || a.to - b.to
}
// : ([Decoration]) → [Decoration]
// Scan a sorted array of decorations for partially overlapping spans,
// and split those so that only fully overlapping spans are left (to
// make subsequent rendering easier). Will return the input array if
// no partially overlapping spans are found (the common case).
function removeOverlap(spans) {
let working = spans
for (let i = 0; i < working.length - 1; i++) {
let span = working[i]
if (span.from != span.to) for (let j = i + 1; j < working.length; j++) {
let next = working[j]
if (next.from == span.from) {
if (next.to != span.to) {
if (working == spans) working = spans.slice()
// Followed by a partially overlapping larger span. Split that
// span.
working[j] = next.copy(next.from, span.to)
insertAhead(working, j + 1, next.copy(span.to, next.to))
}
continue
} else {
if (next.from < span.to) {
if (working == spans) working = spans.slice()
// The end of this one overlaps with a subsequent span. Split
// this one.
working[i] = span.copy(span.from, next.from)
insertAhead(working, j, span.copy(next.from, span.to))
}
break
}
}
}
return working
}
function insertAhead(array, i, deco) {
while (i < array.length && byPos(deco, array[i]) > 0) i++
array.splice(i, 0, deco)
}
// : (EditorView) → union
// Get the decorations associated with the current props of a view.
export function viewDecorations(view) {
let found = []
view.someProp("decorations", f => {
let result = f(view.state)
if (result && result != empty) found.push(result)
})
if (view.cursorWrapper)
found.push(DecorationSet.create(view.state.doc, [view.cursorWrapper.deco]))
return DecorationGroup.from(found)
}
prosemirror-view-1.23.13/src/dom.js 0000664 0000000 0000000 00000006136 14232025710 0017122 0 ustar 00root root 0000000 0000000 import browser from "./browser"
export const domIndex = function(node) {
for (var index = 0;; index++) {
node = node.previousSibling
if (!node) return index
}
}
export const parentNode = function(node) {
let parent = node.assignedSlot || node.parentNode
return parent && parent.nodeType == 11 ? parent.host : parent
}
let reusedRange = null
// Note that this will always return the same range, because DOM range
// objects are every expensive, and keep slowing down subsequent DOM
// updates, for some reason.
export const textRange = function(node, from, to) {
let range = reusedRange || (reusedRange = document.createRange())
range.setEnd(node, to == null ? node.nodeValue.length : to)
range.setStart(node, from || 0)
return range
}
// Scans forward and backward through DOM positions equivalent to the
// given one to see if the two are in the same place (i.e. after a
// text node vs at the end of that text node)
export const isEquivalentPosition = function(node, off, targetNode, targetOff) {
return targetNode && (scanFor(node, off, targetNode, targetOff, -1) ||
scanFor(node, off, targetNode, targetOff, 1))
}
const atomElements = /^(img|br|input|textarea|hr)$/i
function scanFor(node, off, targetNode, targetOff, dir) {
for (;;) {
if (node == targetNode && off == targetOff) return true
if (off == (dir < 0 ? 0 : nodeSize(node))) {
let parent = node.parentNode
if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) ||
node.contentEditable == "false")
return false
off = domIndex(node) + (dir < 0 ? 0 : 1)
node = parent
} else if (node.nodeType == 1) {
node = node.childNodes[off + (dir < 0 ? -1 : 0)]
if (node.contentEditable == "false") return false
off = dir < 0 ? nodeSize(node) : 0
} else {
return false
}
}
}
export function nodeSize(node) {
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length
}
export function isOnEdge(node, offset, parent) {
for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
if (node == parent) return true
let index = domIndex(node)
node = node.parentNode
if (!node) return false
atStart = atStart && index == 0
atEnd = atEnd && index == nodeSize(node)
}
}
function hasBlockDesc(dom) {
let desc
for (let cur = dom; cur; cur = cur.parentNode) if (desc = cur.pmViewDesc) break
return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom)
}
// Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
// (isCollapsed inappropriately returns true in shadow dom)
export const selectionCollapsed = function(domSel) {
let collapsed = domSel.isCollapsed
if (collapsed && browser.chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed)
collapsed = false
return collapsed
}
export function keyEvent(keyCode, key) {
let event = document.createEvent("Event")
event.initEvent("keydown", true, true)
event.keyCode = keyCode
event.key = event.code = key
return event
}
prosemirror-view-1.23.13/src/domchange.js 0000664 0000000 0000000 00000035447 14232025710 0020277 0 ustar 00root root 0000000 0000000 import {Fragment, DOMParser} from "prosemirror-model"
import {Selection, TextSelection} from "prosemirror-state"
import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection"
import {selectionCollapsed, keyEvent} from "./dom"
import browser from "./browser"
// Note that all referencing and parsing is done with the
// start-of-operation selection and document, since that's the one
// that the DOM represents. If any changes came in in the meantime,
// the modification is mapped over those before it is applied, in
// readDOMChange.
function parseBetween(view, from_, to_) {
let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
let domSel = view.root.getSelection(), find = null, anchor = domSel.anchorNode
if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) {
find = [{node: anchor, offset: domSel.anchorOffset}]
if (!selectionCollapsed(domSel))
find.push({node: domSel.focusNode, offset: domSel.focusOffset})
}
// Work around issue in Chrome where backspacing sometimes replaces
// the deleted content with a random BR node (issues #799, #831)
if (browser.chrome && view.lastKeyCode === 8) {
for (let off = toOffset; off > fromOffset; off--) {
let node = parent.childNodes[off - 1], desc = node.pmViewDesc
if (node.nodeName == "BR" && !desc) { toOffset = off; break }
if (!desc || desc.size) break
}
}
let startDoc = view.state.doc
let parser = view.someProp("domParser") || DOMParser.fromSchema(view.state.schema)
let $from = startDoc.resolve(from)
let sel = null, doc = parser.parse(parent, {
topNode: $from.parent,
topMatch: $from.parent.contentMatchAt($from.index()),
topOpen: true,
from: fromOffset,
to: toOffset,
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
editableContent: true,
findPositions: find,
ruleFromNode,
context: $from
})
if (find && find[0].pos != null) {
let anchor = find[0].pos, head = find[1] && find[1].pos
if (head == null) head = anchor
sel = {anchor: anchor + from, head: head + from}
}
return {doc, sel, from, to}
}
function ruleFromNode(dom) {
let desc = dom.pmViewDesc
if (desc) {
return desc.parseRule()
} else if (dom.nodeName == "BR" && dom.parentNode) {
// Safari replaces the list item or table cell with a BR
// directly in the list node (?!) if you delete the last
// character in a list item or table cell (#708, #862)
if (browser.safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
let skip = document.createElement("div")
skip.appendChild(document.createElement("li"))
return {skip}
} else if (dom.parentNode.lastChild == dom || browser.safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
return {ignore: true}
}
} else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
return {ignore: true}
}
}
export function readDOMChange(view, from, to, typeOver, addedNodes) {
if (from < 0) {
let origin = view.lastSelectionTime > Date.now() - 50 ? view.lastSelectionOrigin : null
let newSel = selectionFromDOM(view, origin)
if (newSel && !view.state.selection.eq(newSel)) {
let tr = view.state.tr.setSelection(newSel)
if (origin == "pointer") tr.setMeta("pointer", true)
else if (origin == "key") tr.scrollIntoView()
view.dispatch(tr)
}
return
}
let $before = view.state.doc.resolve(from)
let shared = $before.sharedDepth(to)
from = $before.before(shared + 1)
to = view.state.doc.resolve(to).after(shared + 1)
let sel = view.state.selection
let parse = parseBetween(view, from, to)
// Chrome sometimes leaves the cursor before the inserted text when
// composing after a cursor wrapper. This moves it forward.
if (browser.chrome && view.cursorWrapper && parse.sel && parse.sel.anchor == view.cursorWrapper.deco.from) {
let text = view.cursorWrapper.deco.type.toDOM.nextSibling
let size = text && text.nodeValue ? text.nodeValue.length : 1
parse.sel = {anchor: parse.sel.anchor + size, head: parse.sel.anchor + size}
}
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to)
let preferredPos, preferredSide
// Prefer anchoring to end when Backspace is pressed
if (view.lastKeyCode === 8 && Date.now() - 100 < view.lastKeyCodeTime) {
preferredPos = view.state.selection.to
preferredSide = "end"
} else {
preferredPos = view.state.selection.from
preferredSide = "start"
}
view.lastKeyCode = null
let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide)
if ((browser.ios && view.lastIOSEnter > Date.now() - 225 || browser.android) &&
addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P") &&
(!change || change.endA >= change.endB) &&
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
view.lastIOSEnter = 0
return
}
if (!change) {
if (typeOver && sel instanceof TextSelection && !sel.empty && sel.$head.sameParent(sel.$anchor) &&
!view.composing && !(parse.sel && parse.sel.anchor != parse.sel.head)) {
change = {start: sel.from, endA: sel.to, endB: sel.to}
} else {
if (parse.sel) {
let sel = resolveSelection(view, view.state.doc, parse.sel)
if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel))
}
return
}
}
view.domChangeCount++
// Handle the case where overwriting a selection by typing matches
// the start or end of the selected content, creating a change
// that's smaller than what was actually overwritten.
if (view.state.selection.from < view.state.selection.to &&
change.start == change.endB &&
view.state.selection instanceof TextSelection) {
if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 &&
view.state.selection.from >= parse.from) {
change.start = view.state.selection.from
} else if (change.endA < view.state.selection.to && change.endA >= view.state.selection.to - 2 &&
view.state.selection.to <= parse.to) {
change.endB += (view.state.selection.to - change.endA)
change.endA = view.state.selection.to
}
}
// IE11 will insert a non-breaking space _ahead_ of the space after
// the cursor space when adding a space before another space. When
// that happened, adjust the change to cover the space instead.
if (browser.ie && browser.ie_version <= 11 && change.endB == change.start + 1 &&
change.endA == change.start && change.start > parse.from &&
parse.doc.textBetween(change.start - parse.from - 1, change.start - parse.from + 1) == " \u00a0") {
change.start--
change.endA--
change.endB--
}
let $from = parse.doc.resolveNoCache(change.start - parse.from)
let $to = parse.doc.resolveNoCache(change.endB - parse.from)
let inlineChange = $from.sameParent($to) && $from.parent.inlineContent
let nextSel
// If this looks like the effect of pressing Enter (or was recorded
// as being an iOS enter press), just dispatch an Enter key instead.
if (((browser.ios && view.lastIOSEnter > Date.now() - 225 &&
(!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
(!inlineChange && $from.pos < parse.doc.content.size &&
(nextSel = Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) &&
nextSel.head == $to.pos)) &&
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
view.lastIOSEnter = 0
return
}
// Same for backspace
if (view.state.selection.anchor > change.start &&
looksLikeJoin(doc, change.start, change.endA, $from, $to) &&
view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) {
if (browser.android && browser.chrome) view.domObserver.suppressSelectionUpdates() // #820
return
}
// Chrome Android will occasionally, during composition, delete the
// entire composition and then immediately insert it again. This is
// used to detect that situation.
if (browser.chrome && browser.android && change.toB == change.from)
view.lastAndroidDelete = Date.now()
// This tries to detect Android virtual keyboard
// enter-and-pick-suggestion action. That sometimes (see issue
// #1059) first fires a DOM mutation, before moving the selection to
// the newly created block. And then, because ProseMirror cleans up
// the DOM selection, it gives up moving the selection entirely,
// leaving the cursor in the wrong place. When that happens, we drop
// the new paragraph from the initial change, and fire a simulated
// enter key afterwards.
if (browser.android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth &&
parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) {
change.endB -= 2
$to = parse.doc.resolveNoCache(change.endB - parse.from)
setTimeout(() => {
view.someProp("handleKeyDown", function (f) { return f(view, keyEvent(13, "Enter")); })
}, 20)
}
let chFrom = change.start, chTo = change.endA
let tr, storedMarks, markChange, $from1
if (inlineChange) {
if ($from.pos == $to.pos) { // Deletion
// IE11 sometimes weirdly moves the DOM selection around after
// backspacing out the first element in a textblock
if (browser.ie && browser.ie_version <= 11 && $from.parentOffset == 0) {
view.domObserver.suppressSelectionUpdates()
setTimeout(() => selectionToDOM(view), 20)
}
tr = view.state.tr.delete(chFrom, chTo)
storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA))
} else if ( // Adding or removing a mark
change.endA == change.endB && ($from1 = doc.resolve(change.start)) &&
(markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset),
$from1.parent.content.cut($from1.parentOffset, change.endA - $from1.start())))
) {
tr = view.state.tr
if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark)
else tr.removeMark(chFrom, chTo, markChange.mark)
} else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
// Both positions in the same text node -- simply insert text
let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset)
if (view.someProp("handleTextInput", f => f(view, chFrom, chTo, text))) return
tr = view.state.tr.insertText(text, chFrom, chTo)
}
}
if (!tr)
tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from))
if (parse.sel) {
let sel = resolveSelection(view, tr.doc, parse.sel)
// Chrome Android will sometimes, during composition, report the
// selection in the wrong place. If it looks like that is
// happening, don't update the selection.
// Edge just doesn't move the cursor forward when you start typing
// in an empty block or between br nodes.
if (sel && !(browser.chrome && browser.android && view.composing && sel.empty &&
(change.start != change.endB || view.lastAndroidDelete < Date.now() - 100) &&
(sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
browser.ie && sel.empty && sel.head == chFrom))
tr.setSelection(sel)
}
if (storedMarks) tr.ensureMarks(storedMarks)
view.dispatch(tr.scrollIntoView())
}
function resolveSelection(view, doc, parsedSel) {
if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null
return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head))
}
// : (Fragment, Fragment) → ?{mark: Mark, type: string}
// Given two same-length, non-empty fragments of inline content,
// determine whether the first could be created from the second by
// removing or adding a single mark type.
function isMarkChange(cur, prev) {
let curMarks = cur.firstChild.marks, prevMarks = prev.firstChild.marks
let added = curMarks, removed = prevMarks, type, mark, update
for (let i = 0; i < prevMarks.length; i++) added = prevMarks[i].removeFromSet(added)
for (let i = 0; i < curMarks.length; i++) removed = curMarks[i].removeFromSet(removed)
if (added.length == 1 && removed.length == 0) {
mark = added[0]
type = "add"
update = node => node.mark(mark.addToSet(node.marks))
} else if (added.length == 0 && removed.length == 1) {
mark = removed[0]
type = "remove"
update = node => node.mark(mark.removeFromSet(node.marks))
} else {
return null
}
let updated = []
for (let i = 0; i < prev.childCount; i++) updated.push(update(prev.child(i)))
if (Fragment.from(updated).eq(cur)) return {mark, type}
}
function looksLikeJoin(old, start, end, $newStart, $newEnd) {
if (!$newStart.parent.isTextblock ||
// The content must have shrunk
end - start <= $newEnd.pos - $newStart.pos ||
// newEnd must point directly at or after the end of the block that newStart points into
skipClosingAndOpening($newStart, true, false) < $newEnd.pos)
return false
let $start = old.resolve(start)
// Start must be at the end of a block
if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock)
return false
let $next = old.resolve(skipClosingAndOpening($start, true, true))
// The next textblock must start before end and end near it
if (!$next.parent.isTextblock || $next.pos > end ||
skipClosingAndOpening($next, true, false) < end)
return false
// The fragments after the join point must match
return $newStart.parent.content.cut($newStart.parentOffset).eq($next.parent.content)
}
function skipClosingAndOpening($pos, fromEnd, mayOpen) {
let depth = $pos.depth, end = fromEnd ? $pos.end() : $pos.pos
while (depth > 0 && (fromEnd || $pos.indexAfter(depth) == $pos.node(depth).childCount)) {
depth--
end++
fromEnd = false
}
if (mayOpen) {
let next = $pos.node(depth).maybeChild($pos.indexAfter(depth))
while (next && !next.isLeaf) {
next = next.firstChild
end++
}
}
return end
}
function findDiff(a, b, pos, preferredPos, preferredSide) {
let start = a.findDiffStart(b, pos)
if (start == null) return null
let {a: endA, b: endB} = a.findDiffEnd(b, pos + a.size, pos + b.size)
if (preferredSide == "end") {
let adjust = Math.max(0, start - Math.min(endA, endB))
preferredPos -= endA + adjust - start
}
if (endA < start && a.size < b.size) {
let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0
start -= move
endB = start + (endB - endA)
endA = start
} else if (endB < start) {
let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0
start -= move
endA = start + (endA - endB)
endB = start
}
return {start, endA, endB}
}
prosemirror-view-1.23.13/src/domcoords.js 0000664 0000000 0000000 00000045267 14232025710 0020344 0 ustar 00root root 0000000 0000000 import {nodeSize, textRange, parentNode} from "./dom"
import browser from "./browser"
function windowRect(doc) {
return {left: 0, right: doc.documentElement.clientWidth,
top: 0, bottom: doc.documentElement.clientHeight}
}
function getSide(value, side) {
return typeof value == "number" ? value : value[side]
}
function clientRect(node) {
let rect = node.getBoundingClientRect()
// Adjust for elements with style "transform: scale()"
let scaleX = (rect.width / node.offsetWidth) || 1
let scaleY = (rect.height / node.offsetHeight) || 1
// Make sure scrollbar width isn't included in the rectangle
return {left: rect.left, right: rect.left + node.clientWidth * scaleX,
top: rect.top, bottom: rect.top + node.clientHeight * scaleY}
}
export function scrollRectIntoView(view, rect, startDOM) {
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5
let doc = view.dom.ownerDocument
for (let parent = startDOM || view.dom;; parent = parentNode(parent)) {
if (!parent) break
if (parent.nodeType != 1) continue
let atTop = parent == doc.body || parent.nodeType != 1
let bounding = atTop ? windowRect(doc) : clientRect(parent)
let moveX = 0, moveY = 0
if (rect.top < bounding.top + getSide(scrollThreshold, "top"))
moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top"))
else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom"))
moveY = rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom")
if (rect.left < bounding.left + getSide(scrollThreshold, "left"))
moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left"))
else if (rect.right > bounding.right - getSide(scrollThreshold, "right"))
moveX = rect.right - bounding.right + getSide(scrollMargin, "right")
if (moveX || moveY) {
if (atTop) {
doc.defaultView.scrollBy(moveX, moveY)
} else {
let startX = parent.scrollLeft, startY = parent.scrollTop
if (moveY) parent.scrollTop += moveY
if (moveX) parent.scrollLeft += moveX
let dX = parent.scrollLeft - startX, dY = parent.scrollTop - startY
rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY}
}
}
if (atTop) break
}
}
// Store the scroll position of the editor's parent nodes, along with
// the top position of an element near the top of the editor, which
// will be used to make sure the visible viewport remains stable even
// when the size of the content above changes.
export function storeScrollPos(view) {
let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top)
let refDOM, refTop
for (let x = (rect.left + rect.right) / 2, y = startY + 1;
y < Math.min(innerHeight, rect.bottom); y += 5) {
let dom = view.root.elementFromPoint(x, y)
if (dom == view.dom || !view.dom.contains(dom)) continue
let localRect = dom.getBoundingClientRect()
if (localRect.top >= startY - 20) {
refDOM = dom
refTop = localRect.top
break
}
}
return {refDOM, refTop, stack: scrollStack(view.dom)}
}
function scrollStack(dom) {
let stack = [], doc = dom.ownerDocument
for (; dom; dom = parentNode(dom)) {
stack.push({dom, top: dom.scrollTop, left: dom.scrollLeft})
if (dom == doc) break
}
return stack
}
// Reset the scroll position of the editor's parent nodes to that what
// it was before, when storeScrollPos was called.
export function resetScrollPos({refDOM, refTop, stack}) {
let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0
restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop)
}
function restoreScrollStack(stack, dTop) {
for (let i = 0; i < stack.length; i++) {
let {dom, top, left} = stack[i]
if (dom.scrollTop != top + dTop) dom.scrollTop = top + dTop
if (dom.scrollLeft != left) dom.scrollLeft = left
}
}
let preventScrollSupported = null
// Feature-detects support for .focus({preventScroll: true}), and uses
// a fallback kludge when not supported.
export function focusPreventScroll(dom) {
if (dom.setActive) return dom.setActive() // in IE
if (preventScrollSupported) return dom.focus(preventScrollSupported)
let stored = scrollStack(dom)
dom.focus(preventScrollSupported == null ? {
get preventScroll() {
preventScrollSupported = {preventScroll: true}
return true
}
} : undefined)
if (!preventScrollSupported) {
preventScrollSupported = false
restoreScrollStack(stored, 0)
}
}
function findOffsetInNode(node, coords) {
let closest, dxClosest = 2e8, coordsClosest, offset = 0
let rowBot = coords.top, rowTop = coords.top
for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) {
let rects
if (child.nodeType == 1) rects = child.getClientRects()
else if (child.nodeType == 3) rects = textRange(child).getClientRects()
else continue
for (let i = 0; i < rects.length; i++) {
let rect = rects[i]
if (rect.top <= rowBot && rect.bottom >= rowTop) {
rowBot = Math.max(rect.bottom, rowBot)
rowTop = Math.min(rect.top, rowTop)
let dx = rect.left > coords.left ? rect.left - coords.left
: rect.right < coords.left ? coords.left - rect.right : 0
if (dx < dxClosest) {
closest = child
dxClosest = dx
coordsClosest = dx && closest.nodeType == 3 ? {left: rect.right < coords.left ? rect.right : rect.left, top: coords.top} : coords
if (child.nodeType == 1 && dx)
offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)
continue
}
}
if (!closest && (coords.left >= rect.right && coords.top >= rect.top ||
coords.left >= rect.left && coords.top >= rect.bottom))
offset = childIndex + 1
}
}
if (closest && closest.nodeType == 3) return findOffsetInText(closest, coordsClosest)
if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset}
return findOffsetInNode(closest, coordsClosest)
}
function findOffsetInText(node, coords) {
let len = node.nodeValue.length
let range = document.createRange()
for (let i = 0; i < len; i++) {
range.setEnd(node, i + 1)
range.setStart(node, i)
let rect = singleRect(range, 1)
if (rect.top == rect.bottom) continue
if (inRect(coords, rect))
return {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)}
}
return {node, offset: 0}
}
function inRect(coords, rect) {
return coords.left >= rect.left - 1 && coords.left <= rect.right + 1&&
coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1
}
function targetKludge(dom, coords) {
let parent = dom.parentNode
if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left)
return parent
return dom
}
function posFromElement(view, elt, coords) {
let {node, offset} = findOffsetInNode(elt, coords), bias = -1
if (node.nodeType == 1 && !node.firstChild) {
let rect = node.getBoundingClientRect()
bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1
}
return view.docView.posFromDOM(node, offset, bias)
}
function posFromCaret(view, node, offset, coords) {
// Browser (in caretPosition/RangeFromPoint) will agressively
// normalize towards nearby inline nodes. Since we are interested in
// positions between block nodes too, we first walk up the hierarchy
// of nodes to see if there are block nodes that the coordinates
// fall outside of. If so, we take the position before/after that
// block. If not, we call `posFromDOM` on the raw node/offset.
let outside = -1
for (let cur = node;;) {
if (cur == view.dom) break
let desc = view.docView.nearestDesc(cur, true)
if (!desc) return null
if (desc.node.isBlock && desc.parent) {
let rect = desc.dom.getBoundingClientRect()
if (rect.left > coords.left || rect.top > coords.top) outside = desc.posBefore
else if (rect.right < coords.left || rect.bottom < coords.top) outside = desc.posAfter
else break
}
cur = desc.dom.parentNode
}
return outside > -1 ? outside : view.docView.posFromDOM(node, offset)
}
function elementFromPoint(element, coords, box) {
let len = element.childNodes.length
if (len && box.top < box.bottom) {
for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) {
let child = element.childNodes[i]
if (child.nodeType == 1) {
let rects = child.getClientRects()
for (let j = 0; j < rects.length; j++) {
let rect = rects[j]
if (inRect(coords, rect)) return elementFromPoint(child, coords, rect)
}
}
if ((i = (i + 1) % len) == startI) break
}
}
return element
}
// Given an x,y position on the editor, get the position in the document.
export function posAtCoords(view, coords) {
let doc = view.dom.ownerDocument, node, offset
if (doc.caretPositionFromPoint) {
try { // Firefox throws for this call in hard-to-predict circumstances (#994)
let pos = doc.caretPositionFromPoint(coords.left, coords.top)
if (pos) ({offsetNode: node, offset} = pos)
} catch (_) {}
}
if (!node && doc.caretRangeFromPoint) {
let range = doc.caretRangeFromPoint(coords.left, coords.top)
if (range) ({startContainer: node, startOffset: offset} = range)
}
let elt = (view.root.elementFromPoint ? view.root : doc).elementFromPoint(coords.left, coords.top + 1), pos
if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) {
let box = view.dom.getBoundingClientRect()
if (!inRect(coords, box)) return null
elt = elementFromPoint(view.dom, coords, box)
if (!elt) return null
}
// Safari's caretRangeFromPoint returns nonsense when on a draggable element
if (browser.safari) {
for (let p = elt; node && p; p = parentNode(p))
if (p.draggable) node = offset = null
}
elt = targetKludge(elt, coords)
if (node) {
if (browser.gecko && node.nodeType == 1) {
// Firefox will sometimes return offsets into nodes, which
// have no actual children, from caretPositionFromPoint (#953)
offset = Math.min(offset, node.childNodes.length)
// It'll also move the returned position before image nodes,
// even if those are behind it.
if (offset < node.childNodes.length) {
let next = node.childNodes[offset], box
if (next.nodeName == "IMG" && (box = next.getBoundingClientRect()).right <= coords.left &&
box.bottom > coords.top)
offset++
}
}
// Suspiciously specific kludge to work around caret*FromPoint
// never returning a position at the end of the document
if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild.nodeType == 1 &&
coords.top > node.lastChild.getBoundingClientRect().bottom)
pos = view.state.doc.content.size
// Ignore positions directly after a BR, since caret*FromPoint
// 'round up' positions that would be more accurately placed
// before the BR node.
else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR")
pos = posFromCaret(view, node, offset, coords)
}
if (pos == null) pos = posFromElement(view, elt, coords)
let desc = view.docView.nearestDesc(elt, true)
return {pos, inside: desc ? desc.posAtStart - desc.border : -1}
}
function singleRect(object, bias) {
let rects = object.getClientRects()
return !rects.length ? object.getBoundingClientRect() : rects[bias < 0 ? 0 : rects.length - 1]
}
const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/
// : (EditorView, number, number) → {left: number, top: number, right: number, bottom: number}
// Given a position in the document model, get a bounding box of the
// character at that position, relative to the window.
export function coordsAtPos(view, pos, side) {
let {node, offset} = view.docView.domFromPos(pos, side < 0 ? -1 : 1)
let supportEmptyRange = browser.webkit || browser.gecko
if (node.nodeType == 3) {
// These browsers support querying empty text ranges. Prefer that in
// bidi context or when at the end of a node.
if (supportEmptyRange && (BIDI.test(node.nodeValue) || (side < 0 ? !offset : offset == node.nodeValue.length))) {
let rect = singleRect(textRange(node, offset, offset), side)
// Firefox returns bad results (the position before the space)
// when querying a position directly after line-broken
// whitespace. Detect this situation and and kludge around it
if (browser.gecko && offset && /\s/.test(node.nodeValue[offset - 1]) && offset < node.nodeValue.length) {
let rectBefore = singleRect(textRange(node, offset - 1, offset - 1), -1)
if (rectBefore.top == rect.top) {
let rectAfter = singleRect(textRange(node, offset, offset + 1), -1)
if (rectAfter.top != rect.top)
return flattenV(rectAfter, rectAfter.left < rectBefore.left)
}
}
return rect
} else {
let from = offset, to = offset, takeSide = side < 0 ? 1 : -1
if (side < 0 && !offset) { to++; takeSide = -1 }
else if (side >= 0 && offset == node.nodeValue.length) { from--; takeSide = 1 }
else if (side < 0) { from-- }
else { to ++ }
return flattenV(singleRect(textRange(node, from, to), takeSide), takeSide < 0)
}
}
// Return a horizontal line in block context
if (!view.state.doc.resolve(pos).parent.inlineContent) {
if (offset && (side < 0 || offset == nodeSize(node))) {
let before = node.childNodes[offset - 1]
if (before.nodeType == 1) return flattenH(before.getBoundingClientRect(), false)
}
if (offset < nodeSize(node)) {
let after = node.childNodes[offset]
if (after.nodeType == 1) return flattenH(after.getBoundingClientRect(), true)
}
return flattenH(node.getBoundingClientRect(), side >= 0)
}
// Inline, not in text node (this is not Bidi-safe)
if (offset && (side < 0 || offset == nodeSize(node))) {
let before = node.childNodes[offset - 1]
let target = before.nodeType == 3 ? textRange(before, nodeSize(before) - (supportEmptyRange ? 0 : 1))
// BR nodes tend to only return the rectangle before them.
// Only use them if they are the last element in their parent
: before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null
if (target) return flattenV(singleRect(target, 1), false)
}
if (offset < nodeSize(node)) {
let after = node.childNodes[offset]
while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords) after = after.nextSibling
let target = !after ? null : after.nodeType == 3 ? textRange(after, 0, (supportEmptyRange ? 0 : 1))
: after.nodeType == 1 ? after : null
if (target) return flattenV(singleRect(target, -1), true)
}
// All else failed, just try to get a rectangle for the target node
return flattenV(singleRect(node.nodeType == 3 ? textRange(node) : node, -side), side >= 0)
}
function flattenV(rect, left) {
if (rect.width == 0) return rect
let x = left ? rect.left : rect.right
return {top: rect.top, bottom: rect.bottom, left: x, right: x}
}
function flattenH(rect, top) {
if (rect.height == 0) return rect
let y = top ? rect.top : rect.bottom
return {top: y, bottom: y, left: rect.left, right: rect.right}
}
function withFlushedState(view, state, f) {
let viewState = view.state, active = view.root.activeElement
if (viewState != state) view.updateState(state)
if (active != view.dom) view.focus()
try {
return f()
} finally {
if (viewState != state) view.updateState(viewState)
if (active != view.dom && active) active.focus()
}
}
// : (EditorView, number, number)
// Whether vertical position motion in a given direction
// from a position would leave a text block.
function endOfTextblockVertical(view, state, dir) {
let sel = state.selection
let $pos = dir == "up" ? sel.$from : sel.$to
return withFlushedState(view, state, () => {
let {node: dom} = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1)
for (;;) {
let nearest = view.docView.nearestDesc(dom, true)
if (!nearest) break
if (nearest.node.isBlock) { dom = nearest.dom; break }
dom = nearest.dom.parentNode
}
let coords = coordsAtPos(view, $pos.pos, 1)
for (let child = dom.firstChild; child; child = child.nextSibling) {
let boxes
if (child.nodeType == 1) boxes = child.getClientRects()
else if (child.nodeType == 3) boxes = textRange(child, 0, child.nodeValue.length).getClientRects()
else continue
for (let i = 0; i < boxes.length; i++) {
let box = boxes[i]
if (box.bottom > box.top + 1 &&
(dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2
: box.bottom - coords.bottom > (coords.bottom - box.top) * 2))
return false
}
}
return true
})
}
const maybeRTL = /[\u0590-\u08ac]/
function endOfTextblockHorizontal(view, state, dir) {
let {$head} = state.selection
if (!$head.parent.isTextblock) return false
let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size
let sel = view.root.getSelection()
// If the textblock is all LTR, or the browser doesn't support
// Selection.modify (Edge), fall back to a primitive approach
if (!maybeRTL.test($head.parent.textContent) || !sel.modify)
return dir == "left" || dir == "backward" ? atStart : atEnd
return withFlushedState(view, state, () => {
// This is a huge hack, but appears to be the best we can
// currently do: use `Selection.modify` to move the selection by
// one character, and see if that moves the cursor out of the
// textblock (or doesn't move it at all, when at the start/end of
// the document).
let oldRange = sel.getRangeAt(0), oldNode = sel.focusNode, oldOff = sel.focusOffset
let oldBidiLevel = sel.caretBidiLevel // Only for Firefox
sel.modify("move", dir, "character")
let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom
let result = !parentDOM.contains(sel.focusNode.nodeType == 1 ? sel.focusNode : sel.focusNode.parentNode) ||
(oldNode == sel.focusNode && oldOff == sel.focusOffset)
// Restore the previous selection
sel.removeAllRanges()
sel.addRange(oldRange)
if (oldBidiLevel != null) sel.caretBidiLevel = oldBidiLevel
return result
})
}
let cachedState = null, cachedDir = null, cachedResult = false
export function endOfTextblock(view, state, dir) {
if (cachedState == state && cachedDir == dir) return cachedResult
cachedState = state; cachedDir = dir
return cachedResult = dir == "up" || dir == "down"
? endOfTextblockVertical(view, state, dir)
: endOfTextblockHorizontal(view, state, dir)
}
prosemirror-view-1.23.13/src/domobserver.js 0000664 0000000 0000000 00000021401 14232025710 0020662 0 ustar 00root root 0000000 0000000 import browser from "./browser"
import {domIndex, isEquivalentPosition} from "./dom"
import {hasFocusAndSelection, selectionToDOM} from "./selection"
const observeOptions = {
childList: true,
characterData: true,
characterDataOldValue: true,
attributes: true,
attributeOldValue: true,
subtree: true
}
// IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified
const useCharData = browser.ie && browser.ie_version <= 11
class SelectionState {
constructor() {
this.anchorNode = this.anchorOffset = this.focusNode = this.focusOffset = null
}
set(sel) {
this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
}
eq(sel) {
return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
}
}
export class DOMObserver {
constructor(view, handleDOMChange) {
this.view = view
this.handleDOMChange = handleDOMChange
this.queue = []
this.flushingSoon = -1
this.observer = window.MutationObserver &&
new window.MutationObserver(mutations => {
for (let i = 0; i < mutations.length; i++) this.queue.push(mutations[i])
// IE11 will sometimes (on backspacing out a single character
// text node after a BR node) call the observer callback
// before actually updating the DOM, which will cause
// ProseMirror to miss the change (see #930)
if (browser.ie && browser.ie_version <= 11 && mutations.some(
m => m.type == "childList" && m.removedNodes.length ||
m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length))
this.flushSoon()
else
this.flush()
})
this.currentSelection = new SelectionState
if (useCharData) {
this.onCharData = e => {
this.queue.push({target: e.target, type: "characterData", oldValue: e.prevValue})
this.flushSoon()
}
}
this.onSelectionChange = this.onSelectionChange.bind(this)
this.suppressingSelectionUpdates = false
}
flushSoon() {
if (this.flushingSoon < 0)
this.flushingSoon = window.setTimeout(() => { this.flushingSoon = -1; this.flush() }, 20)
}
forceFlush() {
if (this.flushingSoon > -1) {
window.clearTimeout(this.flushingSoon)
this.flushingSoon = -1
this.flush()
}
}
start() {
if (this.observer)
this.observer.observe(this.view.dom, observeOptions)
if (useCharData)
this.view.dom.addEventListener("DOMCharacterDataModified", this.onCharData)
this.connectSelection()
}
stop() {
if (this.observer) {
let take = this.observer.takeRecords()
if (take.length) {
for (let i = 0; i < take.length; i++) this.queue.push(take[i])
window.setTimeout(() => this.flush(), 20)
}
this.observer.disconnect()
}
if (useCharData) this.view.dom.removeEventListener("DOMCharacterDataModified", this.onCharData)
this.disconnectSelection()
}
connectSelection() {
this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange)
}
disconnectSelection() {
this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange)
}
suppressSelectionUpdates() {
this.suppressingSelectionUpdates = true
setTimeout(() => this.suppressingSelectionUpdates = false, 50)
}
onSelectionChange() {
if (!hasFocusAndSelection(this.view)) return
if (this.suppressingSelectionUpdates) return selectionToDOM(this.view)
// Deletions on IE11 fire their events in the wrong order, giving
// us a selection change event before the DOM changes are
// reported.
if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) {
let sel = this.view.root.getSelection()
// Selection.isCollapsed isn't reliable on IE
if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
return this.flushSoon()
}
this.flush()
}
setCurSelection() {
this.currentSelection.set(this.view.root.getSelection())
}
ignoreSelectionChange(sel) {
if (sel.rangeCount == 0) return true
let container = sel.getRangeAt(0).commonAncestorContainer
let desc = this.view.docView.nearestDesc(container)
if (desc && desc.ignoreMutation({type: "selection", target: container.nodeType == 3 ? container.parentNode : container})) {
this.setCurSelection()
return true
}
}
flush() {
if (!this.view.docView || this.flushingSoon > -1) return
let mutations = this.observer ? this.observer.takeRecords() : []
if (this.queue.length) {
mutations = this.queue.concat(mutations)
this.queue.length = 0
}
let sel = this.view.root.getSelection()
let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel)
let from = -1, to = -1, typeOver = false, added = []
if (this.view.editable) {
for (let i = 0; i < mutations.length; i++) {
let result = this.registerMutation(mutations[i], added)
if (result) {
from = from < 0 ? result.from : Math.min(result.from, from)
to = to < 0 ? result.to : Math.max(result.to, to)
if (result.typeOver) typeOver = true
}
}
}
if (browser.gecko && added.length > 1) {
let brs = added.filter(n => n.nodeName == "BR")
if (brs.length == 2) {
let [a, b] = brs
if (a.parentNode && a.parentNode.parentNode == b.parentNode) b.remove()
else a.remove()
}
}
if (from > -1 || newSel) {
if (from > -1) {
this.view.docView.markDirty(from, to)
checkCSS(this.view)
}
this.handleDOMChange(from, to, typeOver, added)
if (this.view.docView && this.view.docView.dirty) this.view.updateState(this.view.state)
else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view)
this.currentSelection.set(sel)
}
}
registerMutation(mut, added) {
// Ignore mutations inside nodes that were already noted as inserted
if (added.indexOf(mut.target) > -1) return null
let desc = this.view.docView.nearestDesc(mut.target)
if (mut.type == "attributes" &&
(desc == this.view.docView || mut.attributeName == "contenteditable" ||
// Firefox sometimes fires spurious events for null/empty styles
(mut.attributeName == "style" && !mut.oldValue && !mut.target.getAttribute("style"))))
return null
if (!desc || desc.ignoreMutation(mut)) return null
if (mut.type == "childList") {
for (let i = 0; i < mut.addedNodes.length; i++) added.push(mut.addedNodes[i])
if (desc.contentDOM && desc.contentDOM != desc.dom && !desc.contentDOM.contains(mut.target))
return {from: desc.posBefore, to: desc.posAfter}
let prev = mut.previousSibling, next = mut.nextSibling
if (browser.ie && browser.ie_version <= 11 && mut.addedNodes.length) {
// IE11 gives us incorrect next/prev siblings for some
// insertions, so if there are added nodes, recompute those
for (let i = 0; i < mut.addedNodes.length; i++) {
let {previousSibling, nextSibling} = mut.addedNodes[i]
if (!previousSibling || Array.prototype.indexOf.call(mut.addedNodes, previousSibling) < 0) prev = previousSibling
if (!nextSibling || Array.prototype.indexOf.call(mut.addedNodes, nextSibling) < 0) next = nextSibling
}
}
let fromOffset = prev && prev.parentNode == mut.target
? domIndex(prev) + 1 : 0
let from = desc.localPosFromDOM(mut.target, fromOffset, -1)
let toOffset = next && next.parentNode == mut.target
? domIndex(next) : mut.target.childNodes.length
let to = desc.localPosFromDOM(mut.target, toOffset, 1)
return {from, to}
} else if (mut.type == "attributes") {
return {from: desc.posAtStart - desc.border, to: desc.posAtEnd + desc.border}
} else { // "characterData"
return {
from: desc.posAtStart,
to: desc.posAtEnd,
// An event was generated for a text change that didn't change
// any text. Mark the dom change to fall back to assuming the
// selection was typed over with an identical value if it can't
// find another change.
typeOver: mut.target.nodeValue == mut.oldValue
}
}
}
}
let cssChecked = false
function checkCSS(view) {
if (cssChecked) return
cssChecked = true
if (getComputedStyle(view.dom).whiteSpace == "normal")
console["warn"]("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.")
}
prosemirror-view-1.23.13/src/index.js 0000664 0000000 0000000 00000066553 14232025710 0017463 0 ustar 00root root 0000000 0000000 import {NodeSelection} from "prosemirror-state"
import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos,
resetScrollPos, focusPreventScroll} from "./domcoords"
import {docViewDesc} from "./viewdesc"
import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition} from "./input"
import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
import {Decoration, viewDecorations} from "./decoration"
import browser from "./browser"
export {Decoration, DecorationSet} from "./decoration"
// Exported for testing
export {serializeForClipboard as __serializeForClipboard, parseFromClipboard as __parseFromClipboard} from "./clipboard"
export {endComposition as __endComposition} from "./input"
// ::- An editor view manages the DOM structure that represents an
// editable document. Its state and behavior are determined by its
// [props](#view.DirectEditorProps).
export class EditorView {
// :: (?union, DirectEditorProps)
// Create a view. `place` may be a DOM node that the editor should
// be appended to, a function that will place it into the document,
// or an object whose `mount` property holds the node to use as the
// document container. If it is `null`, the editor will not be added
// to the document.
constructor(place, props) {
this._props = props
// :: EditorState
// The view's current [state](#state.EditorState).
this.state = props.state
this.directPlugins = props.plugins || []
this.directPlugins.forEach(checkStateComponent)
this.dispatch = this.dispatch.bind(this)
this._root = null
this.focused = false
// Kludge used to work around a Chrome bug
this.trackWrites = null
// :: dom.Element
// An editable DOM node containing the document. (You probably
// should not directly interfere with its content.)
this.dom = (place && place.mount) || document.createElement("div")
if (place) {
if (place.appendChild) place.appendChild(this.dom)
else if (place.apply) place(this.dom)
else if (place.mount) this.mounted = true
}
// :: bool
// Indicates whether the editor is currently [editable](#view.EditorProps.editable).
this.editable = getEditable(this)
this.markCursor = null
this.cursorWrapper = null
updateCursorWrapper(this)
this.nodeViews = buildNodeViews(this)
this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this)
this.lastSelectedViewDesc = null
// :: ?{slice: Slice, move: bool}
// When editor content is being dragged, this object contains
// information about the dragged slice and whether it is being
// copied or moved. At any other time, it is null.
this.dragging = null
initInput(this)
this.prevDirectPlugins = []
this.pluginViews = []
this.updatePluginViews()
}
// composing:: boolean
// Holds `true` when a
// [composition](https://w3c.github.io/uievents/#events-compositionevents)
// is active.
// :: DirectEditorProps
// The view's current [props](#view.EditorProps).
get props() {
if (this._props.state != this.state) {
let prev = this._props
this._props = {}
for (let name in prev) this._props[name] = prev[name]
this._props.state = this.state
}
return this._props
}
// :: (DirectEditorProps)
// Update the view's props. Will immediately cause an update to
// the DOM.
update(props) {
if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this)
this._props = props
if (props.plugins) {
props.plugins.forEach(checkStateComponent)
this.directPlugins = props.plugins
}
this.updateStateInner(props.state, true)
}
// :: (DirectEditorProps)
// Update the view by updating existing props object with the object
// given as argument. Equivalent to `view.update(Object.assign({},
// view.props, props))`.
setProps(props) {
let updated = {}
for (let name in this._props) updated[name] = this._props[name]
updated.state = this.state
for (let name in props) updated[name] = props[name]
this.update(updated)
}
// :: (EditorState)
// Update the editor's `state` prop, without touching any of the
// other props.
updateState(state) {
this.updateStateInner(state, this.state.plugins != state.plugins)
}
updateStateInner(state, reconfigured) {
let prev = this.state, redraw = false, updateSel = false
// When stored marks are added, stop composition, so that they can
// be displayed.
if (state.storedMarks && this.composing) {
clearComposition(this)
updateSel = true
}
this.state = state
if (reconfigured) {
let nodeViews = buildNodeViews(this)
if (changedNodeViews(nodeViews, this.nodeViews)) {
this.nodeViews = nodeViews
redraw = true
}
ensureListeners(this)
}
this.editable = getEditable(this)
updateCursorWrapper(this)
let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this)
let scroll = reconfigured ? "reset"
: state.scrollToSelection > prev.scrollToSelection ? "to selection" : "preserve"
let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco)
if (updateDoc || !state.selection.eq(prev.selection)) updateSel = true
let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this)
if (updateSel) {
this.domObserver.stop()
// Work around an issue in Chrome, IE, and Edge where changing
// the DOM around an active selection puts it into a broken
// state where the thing the user sees differs from the
// selection reported by the Selection object (#710, #973,
// #1011, #1013, #1035).
let forceSelUpdate = updateDoc && (browser.ie || browser.chrome) && !this.composing &&
!prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection)
if (updateDoc) {
// If the node that the selection points into is written to,
// Chrome sometimes starts misreporting the selection, so this
// tracks that and forces a selection reset when our update
// did write to the node.
let chromeKludge = browser.chrome ? (this.trackWrites = this.root.getSelection().focusNode) : null
if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
this.docView.updateOuterDeco([])
this.docView.destroy()
this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this)
}
if (chromeKludge && !this.trackWrites) forceSelUpdate = true
}
// Work around for an issue where an update arriving right between
// a DOM selection change and the "selectionchange" event for it
// can cause a spurious DOM selection update, disrupting mouse
// drag selection.
if (forceSelUpdate ||
!(this.mouseDown && this.domObserver.currentSelection.eq(this.root.getSelection()) && anchorInRightPlace(this))) {
selectionToDOM(this, forceSelUpdate)
} else {
syncNodeSelection(this, state.selection)
this.domObserver.setCurSelection()
}
this.domObserver.start()
}
this.updatePluginViews(prev)
if (scroll == "reset") {
this.dom.scrollTop = 0
} else if (scroll == "to selection") {
let startDOM = this.root.getSelection().focusNode
if (this.someProp("handleScrollToSelection", f => f(this)))
{} // Handled
else if (state.selection instanceof NodeSelection)
scrollRectIntoView(this, this.docView.domAfterPos(state.selection.from).getBoundingClientRect(), startDOM)
else
scrollRectIntoView(this, this.coordsAtPos(state.selection.head, 1), startDOM)
} else if (oldScrollPos) {
resetScrollPos(oldScrollPos)
}
}
destroyPluginViews() {
let view
while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
}
updatePluginViews(prevState) {
if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) {
this.prevDirectPlugins = this.directPlugins
this.destroyPluginViews()
for (let i = 0; i < this.directPlugins.length; i++) {
let plugin = this.directPlugins[i]
if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
}
for (let i = 0; i < this.state.plugins.length; i++) {
let plugin = this.state.plugins[i]
if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
}
} else {
for (let i = 0; i < this.pluginViews.length; i++) {
let pluginView = this.pluginViews[i]
if (pluginView.update) pluginView.update(this, prevState)
}
}
}
// :: (string, ?(prop: *) → *) → *
// Goes over the values of a prop, first those provided directly,
// then those from plugins given to the view, then from plugins in
// the state (in order), and calls `f` every time a non-undefined
// value is found. When `f` returns a truthy value, that is
// immediately returned. When `f` isn't provided, it is treated as
// the identity function (the prop value is returned directly).
someProp(propName, f) {
let prop = this._props && this._props[propName], value
if (prop != null && (value = f ? f(prop) : prop)) return value
for (let i = 0; i < this.directPlugins.length; i++) {
let prop = this.directPlugins[i].props[propName]
if (prop != null && (value = f ? f(prop) : prop)) return value
}
let plugins = this.state.plugins
if (plugins) for (let i = 0; i < plugins.length; i++) {
let prop = plugins[i].props[propName]
if (prop != null && (value = f ? f(prop) : prop)) return value
}
}
// :: () → bool
// Query whether the view has focus.
hasFocus() {
return this.root.activeElement == this.dom
}
// :: ()
// Focus the editor.
focus() {
this.domObserver.stop()
if (this.editable) focusPreventScroll(this.dom)
selectionToDOM(this)
this.domObserver.start()
}
// :: union
// Get the document root in which the editor exists. This will
// usually be the top-level `document`, but might be a [shadow
// DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
// root if the editor is inside one.
get root() {
let cached = this._root
if (cached == null) for (let search = this.dom.parentNode; search; search = search.parentNode) {
if (search.nodeType == 9 || (search.nodeType == 11 && search.host)) {
if (!search.getSelection) Object.getPrototypeOf(search).getSelection = () => document.getSelection()
return this._root = search
}
}
return cached || document
}
// :: ({left: number, top: number}) → ?{pos: number, inside: number}
// Given a pair of viewport coordinates, return the document
// position that corresponds to them. May return null if the given
// coordinates aren't inside of the editor. When an object is
// returned, its `pos` property is the position nearest to the
// coordinates, and its `inside` property holds the position of the
// inner node that the position falls inside of, or -1 if it is at
// the top level, not in any node.
posAtCoords(coords) {
return posAtCoords(this, coords)
}
// :: (number, number) → {left: number, right: number, top: number, bottom: number}
// Returns the viewport rectangle at a given document position.
// `left` and `right` will be the same number, as this returns a
// flat cursor-ish rectangle. If the position is between two things
// that aren't directly adjacent, `side` determines which element is
// used. When < 0, the element before the position is used,
// otherwise the element after.
coordsAtPos(pos, side = 1) {
return coordsAtPos(this, pos, side)
}
// :: (number, number) → {node: dom.Node, offset: number}
// Find the DOM position that corresponds to the given document
// position. When `side` is negative, find the position as close as
// possible to the content before the position. When positive,
// prefer positions close to the content after the position. When
// zero, prefer as shallow a position as possible.
//
// Note that you should **not** mutate the editor's internal DOM,
// only inspect it (and even that is usually not necessary).
domAtPos(pos, side = 0) {
return this.docView.domFromPos(pos, side)
}
// :: (number) → ?dom.Node
// Find the DOM node that represents the document node after the
// given position. May return `null` when the position doesn't point
// in front of a node or if the node is inside an opaque node view.
//
// This is intended to be able to call things like
// `getBoundingClientRect` on that DOM node. Do **not** mutate the
// editor DOM directly, or add styling this way, since that will be
// immediately overriden by the editor as it redraws the node.
nodeDOM(pos) {
let desc = this.docView.descAt(pos)
return desc ? desc.nodeDOM : null
}
// :: (dom.Node, number, ?number) → number
// Find the document position that corresponds to a given DOM
// position. (Whenever possible, it is preferable to inspect the
// document structure directly, rather than poking around in the
// DOM, but sometimes—for example when interpreting an event
// target—you don't have a choice.)
//
// The `bias` parameter can be used to influence which side of a DOM
// node to use when the position is inside a leaf node.
posAtDOM(node, offset, bias = -1) {
let pos = this.docView.posFromDOM(node, offset, bias)
if (pos == null) throw new RangeError("DOM position not inside the editor")
return pos
}
// :: (union<"up", "down", "left", "right", "forward", "backward">, ?EditorState) → bool
// Find out whether the selection is at the end of a textblock when
// moving in a given direction. When, for example, given `"left"`,
// it will return true if moving left from the current cursor
// position would leave that position's parent textblock. Will apply
// to the view's current state by default, but it is possible to
// pass a different state.
endOfTextblock(dir, state) {
return endOfTextblock(this, state || this.state, dir)
}
// :: ()
// Removes the editor from the DOM and destroys all [node
// views](#view.NodeView).
destroy() {
if (!this.docView) return
destroyInput(this)
this.destroyPluginViews()
if (this.mounted) {
this.docView.update(this.state.doc, [], viewDecorations(this), this)
this.dom.textContent = ""
} else if (this.dom.parentNode) {
this.dom.parentNode.removeChild(this.dom)
}
this.docView.destroy()
this.docView = null
}
// :: boolean
// This is true when the view has been
// [destroyed](#view.EditorView.destroy) (and thus should not be
// used anymore).
get isDestroyed() {
return this.docView == null
}
// Used for testing.
dispatchEvent(event) {
return dispatchEvent(this, event)
}
// :: (Transaction)
// Dispatch a transaction. Will call
// [`dispatchTransaction`](#view.DirectEditorProps.dispatchTransaction)
// when given, and otherwise defaults to applying the transaction to
// the current state and calling
// [`updateState`](#view.EditorView.updateState) with the result.
// This method is bound to the view instance, so that it can be
// easily passed around.
dispatch(tr) {
let dispatchTransaction = this._props.dispatchTransaction
if (dispatchTransaction) dispatchTransaction.call(this, tr)
else this.updateState(this.state.apply(tr))
}
}
function computeDocDeco(view) {
let attrs = Object.create(null)
attrs.class = "ProseMirror"
attrs.contenteditable = String(view.editable)
attrs.translate = "no"
view.someProp("attributes", value => {
if (typeof value == "function") value = value(view.state)
if (value) for (let attr in value) {
if (attr == "class")
attrs.class += " " + value[attr]
if (attr == "style") {
attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]
}
else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName")
attrs[attr] = String(value[attr])
}
})
return [Decoration.node(0, view.state.doc.content.size, attrs)]
}
function updateCursorWrapper(view) {
if (view.markCursor) {
let dom = document.createElement("img")
dom.className = "ProseMirror-separator"
dom.setAttribute("mark-placeholder", "true")
dom.setAttribute("alt", "")
view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.head, dom, {raw: true, marks: view.markCursor})}
} else {
view.cursorWrapper = null
}
}
function getEditable(view) {
return !view.someProp("editable", value => value(view.state) === false)
}
function selectionContextChanged(sel1, sel2) {
let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head))
return sel1.$anchor.start(depth) != sel2.$anchor.start(depth)
}
function buildNodeViews(view) {
let result = {}
view.someProp("nodeViews", obj => {
for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop))
result[prop] = obj[prop]
})
return result
}
function changedNodeViews(a, b) {
let nA = 0, nB = 0
for (let prop in a) {
if (a[prop] != b[prop]) return true
nA++
}
for (let _ in b) nB++
return nA != nB
}
function checkStateComponent(plugin) {
if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction)
throw new RangeError("Plugins passed directly to the view must not have a state component")
}
// EditorProps:: interface
//
// Props are configuration values that can be passed to an editor view
// or included in a plugin. This interface lists the supported props.
//
// The various event-handling functions may all return `true` to
// indicate that they handled the given event. The view will then take
// care to call `preventDefault` on the event, except with
// `handleDOMEvents`, where the handler itself is responsible for that.
//
// How a prop is resolved depends on the prop. Handler functions are
// called one at a time, starting with the base props and then
// searching through the plugins (in order of appearance) until one of
// them returns true. For some props, the first plugin that yields a
// value gets precedence.
//
// handleDOMEvents:: ?Object<(view: EditorView, event: dom.Event) → bool>
// Can be an object mapping DOM event type names to functions that
// handle them. Such functions will be called before any handling
// ProseMirror does of events fired on the editable DOM element.
// Contrary to the other event handling props, when returning true
// from such a function, you are responsible for calling
// `preventDefault` yourself (or not, if you want to allow the
// default behavior).
//
// handleKeyDown:: ?(view: EditorView, event: dom.KeyboardEvent) → bool
// Called when the editor receives a `keydown` event.
//
// handleKeyPress:: ?(view: EditorView, event: dom.KeyboardEvent) → bool
// Handler for `keypress` events.
//
// handleTextInput:: ?(view: EditorView, from: number, to: number, text: string) → bool
// Whenever the user directly input text, this handler is called
// before the input is applied. If it returns `true`, the default
// behavior of actually inserting the text is suppressed.
//
// handleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool
// Called for each node around a click, from the inside out. The
// `direct` flag will be true for the inner node.
//
// handleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool
// Called when the editor is clicked, after `handleClickOn` handlers
// have been called.
//
// handleDoubleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool
// Called for each node around a double click.
//
// handleDoubleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool
// Called when the editor is double-clicked, after `handleDoubleClickOn`.
//
// handleTripleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool
// Called for each node around a triple click.
//
// handleTripleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool
// Called when the editor is triple-clicked, after `handleTripleClickOn`.
//
// handlePaste:: ?(view: EditorView, event: dom.ClipboardEvent, slice: Slice) → bool
// Can be used to override the behavior of pasting. `slice` is the
// pasted content parsed by the editor, but you can directly access
// the event to get at the raw content.
//
// handleDrop:: ?(view: EditorView, event: dom.Event, slice: Slice, moved: bool) → bool
// Called when something is dropped on the editor. `moved` will be
// true if this drop moves from the current selection (which should
// thus be deleted).
//
// handleScrollToSelection:: ?(view: EditorView) → bool
// Called when the view, after updating its state, tries to scroll
// the selection into view. A handler function may return false to
// indicate that it did not handle the scrolling and further
// handlers or the default behavior should be tried.
//
// createSelectionBetween:: ?(view: EditorView, anchor: ResolvedPos, head: ResolvedPos) → ?Selection
// Can be used to override the way a selection is created when
// reading a DOM selection between the given anchor and head.
//
// domParser:: ?DOMParser
// The [parser](#model.DOMParser) to use when reading editor changes
// from the DOM. Defaults to calling
// [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) on the
// editor's schema.
//
// transformPastedHTML:: ?(html: string) → string
// Can be used to transform pasted HTML text, _before_ it is parsed,
// for example to clean it up.
//
// clipboardParser:: ?DOMParser
// The [parser](#model.DOMParser) to use when reading content from
// the clipboard. When not given, the value of the
// [`domParser`](#view.EditorProps.domParser) prop is used.
//
// transformPastedText:: ?(text: string, plain: bool) → string
// Transform pasted plain text. The `plain` flag will be true when
// the text is pasted as plain text.
//
// clipboardTextParser:: ?(text: string, $context: ResolvedPos, plain: bool) → Slice
// A function to parse text from the clipboard into a document
// slice. Called after
// [`transformPastedText`](#view.EditorProps.transformPastedText).
// The default behavior is to split the text into lines, wrap them
// in `` tags, and call
// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
// The `plain` flag will be true when the text is pasted as plain text.
//
// transformPasted:: ?(Slice) → Slice
// Can be used to transform pasted content before it is applied to
// the document.
//
// nodeViews:: ?Object<(node: Node, view: EditorView, getPos: () → number, decorations: [Decoration], innerDecorations: DecorationSource) → NodeView>
// Allows you to pass custom rendering and behavior logic for nodes
// and marks. Should map node and mark names to constructor
// functions that produce a [`NodeView`](#view.NodeView) object
// implementing the node's display behavior. For nodes, the third
// argument `getPos` is a function that can be called to get the
// node's current position, which can be useful when creating
// transactions to update it. For marks, the third argument is a
// boolean that indicates whether the mark's content is inline.
//
// `decorations` is an array of node or inline decorations that are
// active around the node. They are automatically drawn in the
// normal way, and you will usually just want to ignore this, but
// they can also be used as a way to provide context information to
// the node view without adding it to the document itself.
//
// `innerDecorations` holds the decorations for the node's content.
// You can safely ignore this if your view has no content or a
// `contentDOM` property, since the editor will draw the decorations
// on the content. But if you, for example, want to create a nested
// editor with the content, it may make sense to provide it with the
// inner decorations.
//
// clipboardSerializer:: ?DOMSerializer
// The DOM serializer to use when putting content onto the
// clipboard. If not given, the result of
// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)
// will be used. This object will only have its
// [`serializeFragment`](#model.DOMSerializer.serializeFragment)
// method called, and you may provide an alternative object type
// implementing a compatible method.
//
// clipboardTextSerializer:: ?(Slice) → string
// A function that will be called to get the text for the current
// selection when copying text to the clipboard. By default, the
// editor will use [`textBetween`](#model.Node.textBetween) on the
// selected range.
//
// decorations:: ?(state: EditorState) → ?DecorationSource
// A set of [document decorations](#view.Decoration) to show in the
// view.
//
// editable:: ?(state: EditorState) → bool
// When this returns false, the content of the view is not directly
// editable.
//
// attributes:: ?union