Initial
This commit is contained in:
8
resources/app/node_modules/prosemirror-view/.tern-project
generated
vendored
Normal file
8
resources/app/node_modules/prosemirror-view/.tern-project
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"libs": ["browser"],
|
||||
"plugins": {
|
||||
"node": {},
|
||||
"complete_strings": {},
|
||||
"es_modules": {}
|
||||
}
|
||||
}
|
||||
104
resources/app/node_modules/prosemirror-view/CONTRIBUTING.md
generated
vendored
Normal file
104
resources/app/node_modules/prosemirror-view/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
# How to contribute
|
||||
|
||||
- [Getting help](#getting-help)
|
||||
- [Submitting bug reports](#submitting-bug-reports)
|
||||
- [Contributing code](#contributing-code)
|
||||
|
||||
## Getting help
|
||||
|
||||
Community discussion, questions, and informal bug reporting is done on the
|
||||
[discuss.ProseMirror forum](http://discuss.prosemirror.net).
|
||||
|
||||
## Submitting bug reports
|
||||
|
||||
Report bugs on the
|
||||
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
|
||||
Before reporting a bug, please read these pointers.
|
||||
|
||||
- The issue tracker is for *bugs*, not requests for help. Questions
|
||||
should be asked on the [forum](http://discuss.prosemirror.net).
|
||||
|
||||
- Include information about the version of the code that exhibits the
|
||||
problem. For browser-related issues, include the browser and browser
|
||||
version on which the problem occurred.
|
||||
|
||||
- Mention very precisely what went wrong. "X is broken" is not a good
|
||||
bug report. What did you expect to happen? What happened instead?
|
||||
Describe the exact steps a maintainer has to take to make the
|
||||
problem occur. A screencast can be useful, but is no substitute for
|
||||
a textual description.
|
||||
|
||||
- A great way to make it easy to reproduce your problem, if it can not
|
||||
be trivially reproduced on the website demos, is to submit a script
|
||||
that triggers the issue.
|
||||
|
||||
## Contributing code
|
||||
|
||||
If you want to make a change that involves a significant overhaul of
|
||||
the code or introduces a user-visible new feature, create an
|
||||
[RFC](https://github.com/ProseMirror/rfcs/) first with your proposal.
|
||||
|
||||
- Make sure you have a [GitHub Account](https://github.com/signup/free)
|
||||
|
||||
- Fork the relevant repository
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
|
||||
- Create a local checkout of the code. You can use the
|
||||
[main repository](https://github.com/prosemirror/prosemirror) to
|
||||
easily check out all core modules.
|
||||
|
||||
- Make your changes, and commit them
|
||||
|
||||
- Follow the code style of the rest of the project (see below). Run
|
||||
`npm run lint` (in the main repository checkout) to make sure that
|
||||
the linter is happy.
|
||||
|
||||
- If your changes are easy to test or likely to regress, add tests in
|
||||
the relevant `test/` directory. Either put them in an existing
|
||||
`test-*.js` file, if they fit there, or add a new file.
|
||||
|
||||
- Make sure all tests pass. Run `npm run test` to verify tests pass
|
||||
(you will need Node.js v6+).
|
||||
|
||||
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
Don't put more than one feature/fix in a single pull request.
|
||||
|
||||
By contributing code to ProseMirror you
|
||||
|
||||
- Agree to license the contributed code under the project's [MIT
|
||||
license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
|
||||
|
||||
- Confirm that you have the right to contribute and license the code
|
||||
in question. (Either you hold all rights on the code, or the rights
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Coding standards
|
||||
|
||||
- ES6 syntax, targeting an ES5 runtime (i.e. don't use library
|
||||
elements added by ES6, don't use ES7/ES.next syntax).
|
||||
|
||||
- 2 spaces per indentation level, no tabs.
|
||||
|
||||
- No semicolons except when necessary.
|
||||
|
||||
- Follow the surrounding code when it comes to spacing, brace
|
||||
placement, etc.
|
||||
|
||||
- Brace-less single-statement bodies are encouraged (whenever they
|
||||
don't impact readability).
|
||||
|
||||
- [getdocs](https://github.com/marijnh/getdocs)-style doc comments
|
||||
above items that are part of the public API.
|
||||
|
||||
- When documenting non-public items, you can put the type after a
|
||||
single colon, so that getdocs doesn't pick it up and add it to the
|
||||
API reference.
|
||||
|
||||
- The linter (`npm run lint`) complains about unused variables and
|
||||
functions. Prefix their names with an underscore to muffle it.
|
||||
|
||||
- ProseMirror does *not* follow JSHint or JSLint prescribed style.
|
||||
Patches that try to 'fix' code to pass one of these linters will not
|
||||
be accepted.
|
||||
19
resources/app/node_modules/prosemirror-view/LICENSE
generated
vendored
Normal file
19
resources/app/node_modules/prosemirror-view/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
5138
resources/app/node_modules/prosemirror-view/dist/index.cjs
generated
vendored
Normal file
5138
resources/app/node_modules/prosemirror-view/dist/index.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
924
resources/app/node_modules/prosemirror-view/dist/index.d.cts
generated
vendored
Normal file
924
resources/app/node_modules/prosemirror-view/dist/index.d.cts
generated
vendored
Normal file
@@ -0,0 +1,924 @@
|
||||
import { EditorState, Transaction, Selection, Plugin } from 'prosemirror-state';
|
||||
import { Mark, Node, TagParseRule, Slice, ResolvedPos, DOMParser, DOMSerializer } from 'prosemirror-model';
|
||||
import { Mapping } from 'prosemirror-transform';
|
||||
|
||||
type DOMNode = InstanceType<typeof window.Node>;
|
||||
|
||||
type WidgetConstructor = ((view: EditorView, getPos: () => number | undefined) => DOMNode) | DOMNode;
|
||||
/**
|
||||
Decoration objects can be provided to the view through the
|
||||
[`decorations` prop](https://prosemirror.net/docs/ref/#view.EditorProps.decorations). They come in
|
||||
several variants—see the static members of this class for details.
|
||||
*/
|
||||
declare class Decoration {
|
||||
/**
|
||||
The start position of the decoration.
|
||||
*/
|
||||
readonly from: number;
|
||||
/**
|
||||
The end position. Will be the same as `from` for [widget
|
||||
decorations](https://prosemirror.net/docs/ref/#view.Decoration^widget).
|
||||
*/
|
||||
readonly to: number;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
static widget(pos: number, toDOM: WidgetConstructor, spec?: {
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
side?: number;
|
||||
/**
|
||||
The precise set of marks to draw around the widget.
|
||||
*/
|
||||
marks?: readonly Mark[];
|
||||
/**
|
||||
Can be used to control which DOM events, when they bubble out
|
||||
of this widget, the editor view should ignore.
|
||||
*/
|
||||
stopEvent?: (event: Event) => boolean;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
ignoreSelection?: boolean;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
Called when the widget decoration is removed or the editor is
|
||||
destroyed.
|
||||
*/
|
||||
destroy?: (node: DOMNode) => void;
|
||||
/**
|
||||
Specs allow arbitrary additional properties.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}): Decoration;
|
||||
/**
|
||||
Creates an inline decoration, which adds the given attributes to
|
||||
each inline node between `from` and `to`.
|
||||
*/
|
||||
static inline(from: number, to: number, attrs: DecorationAttrs, spec?: {
|
||||
/**
|
||||
Determines how the left side of the decoration is
|
||||
[mapped](https://prosemirror.net/docs/ref/#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.
|
||||
*/
|
||||
inclusiveStart?: boolean;
|
||||
/**
|
||||
Determines how the right side of the decoration is mapped.
|
||||
See
|
||||
[`inclusiveStart`](https://prosemirror.net/docs/ref/#view.Decoration^inline^spec.inclusiveStart).
|
||||
*/
|
||||
inclusiveEnd?: boolean;
|
||||
/**
|
||||
Specs may have arbitrary additional properties.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}): 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.
|
||||
*/
|
||||
static node(from: number, to: number, attrs: DecorationAttrs, spec?: any): Decoration;
|
||||
/**
|
||||
The spec provided when creating this decoration. Can be useful
|
||||
if you've stored extra information in that object.
|
||||
*/
|
||||
get spec(): any;
|
||||
}
|
||||
/**
|
||||
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:
|
||||
*/
|
||||
type DecorationAttrs = {
|
||||
/**
|
||||
When non-null, the target node is wrapped in a DOM element of
|
||||
this type (and the other attributes are applied to this element).
|
||||
*/
|
||||
nodeName?: string;
|
||||
/**
|
||||
A CSS class name or a space-separated set of class names to be
|
||||
_added_ to the classes that the node already had.
|
||||
*/
|
||||
class?: string;
|
||||
/**
|
||||
A string of CSS to be _added_ to the node's existing `style` property.
|
||||
*/
|
||||
style?: string;
|
||||
/**
|
||||
Any other properties are treated as regular DOM attributes.
|
||||
*/
|
||||
[attribute: string]: string | undefined;
|
||||
};
|
||||
/**
|
||||
An object that can [provide](https://prosemirror.net/docs/ref/#view.EditorProps.decorations)
|
||||
decorations. Implemented by [`DecorationSet`](https://prosemirror.net/docs/ref/#view.DecorationSet),
|
||||
and passed to [node views](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews).
|
||||
*/
|
||||
interface DecorationSource {
|
||||
/**
|
||||
Map the set of decorations in response to a change in the
|
||||
document.
|
||||
*/
|
||||
map: (mapping: Mapping, node: Node) => DecorationSource;
|
||||
/**
|
||||
Extract a DecorationSource containing decorations for the given child node at the given offset.
|
||||
*/
|
||||
forChild(offset: number, child: Node): DecorationSource;
|
||||
}
|
||||
/**
|
||||
A collection of [decorations](https://prosemirror.net/docs/ref/#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.
|
||||
*/
|
||||
declare class DecorationSet implements DecorationSource {
|
||||
/**
|
||||
Create a set of decorations, using the structure of the given
|
||||
document. This will consume (modify) the `decorations` array, so
|
||||
you must make a copy if you want need to preserve that.
|
||||
*/
|
||||
static create(doc: Node, decorations: Decoration[]): DecorationSet;
|
||||
/**
|
||||
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?: number, end?: number, predicate?: (spec: any) => boolean): Decoration[];
|
||||
private findInner;
|
||||
/**
|
||||
Map the set of decorations in response to a change in the
|
||||
document.
|
||||
*/
|
||||
map(mapping: Mapping, doc: Node, options?: {
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
onRemove?: (decorationSpec: any) => void;
|
||||
}): DecorationSet;
|
||||
/**
|
||||
Add the given array of decorations to the ones in the set,
|
||||
producing a new set. Consumes the `decorations` array. Needs
|
||||
access to the current document to create the appropriate tree
|
||||
structure.
|
||||
*/
|
||||
add(doc: Node, decorations: Decoration[]): DecorationSet;
|
||||
private addInner;
|
||||
/**
|
||||
Create a new set that contains the decorations in this set, minus
|
||||
the ones in the given array.
|
||||
*/
|
||||
remove(decorations: Decoration[]): DecorationSet;
|
||||
private removeInner;
|
||||
forChild(offset: number, node: Node): DecorationSet | DecorationGroup;
|
||||
/**
|
||||
The empty set of decorations.
|
||||
*/
|
||||
static empty: DecorationSet;
|
||||
}
|
||||
declare class DecorationGroup implements DecorationSource {
|
||||
readonly members: readonly DecorationSet[];
|
||||
constructor(members: readonly DecorationSet[]);
|
||||
map(mapping: Mapping, doc: Node): DecorationSource;
|
||||
forChild(offset: number, child: Node): DecorationSource | DecorationSet;
|
||||
eq(other: DecorationGroup): boolean;
|
||||
locals(node: Node): readonly any[];
|
||||
static from(members: readonly DecorationSource[]): DecorationSource;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Node {
|
||||
pmViewDesc?: ViewDesc;
|
||||
}
|
||||
}
|
||||
/**
|
||||
By default, document nodes are rendered using the result of the
|
||||
[`toDOM`](https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM) method of their spec, and managed
|
||||
entirely by the editor. For some use cases, such as embedded
|
||||
node-specific editing interfaces, you want more control over
|
||||
the behavior of a node's in-editor representation, and need to
|
||||
[define](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) a custom node view.
|
||||
|
||||
Mark views only support `dom` and `contentDOM`, and don't support
|
||||
any of the node view methods.
|
||||
|
||||
Objects returned as node views must conform to this interface.
|
||||
*/
|
||||
interface NodeView {
|
||||
/**
|
||||
The outer DOM node that represents the document node.
|
||||
*/
|
||||
dom: DOMNode;
|
||||
/**
|
||||
The DOM node that should hold the node's content. Only meaningful
|
||||
if the node view also defines a `dom` property and if its node
|
||||
type is not a leaf node type. When this is present, ProseMirror
|
||||
will take care of rendering the node's children into it. When it
|
||||
is not present, the node view itself is responsible for rendering
|
||||
(or deciding not to render) its child nodes.
|
||||
*/
|
||||
contentDOM?: HTMLElement | null;
|
||||
/**
|
||||
When given, this will be called when the view is updating itself.
|
||||
It will be given a node (possibly of a different type), an array
|
||||
of active decorations around the node (which are automatically
|
||||
drawn, and the node view may ignore if it isn't interested in
|
||||
them), and a [decoration source](https://prosemirror.net/docs/ref/#view.DecorationSource) that
|
||||
represents any decorations that apply to the content of the node
|
||||
(which again may be ignored). It should return true if it was
|
||||
able to update to that node, and false otherwise. If the node
|
||||
view has a `contentDOM` property (or no `dom` property), updating
|
||||
its child nodes will be handled by ProseMirror.
|
||||
*/
|
||||
update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean;
|
||||
/**
|
||||
Can be used to override the way the node's selected status (as a
|
||||
node selection) is displayed.
|
||||
*/
|
||||
selectNode?: () => void;
|
||||
/**
|
||||
When defining a `selectNode` method, you should also provide a
|
||||
`deselectNode` method to remove the effect again.
|
||||
*/
|
||||
deselectNode?: () => void;
|
||||
/**
|
||||
This will be called to handle setting the selection inside the
|
||||
node. The `anchor` and `head` positions are relative to the start
|
||||
of the node. By default, a DOM selection will be created between
|
||||
the DOM positions corresponding to those positions, but if you
|
||||
override it you can do something else.
|
||||
*/
|
||||
setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void;
|
||||
/**
|
||||
Can be used to prevent the editor view from trying to handle some
|
||||
or all DOM events that bubble up from the node view. Events for
|
||||
which this returns true are not handled by the editor.
|
||||
*/
|
||||
stopEvent?: (event: Event) => boolean;
|
||||
/**
|
||||
Called when a DOM
|
||||
[mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
|
||||
or a selection change happens within the view. When the change is
|
||||
a selection change, the record will have a `type` property of
|
||||
`"selection"` (which doesn't occur for native mutation records).
|
||||
Return false if the editor should re-read the selection or
|
||||
re-parse the range around the mutation, true if it can safely be
|
||||
ignored.
|
||||
*/
|
||||
ignoreMutation?: (mutation: MutationRecord) => boolean;
|
||||
/**
|
||||
Called when the node view is removed from the editor or the whole
|
||||
editor is destroyed. (Not available for marks.)
|
||||
*/
|
||||
destroy?: () => void;
|
||||
}
|
||||
declare class ViewDesc {
|
||||
parent: ViewDesc | undefined;
|
||||
children: ViewDesc[];
|
||||
dom: DOMNode;
|
||||
contentDOM: HTMLElement | null;
|
||||
dirty: number;
|
||||
node: Node | null;
|
||||
constructor(parent: ViewDesc | undefined, children: ViewDesc[], dom: DOMNode, contentDOM: HTMLElement | null);
|
||||
matchesWidget(widget: Decoration): boolean;
|
||||
matchesMark(mark: Mark): boolean;
|
||||
matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource): boolean;
|
||||
matchesHack(nodeName: string): boolean;
|
||||
parseRule(): Omit<TagParseRule, "tag"> | null;
|
||||
stopEvent(event: Event): boolean;
|
||||
get size(): number;
|
||||
get border(): number;
|
||||
destroy(): void;
|
||||
posBeforeChild(child: ViewDesc): number;
|
||||
get posBefore(): number;
|
||||
get posAtStart(): number;
|
||||
get posAfter(): number;
|
||||
get posAtEnd(): number;
|
||||
localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
||||
nearestDesc(dom: DOMNode): ViewDesc | undefined;
|
||||
nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined;
|
||||
getDesc(dom: DOMNode): ViewDesc | undefined;
|
||||
posFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
||||
descAt(pos: number): ViewDesc | undefined;
|
||||
domFromPos(pos: number, side: number): {
|
||||
node: DOMNode;
|
||||
offset: number;
|
||||
atom?: number;
|
||||
};
|
||||
parseRange(from: number, to: number, base?: number): {
|
||||
node: DOMNode;
|
||||
from: number;
|
||||
to: number;
|
||||
fromOffset: number;
|
||||
toOffset: number;
|
||||
};
|
||||
emptyChildAt(side: number): boolean;
|
||||
domAfterPos(pos: number): DOMNode;
|
||||
setSelection(anchor: number, head: number, root: Document | ShadowRoot, force?: boolean): void;
|
||||
ignoreMutation(mutation: MutationRecord): boolean;
|
||||
get contentLost(): boolean | null;
|
||||
markDirty(from: number, to: number): void;
|
||||
markParentsDirty(): void;
|
||||
get domAtom(): boolean;
|
||||
get ignoreForCoords(): boolean;
|
||||
isText(text: string): boolean;
|
||||
}
|
||||
declare class NodeViewDesc extends ViewDesc {
|
||||
node: Node;
|
||||
outerDeco: readonly Decoration[];
|
||||
innerDeco: DecorationSource;
|
||||
readonly nodeDOM: DOMNode;
|
||||
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, view: EditorView, pos: number);
|
||||
static create(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView, pos: number): NodeViewDesc | TextViewDesc;
|
||||
parseRule(): Omit<TagParseRule, "tag"> | null;
|
||||
matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource): boolean;
|
||||
get size(): number;
|
||||
get border(): 0 | 1;
|
||||
updateChildren(view: EditorView, pos: number): void;
|
||||
localCompositionInfo(view: EditorView, pos: number): {
|
||||
node: Text;
|
||||
pos: number;
|
||||
text: string;
|
||||
} | null;
|
||||
protectLocalComposition(view: EditorView, { node, pos, text }: {
|
||||
node: Text;
|
||||
pos: number;
|
||||
text: string;
|
||||
}): void;
|
||||
update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): boolean;
|
||||
updateInner(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): void;
|
||||
updateOuterDeco(outerDeco: readonly Decoration[]): void;
|
||||
selectNode(): void;
|
||||
deselectNode(): void;
|
||||
get domAtom(): boolean;
|
||||
}
|
||||
declare class TextViewDesc extends NodeViewDesc {
|
||||
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode, view: EditorView);
|
||||
parseRule(): {
|
||||
skip: any;
|
||||
};
|
||||
update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): boolean;
|
||||
inParent(): boolean;
|
||||
domFromPos(pos: number): {
|
||||
node: globalThis.Node;
|
||||
offset: number;
|
||||
};
|
||||
localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
||||
ignoreMutation(mutation: MutationRecord): boolean;
|
||||
slice(from: number, to: number, view: EditorView): TextViewDesc;
|
||||
markDirty(from: number, to: number): void;
|
||||
get domAtom(): boolean;
|
||||
isText(text: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
An editor view manages the DOM structure that represents an
|
||||
editable document. Its state and behavior are determined by its
|
||||
[props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).
|
||||
*/
|
||||
declare class EditorView {
|
||||
private _props;
|
||||
private directPlugins;
|
||||
private _root;
|
||||
private mounted;
|
||||
private prevDirectPlugins;
|
||||
private pluginViews;
|
||||
/**
|
||||
The view's current [state](https://prosemirror.net/docs/ref/#state.EditorState).
|
||||
*/
|
||||
state: EditorState;
|
||||
/**
|
||||
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: null | DOMNode | ((editor: HTMLElement) => void) | {
|
||||
mount: HTMLElement;
|
||||
}, props: DirectEditorProps);
|
||||
/**
|
||||
An editable DOM node containing the document. (You probably
|
||||
should not directly interfere with its content.)
|
||||
*/
|
||||
readonly dom: HTMLElement;
|
||||
/**
|
||||
Indicates whether the editor is currently [editable](https://prosemirror.net/docs/ref/#view.EditorProps.editable).
|
||||
*/
|
||||
editable: boolean;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
dragging: null | {
|
||||
slice: Slice;
|
||||
move: boolean;
|
||||
};
|
||||
/**
|
||||
Holds `true` when a
|
||||
[composition](https://w3c.github.io/uievents/#events-compositionevents)
|
||||
is active.
|
||||
*/
|
||||
get composing(): boolean;
|
||||
/**
|
||||
The view's current [props](https://prosemirror.net/docs/ref/#view.EditorProps).
|
||||
*/
|
||||
get props(): DirectEditorProps;
|
||||
/**
|
||||
Update the view's props. Will immediately cause an update to
|
||||
the DOM.
|
||||
*/
|
||||
update(props: DirectEditorProps): void;
|
||||
/**
|
||||
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: Partial<DirectEditorProps>): void;
|
||||
/**
|
||||
Update the editor's `state` prop, without touching any of the
|
||||
other props.
|
||||
*/
|
||||
updateState(state: EditorState): void;
|
||||
private updateStateInner;
|
||||
private destroyPluginViews;
|
||||
private updatePluginViews;
|
||||
private updateDraggedNode;
|
||||
/**
|
||||
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 extends keyof EditorProps, Result>(propName: PropName, f: (value: NonNullable<EditorProps[PropName]>) => Result): Result | undefined;
|
||||
someProp<PropName extends keyof EditorProps>(propName: PropName): NonNullable<EditorProps[PropName]> | undefined;
|
||||
/**
|
||||
Query whether the view has focus.
|
||||
*/
|
||||
hasFocus(): boolean;
|
||||
/**
|
||||
Focus the editor.
|
||||
*/
|
||||
focus(): void;
|
||||
/**
|
||||
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(): Document | ShadowRoot;
|
||||
/**
|
||||
When an existing editor view is moved to a new document or
|
||||
shadow tree, call this to make it recompute its root.
|
||||
*/
|
||||
updateRoot(): void;
|
||||
/**
|
||||
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: {
|
||||
left: number;
|
||||
top: number;
|
||||
}): {
|
||||
pos: number;
|
||||
inside: number;
|
||||
} | null;
|
||||
/**
|
||||
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: number, side?: number): {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: 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: number, side?: number): {
|
||||
node: DOMNode;
|
||||
offset: number;
|
||||
};
|
||||
/**
|
||||
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: number): DOMNode | null;
|
||||
/**
|
||||
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: DOMNode, offset: number, bias?: number): number;
|
||||
/**
|
||||
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: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean;
|
||||
/**
|
||||
Run the editor's paste logic with the given HTML string. The
|
||||
`event`, if given, will be passed to the
|
||||
[`handlePaste`](https://prosemirror.net/docs/ref/#view.EditorProps.handlePaste) hook.
|
||||
*/
|
||||
pasteHTML(html: string, event?: ClipboardEvent): boolean;
|
||||
/**
|
||||
Run the editor's paste logic with the given plain-text input.
|
||||
*/
|
||||
pasteText(text: string, event?: ClipboardEvent): boolean;
|
||||
/**
|
||||
Removes the editor from the DOM and destroys all [node
|
||||
views](https://prosemirror.net/docs/ref/#view.NodeView).
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
This is true when the view has been
|
||||
[destroyed](https://prosemirror.net/docs/ref/#view.EditorView.destroy) (and thus should not be
|
||||
used anymore).
|
||||
*/
|
||||
get isDestroyed(): boolean;
|
||||
/**
|
||||
Used for testing.
|
||||
*/
|
||||
dispatchEvent(event: Event): void;
|
||||
/**
|
||||
Dispatch a transaction. Will call
|
||||
[`dispatchTransaction`](https://prosemirror.net/docs/ref/#view.DirectEditorProps.dispatchTransaction)
|
||||
when given, and otherwise defaults to applying the transaction to
|
||||
the current state and calling
|
||||
[`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) with the result.
|
||||
This method is bound to the view instance, so that it can be
|
||||
easily passed around.
|
||||
*/
|
||||
dispatch(tr: Transaction): void;
|
||||
}
|
||||
/**
|
||||
The type of function [provided](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) to
|
||||
create [node views](https://prosemirror.net/docs/ref/#view.NodeView).
|
||||
*/
|
||||
type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined, decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView;
|
||||
/**
|
||||
The function types [used](https://prosemirror.net/docs/ref/#view.EditorProps.markViews) to create
|
||||
mark views.
|
||||
*/
|
||||
type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => {
|
||||
dom: HTMLElement;
|
||||
contentDOM?: HTMLElement;
|
||||
};
|
||||
/**
|
||||
Helper type that maps event names to event object types, but
|
||||
includes events that TypeScript's HTMLElementEventMap doesn't know
|
||||
about.
|
||||
*/
|
||||
interface DOMEventMap extends HTMLElementEventMap {
|
||||
[event: string]: any;
|
||||
}
|
||||
/**
|
||||
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.
|
||||
|
||||
The optional type parameter refers to the type of `this` in prop
|
||||
functions, and is used to pass in the plugin type when defining a
|
||||
[plugin](https://prosemirror.net/docs/ref/#state.Plugin).
|
||||
*/
|
||||
interface EditorProps<P = any> {
|
||||
/**
|
||||
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).
|
||||
*/
|
||||
handleDOMEvents?: {
|
||||
[event in keyof DOMEventMap]?: (this: P, view: EditorView, event: DOMEventMap[event]) => boolean | void;
|
||||
};
|
||||
/**
|
||||
Called when the editor receives a `keydown` event.
|
||||
*/
|
||||
handleKeyDown?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void;
|
||||
/**
|
||||
Handler for `keypress` events.
|
||||
*/
|
||||
handleKeyPress?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void;
|
||||
/**
|
||||
Called for each node around a click, from the inside out. The
|
||||
`direct` flag will be true for the inner node.
|
||||
*/
|
||||
handleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void;
|
||||
/**
|
||||
Called when the editor is clicked, after `handleClickOn` handlers
|
||||
have been called.
|
||||
*/
|
||||
handleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void;
|
||||
/**
|
||||
Called for each node around a double click.
|
||||
*/
|
||||
handleDoubleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void;
|
||||
/**
|
||||
Called when the editor is double-clicked, after `handleDoubleClickOn`.
|
||||
*/
|
||||
handleDoubleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void;
|
||||
/**
|
||||
Called for each node around a triple click.
|
||||
*/
|
||||
handleTripleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void;
|
||||
/**
|
||||
Called when the editor is triple-clicked, after `handleTripleClickOn`.
|
||||
*/
|
||||
handleTripleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
handlePaste?: (this: P, view: EditorView, event: ClipboardEvent, slice: Slice) => boolean | void;
|
||||
/**
|
||||
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).
|
||||
*/
|
||||
handleDrop?: (this: P, view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => boolean | void;
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
handleScrollToSelection?: (this: P, view: EditorView) => boolean;
|
||||
/**
|
||||
Can be used to override the way a selection is created when
|
||||
reading a DOM selection between the given anchor and head.
|
||||
*/
|
||||
createSelectionBetween?: (this: P, view: EditorView, anchor: ResolvedPos, head: ResolvedPos) => Selection | null;
|
||||
/**
|
||||
The [parser](https://prosemirror.net/docs/ref/#model.DOMParser) to use when reading editor changes
|
||||
from the DOM. Defaults to calling
|
||||
[`DOMParser.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMParser^fromSchema) on the
|
||||
editor's schema.
|
||||
*/
|
||||
domParser?: DOMParser;
|
||||
/**
|
||||
Can be used to transform pasted HTML text, _before_ it is parsed,
|
||||
for example to clean it up.
|
||||
*/
|
||||
transformPastedHTML?: (this: P, html: string, view: EditorView) => string;
|
||||
/**
|
||||
The [parser](https://prosemirror.net/docs/ref/#model.DOMParser) to use when reading content from
|
||||
the clipboard. When not given, the value of the
|
||||
[`domParser`](https://prosemirror.net/docs/ref/#view.EditorProps.domParser) prop is used.
|
||||
*/
|
||||
clipboardParser?: DOMParser;
|
||||
/**
|
||||
Transform pasted plain text. The `plain` flag will be true when
|
||||
the text is pasted as plain text.
|
||||
*/
|
||||
transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string;
|
||||
/**
|
||||
A function to parse text from the clipboard into a document
|
||||
slice. Called after
|
||||
[`transformPastedText`](https://prosemirror.net/docs/ref/#view.EditorProps.transformPastedText).
|
||||
The default behavior is to split the text into lines, wrap them
|
||||
in `<p>` tags, and call
|
||||
[`clipboardParser`](https://prosemirror.net/docs/ref/#view.EditorProps.clipboardParser) on it.
|
||||
The `plain` flag will be true when the text is pasted as plain text.
|
||||
*/
|
||||
clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice;
|
||||
/**
|
||||
Can be used to transform pasted or dragged-and-dropped content
|
||||
before it is applied to the document.
|
||||
*/
|
||||
transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice;
|
||||
/**
|
||||
Can be used to transform copied or cut content before it is
|
||||
serialized to the clipboard.
|
||||
*/
|
||||
transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice;
|
||||
/**
|
||||
Allows you to pass custom rendering and behavior logic for
|
||||
nodes. Should map node names to constructor functions that
|
||||
produce a [`NodeView`](https://prosemirror.net/docs/ref/#view.NodeView) object implementing the
|
||||
node's display behavior. 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.
|
||||
Note that if the node is not in the document, the position
|
||||
returned by this function will be `undefined`.
|
||||
|
||||
`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.
|
||||
|
||||
(For backwards compatibility reasons, [mark
|
||||
views](https://prosemirror.net/docs/ref/#view.EditorProps.markViews) can also be included in this
|
||||
object.)
|
||||
*/
|
||||
nodeViews?: {
|
||||
[node: string]: NodeViewConstructor;
|
||||
};
|
||||
/**
|
||||
Pass custom mark rendering functions. Note that these cannot
|
||||
provide the kind of dynamic behavior that [node
|
||||
views](https://prosemirror.net/docs/ref/#view.NodeView) can—they just provide custom rendering
|
||||
logic. The third argument indicates whether the mark's content
|
||||
is inline.
|
||||
*/
|
||||
markViews?: {
|
||||
[mark: string]: MarkViewConstructor;
|
||||
};
|
||||
/**
|
||||
The DOM serializer to use when putting content onto the
|
||||
clipboard. If not given, the result of
|
||||
[`DOMSerializer.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMSerializer^fromSchema)
|
||||
will be used. This object will only have its
|
||||
[`serializeFragment`](https://prosemirror.net/docs/ref/#model.DOMSerializer.serializeFragment)
|
||||
method called, and you may provide an alternative object type
|
||||
implementing a compatible method.
|
||||
*/
|
||||
clipboardSerializer?: DOMSerializer;
|
||||
/**
|
||||
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`](https://prosemirror.net/docs/ref/#model.Node.textBetween) on the
|
||||
selected range.
|
||||
*/
|
||||
clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string;
|
||||
/**
|
||||
A set of [document decorations](https://prosemirror.net/docs/ref/#view.Decoration) to show in the
|
||||
view.
|
||||
*/
|
||||
decorations?: (this: P, state: EditorState) => DecorationSource | null | undefined;
|
||||
/**
|
||||
When this returns false, the content of the view is not directly
|
||||
editable.
|
||||
*/
|
||||
editable?: (this: P, state: EditorState) => boolean;
|
||||
/**
|
||||
Control the DOM attributes of the editable element. May be either
|
||||
an object or a function going from an editor state to an object.
|
||||
By default, the element will get a class `"ProseMirror"`, and
|
||||
will have its `contentEditable` attribute determined by the
|
||||
[`editable` prop](https://prosemirror.net/docs/ref/#view.EditorProps.editable). Additional classes
|
||||
provided here will be added to the class. For other attributes,
|
||||
the value provided first (as in
|
||||
[`someProp`](https://prosemirror.net/docs/ref/#view.EditorView.someProp)) will be used.
|
||||
*/
|
||||
attributes?: {
|
||||
[name: string]: string;
|
||||
} | ((state: EditorState) => {
|
||||
[name: string]: string;
|
||||
});
|
||||
/**
|
||||
Determines the distance (in pixels) between the cursor and the
|
||||
end of the visible viewport at which point, when scrolling the
|
||||
cursor into view, scrolling takes place. Defaults to 0.
|
||||
*/
|
||||
scrollThreshold?: number | {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
/**
|
||||
Determines the extra space (in pixels) that is left above or
|
||||
below the cursor when it is scrolled into view. Defaults to 5.
|
||||
*/
|
||||
scrollMargin?: number | {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
The props object given directly to the editor view supports some
|
||||
fields that can't be used in plugins:
|
||||
*/
|
||||
interface DirectEditorProps extends EditorProps {
|
||||
/**
|
||||
The current state of the editor.
|
||||
*/
|
||||
state: EditorState;
|
||||
/**
|
||||
A set of plugins to use in the view, applying their [plugin
|
||||
view](https://prosemirror.net/docs/ref/#state.PluginSpec.view) and
|
||||
[props](https://prosemirror.net/docs/ref/#state.PluginSpec.props). Passing plugins with a state
|
||||
component (a [state field](https://prosemirror.net/docs/ref/#state.PluginSpec.state) field or a
|
||||
[transaction](https://prosemirror.net/docs/ref/#state.PluginSpec.filterTransaction) filter or
|
||||
appender) will result in an error, since such plugins must be
|
||||
present in the state to work.
|
||||
*/
|
||||
plugins?: readonly Plugin[];
|
||||
/**
|
||||
The callback over which to send transactions (state updates)
|
||||
produced by the view. If you specify this, you probably want to
|
||||
make sure this ends up calling the view's
|
||||
[`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) method with a new
|
||||
state that has the transaction
|
||||
[applied](https://prosemirror.net/docs/ref/#state.EditorState.apply). The callback will be bound to have
|
||||
the view instance as its `this` binding.
|
||||
*/
|
||||
dispatchTransaction?: (tr: Transaction) => void;
|
||||
}
|
||||
|
||||
export { type DOMEventMap, Decoration, type DecorationAttrs, DecorationSet, type DecorationSource, type DirectEditorProps, type EditorProps, EditorView, type MarkViewConstructor, type NodeView, type NodeViewConstructor };
|
||||
5697
resources/app/node_modules/prosemirror-view/dist/index.js
generated
vendored
Normal file
5697
resources/app/node_modules/prosemirror-view/dist/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
41
resources/app/node_modules/prosemirror-view/package.json
generated
vendored
Normal file
41
resources/app/node_modules/prosemirror-view/package.json
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "prosemirror-view",
|
||||
"version": "1.33.6",
|
||||
"description": "ProseMirror's view component",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./style/prosemirror.css": "./style/prosemirror.css"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./style/prosemirror.css"
|
||||
],
|
||||
"style": "style/prosemirror.css",
|
||||
"license": "MIT",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Marijn Haverbeke",
|
||||
"email": "marijn@haverbeke.berlin",
|
||||
"web": "http://marijnhaverbeke.nl"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/prosemirror/prosemirror-view.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prosemirror/buildhelper": "^0.1.5",
|
||||
"prosemirror-test-builder": "^1.0.0"
|
||||
}
|
||||
}
|
||||
34
resources/app/node_modules/prosemirror-view/src/README.md
generated
vendored
Normal file
34
resources/app/node_modules/prosemirror-view/src/README.md
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
|
||||
@NodeViewConstructor
|
||||
|
||||
@MarkViewConstructor
|
||||
|
||||
@DirectEditorProps
|
||||
|
||||
@NodeView
|
||||
|
||||
@DOMEventMap
|
||||
|
||||
### Decorations
|
||||
|
||||
Decorations make it possible to influence the way the document is
|
||||
drawn, without actually changing the document.
|
||||
|
||||
@Decoration
|
||||
|
||||
@DecorationAttrs
|
||||
|
||||
@DecorationSet
|
||||
|
||||
@DecorationSource
|
||||
24
resources/app/node_modules/prosemirror-view/src/browser.ts
generated
vendored
Normal file
24
resources/app/node_modules/prosemirror-view/src/browser.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
const nav = typeof navigator != "undefined" ? navigator : null
|
||||
const doc = typeof document != "undefined" ? document : null
|
||||
const agent = (nav && nav.userAgent) || ""
|
||||
|
||||
const ie_edge = /Edge\/(\d+)/.exec(agent)
|
||||
const ie_upto10 = /MSIE \d/.exec(agent)
|
||||
const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent)
|
||||
|
||||
export const ie = !!(ie_upto10 || ie_11up || ie_edge)
|
||||
export const ie_version = ie_upto10 ? (document as any).documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0
|
||||
export const gecko = !ie && /gecko\/(\d+)/i.test(agent)
|
||||
export const gecko_version = gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1]
|
||||
|
||||
const _chrome = !ie && /Chrome\/(\d+)/.exec(agent)
|
||||
export const chrome = !!_chrome
|
||||
export const chrome_version = _chrome ? +_chrome[1] : 0
|
||||
export const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor)
|
||||
// Is true for both iOS and iPadOS for convenience
|
||||
export const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2)
|
||||
export const mac = ios || (nav ? /Mac/.test(nav.platform) : false)
|
||||
export const windows = nav ? /Win/.test(nav.platform) : false
|
||||
export const android = /Android \d/.test(agent)
|
||||
export const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style
|
||||
export const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0
|
||||
344
resources/app/node_modules/prosemirror-view/src/capturekeys.ts
generated
vendored
Normal file
344
resources/app/node_modules/prosemirror-view/src/capturekeys.ts
generated
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
import {Selection, NodeSelection, TextSelection, AllSelection, EditorState} from "prosemirror-state"
|
||||
import {EditorView} from "./index"
|
||||
import * as browser from "./browser"
|
||||
import {domIndex, selectionCollapsed, hasBlockDesc} from "./dom"
|
||||
import {selectionToDOM} from "./selection"
|
||||
|
||||
function moveSelectionBlock(state: EditorState, dir: number) {
|
||||
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: EditorView, sel: Selection) {
|
||||
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView())
|
||||
return true
|
||||
}
|
||||
|
||||
function selectHorizontally(view: EditorView, dir: number, mods: string) {
|
||||
let sel = view.state.selection
|
||||
if (sel instanceof TextSelection) {
|
||||
if (mods.indexOf("s") > -1) {
|
||||
let {$head} = sel, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter
|
||||
if (!node || node.isText || !node.isLeaf) return false
|
||||
let $newHead = view.state.doc.resolve($head.pos + node.nodeSize * (dir < 0 ? -1 : 1))
|
||||
return apply(view, new TextSelection(sel.$anchor, $newHead))
|
||||
} else if (!sel.empty) {
|
||||
return false
|
||||
} else if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) {
|
||||
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: Node) {
|
||||
return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length
|
||||
}
|
||||
|
||||
function isIgnorable(dom: Node, dir: number) {
|
||||
let desc = dom.pmViewDesc
|
||||
return desc && desc.size == 0 && (dir < 0 || dom.nextSibling || dom.nodeName != "BR")
|
||||
}
|
||||
|
||||
function skipIgnoredNodes(view: EditorView, dir: number) {
|
||||
return dir < 0 ? skipIgnoredNodesBefore(view) : skipIgnoredNodesAfter(view)
|
||||
}
|
||||
|
||||
// Make sure the cursor isn't directly after one or more ignored
|
||||
// nodes, which will confuse the browser's cursor motion logic.
|
||||
function skipIgnoredNodesBefore(view: EditorView) {
|
||||
let sel = view.domSelectionRange()
|
||||
let node = sel.focusNode!, offset = sel.focusOffset
|
||||
if (!node) return
|
||||
let moveNode, moveOffset: number | undefined, 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], -1)) force = true
|
||||
for (;;) {
|
||||
if (offset > 0) {
|
||||
if (node.nodeType != 1) {
|
||||
break
|
||||
} else {
|
||||
let before = node.childNodes[offset - 1]
|
||||
if (isIgnorable(before, -1)) {
|
||||
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, -1)) {
|
||||
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, node, offset)
|
||||
else if (moveNode) setSelFocus(view, moveNode, moveOffset!)
|
||||
}
|
||||
|
||||
// Make sure the cursor isn't directly before one or more ignored
|
||||
// nodes.
|
||||
function skipIgnoredNodesAfter(view: EditorView) {
|
||||
let sel = view.domSelectionRange()
|
||||
let node = sel.focusNode!, offset = sel.focusOffset
|
||||
if (!node) return
|
||||
let len = nodeLen(node)
|
||||
let moveNode, moveOffset: number | undefined
|
||||
for (;;) {
|
||||
if (offset < len) {
|
||||
if (node.nodeType != 1) break
|
||||
let after = node.childNodes[offset]
|
||||
if (isIgnorable(after, 1)) {
|
||||
moveNode = node
|
||||
moveOffset = ++offset
|
||||
}
|
||||
else break
|
||||
} else if (isBlockNode(node)) {
|
||||
break
|
||||
} else {
|
||||
let next = node.nextSibling
|
||||
while (next && isIgnorable(next, 1)) {
|
||||
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, moveNode, moveOffset!)
|
||||
}
|
||||
|
||||
function isBlockNode(dom: Node) {
|
||||
let desc = dom.pmViewDesc
|
||||
return desc && desc.node && desc.node.isBlock
|
||||
}
|
||||
|
||||
function textNodeAfter(node: Node | null, offset: number): Text | undefined {
|
||||
while (node && offset == node.childNodes.length && !hasBlockDesc(node)) {
|
||||
offset = domIndex(node) + 1
|
||||
node = node.parentNode
|
||||
}
|
||||
while (node && offset < node.childNodes.length) {
|
||||
let next = node.childNodes[offset]
|
||||
if (next.nodeType == 3) return next as Text
|
||||
if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break
|
||||
node = next
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
function textNodeBefore(node: Node | null, offset: number): Text | undefined {
|
||||
while (node && !offset && !hasBlockDesc(node)) {
|
||||
offset = domIndex(node)
|
||||
node = node.parentNode
|
||||
}
|
||||
while (node && offset) {
|
||||
let next = node.childNodes[offset - 1]
|
||||
if (next.nodeType == 3) return next as Text
|
||||
if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break
|
||||
node = next
|
||||
offset = node.childNodes.length
|
||||
}
|
||||
}
|
||||
|
||||
function setSelFocus(view: EditorView, node: Node, offset: number) {
|
||||
if (node.nodeType != 3) {
|
||||
let before, after
|
||||
if (after = textNodeAfter(node, offset)) {
|
||||
node = after
|
||||
offset = 0
|
||||
} else if (before = textNodeBefore(node, offset)) {
|
||||
node = before
|
||||
offset = before.nodeValue!.length
|
||||
}
|
||||
}
|
||||
|
||||
let sel = view.domSelection()
|
||||
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)
|
||||
}
|
||||
|
||||
function findDirection(view: EditorView, pos: number): "rtl" | "ltr" {
|
||||
let $pos = view.state.doc.resolve(pos)
|
||||
if (!(browser.chrome || browser.windows) && $pos.parent.inlineContent) {
|
||||
let coords = view.coordsAtPos(pos)
|
||||
if (pos > $pos.start()) {
|
||||
let before = view.coordsAtPos(pos - 1)
|
||||
let mid = (before.top + before.bottom) / 2
|
||||
if (mid > coords.top && mid < coords.bottom && Math.abs(before.left - coords.left) > 1)
|
||||
return before.left < coords.left ? "ltr" : "rtl"
|
||||
}
|
||||
if (pos < $pos.end()) {
|
||||
let after = view.coordsAtPos(pos + 1)
|
||||
let mid = (after.top + after.bottom) / 2
|
||||
if (mid > coords.top && mid < coords.bottom && Math.abs(after.left - coords.left) > 1)
|
||||
return after.left > coords.left ? "ltr" : "rtl"
|
||||
}
|
||||
}
|
||||
let computed = getComputedStyle(view.dom).direction
|
||||
return computed == "rtl" ? "rtl" : "ltr"
|
||||
}
|
||||
|
||||
// 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: EditorView, dir: number, mods: string) {
|
||||
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: EditorView, dir: number) {
|
||||
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: EditorView, node: HTMLElement, state: string) {
|
||||
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: EditorView) {
|
||||
if (!browser.safari || view.state.selection.$head.parentOffset > 0) return false
|
||||
let {focusNode, focusOffset} = view.domSelectionRange()
|
||||
if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
|
||||
focusNode.firstChild && (focusNode.firstChild as HTMLElement).contentEditable == "false") {
|
||||
let child = focusNode.firstChild as HTMLElement
|
||||
switchEditable(view, child, "true")
|
||||
setTimeout(() => switchEditable(view, child, "false"), 20)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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: KeyboardEvent) {
|
||||
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: EditorView, event: KeyboardEvent) {
|
||||
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) || skipIgnoredNodes(view, -1)
|
||||
} else if ((code == 46 && !event.shiftKey) || (browser.mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac
|
||||
return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodes(view, 1)
|
||||
} else if (code == 13 || code == 27) { // Enter, Esc
|
||||
return true
|
||||
} else if (code == 37 || (browser.mac && code == 66 && mods == "c")) { // Left arrow, Ctrl-b on Mac
|
||||
let dir = code == 37 ? (findDirection(view, view.state.selection.from) == "ltr" ? -1 : 1) : -1
|
||||
return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir)
|
||||
} else if (code == 39 || (browser.mac && code == 70 && mods == "c")) { // Right arrow, Ctrl-f on Mac
|
||||
let dir = code == 39 ? (findDirection(view, view.state.selection.from) == "ltr" ? 1 : -1) : 1
|
||||
return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir)
|
||||
} else if (code == 38 || (browser.mac && code == 80 && mods == "c")) { // Up arrow, Ctrl-p on Mac
|
||||
return selectVertically(view, -1, mods) || skipIgnoredNodes(view, -1)
|
||||
} else if (code == 40 || (browser.mac && code == 78 && mods == "c")) { // Down arrow, Ctrl-n on Mac
|
||||
return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodes(view, 1)
|
||||
} else if (mods == (browser.mac ? "m" : "c") &&
|
||||
(code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
246
resources/app/node_modules/prosemirror-view/src/clipboard.ts
generated
vendored
Normal file
246
resources/app/node_modules/prosemirror-view/src/clipboard.ts
generated
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
import {Slice, Fragment, DOMParser, DOMSerializer, ResolvedPos, NodeType, Node} from "prosemirror-model"
|
||||
import * as browser from "./browser"
|
||||
import {EditorView} from "./index"
|
||||
|
||||
export function serializeForClipboard(view: EditorView, slice: Slice) {
|
||||
view.someProp("transformCopied", f => { slice = f(slice!, view) })
|
||||
|
||||
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, wrappers = 0
|
||||
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)
|
||||
wrappers++
|
||||
}
|
||||
firstChild = wrap.firstChild
|
||||
}
|
||||
|
||||
if (firstChild && firstChild.nodeType == 1)
|
||||
(firstChild as HTMLElement).setAttribute(
|
||||
"data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`)
|
||||
|
||||
let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
|
||||
slice.content.textBetween(0, slice.content.size, "\n\n")
|
||||
|
||||
return {dom: wrap, text, slice}
|
||||
}
|
||||
|
||||
// Read a slice of content from the clipboard (or drop data).
|
||||
export function parseFromClipboard(view: EditorView, text: string, html: string | null, plainText: boolean, $context: ResolvedPos) {
|
||||
let inCode = $context.parent.type.spec.code
|
||||
let dom: HTMLElement | undefined, slice: Slice | undefined
|
||||
if (!html && !text) return null
|
||||
let asText = text && (plainText || inCode || !html)
|
||||
if (asText) {
|
||||
view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view) })
|
||||
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, view))
|
||||
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!, view) })
|
||||
dom = readHTML(html!)
|
||||
if (browser.webkit) restoreReplacedSpaces(dom)
|
||||
}
|
||||
|
||||
let contextNode = dom && dom.querySelector("[data-pm-slice]")
|
||||
let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "")
|
||||
if (sliceData && sliceData[3]) for (let i = +sliceData[3]; i > 0; i--) {
|
||||
let child = dom!.firstChild
|
||||
while (child && child.nodeType != 1) child = child.nextSibling
|
||||
if (!child) break
|
||||
dom = child as HTMLElement
|
||||
}
|
||||
|
||||
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}
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
if (sliceData) {
|
||||
slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4])
|
||||
} 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!, view) })
|
||||
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: Fragment, $context: ResolvedPos) {
|
||||
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: readonly NodeType[] | undefined, result: Node[] | null = []
|
||||
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)!
|
||||
lastWrap = wrap
|
||||
}
|
||||
})
|
||||
if (result) return Fragment.from(result)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
function withWrappers(node: Node, wrap: readonly NodeType[], 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: readonly NodeType[], lastWrap: readonly NodeType[],
|
||||
node: Node, sibling: Node, depth: number): Node | undefined {
|
||||
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: Node, depth: number) {
|
||||
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: Fragment, side: number, from: number, to: number, depth: number, openEnd: number) {
|
||||
let node = side < 0 ? fragment.firstChild! : fragment.lastChild!, inner = node.content
|
||||
if (fragment.childCount > 1) openEnd = 0
|
||||
if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd)
|
||||
if (depth >= from)
|
||||
inner = side < 0 ? node.contentMatchAt(0)!.fillBefore(inner, 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: Slice, openStart: number, openEnd: number) {
|
||||
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 =
|
||||
// "<td>..</td>"` the table cells are ignored.
|
||||
const wrapMap: {[node: string]: string[]} = {
|
||||
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: Document | null = null
|
||||
function detachedDoc() {
|
||||
return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"))
|
||||
}
|
||||
|
||||
function readHTML(html: string) {
|
||||
let metas = /^(\s*<meta [^>]*>)*/.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: HTMLElement) {
|
||||
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: Slice, context: string) {
|
||||
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)
|
||||
}
|
||||
772
resources/app/node_modules/prosemirror-view/src/decoration.ts
generated
vendored
Normal file
772
resources/app/node_modules/prosemirror-view/src/decoration.ts
generated
vendored
Normal file
@@ -0,0 +1,772 @@
|
||||
import {Node, Mark} from "prosemirror-model"
|
||||
import {Mappable, Mapping} from "prosemirror-transform"
|
||||
import {EditorView} from "./index"
|
||||
import {DOMNode} from "./dom"
|
||||
|
||||
function compareObjs(a: {[prop: string]: any}, b: {[prop: string]: any}) {
|
||||
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
|
||||
}
|
||||
|
||||
export interface DecorationType {
|
||||
spec: any
|
||||
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null
|
||||
valid(node: Node, span: Decoration): boolean
|
||||
eq(other: DecorationType): boolean
|
||||
destroy(dom: DOMNode): void
|
||||
}
|
||||
|
||||
export type WidgetConstructor = ((view: EditorView, getPos: () => number | undefined) => DOMNode) | DOMNode
|
||||
|
||||
export class WidgetType implements DecorationType {
|
||||
spec: any
|
||||
side: number
|
||||
|
||||
constructor(readonly toDOM: WidgetConstructor, spec: any) {
|
||||
this.spec = spec || noSpec
|
||||
this.side = this.spec.side || 0
|
||||
}
|
||||
|
||||
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
||||
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: WidgetType) {
|
||||
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: DOMNode) {
|
||||
if (this.spec.destroy) this.spec.destroy(node)
|
||||
}
|
||||
}
|
||||
|
||||
export class InlineType implements DecorationType {
|
||||
spec: any
|
||||
|
||||
constructor(readonly attrs: DecorationAttrs, spec: any) {
|
||||
this.spec = spec || noSpec
|
||||
}
|
||||
|
||||
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
||||
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(_: Node, span: Decoration) { return span.from < span.to }
|
||||
|
||||
eq(other: DecorationType): boolean {
|
||||
return this == other ||
|
||||
(other instanceof InlineType && compareObjs(this.attrs, other.attrs) &&
|
||||
compareObjs(this.spec, other.spec))
|
||||
}
|
||||
|
||||
static is(span: Decoration) { return span.type instanceof InlineType }
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
export class NodeType implements DecorationType {
|
||||
spec: any
|
||||
constructor(readonly attrs: DecorationAttrs, spec: any) {
|
||||
this.spec = spec || noSpec
|
||||
}
|
||||
|
||||
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
||||
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: Node, span: Decoration): boolean {
|
||||
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: DecorationType): boolean {
|
||||
return this == other ||
|
||||
(other instanceof NodeType && compareObjs(this.attrs, other.attrs) &&
|
||||
compareObjs(this.spec, other.spec))
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The start position of the decoration.
|
||||
readonly from: number,
|
||||
/// The end position. Will be the same as `from` for [widget
|
||||
/// decorations](#view.Decoration^widget).
|
||||
readonly to: number,
|
||||
/// @internal
|
||||
readonly type: DecorationType
|
||||
) {}
|
||||
|
||||
/// @internal
|
||||
copy(from: number, to: number) {
|
||||
return new Decoration(from, to, this.type)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
eq(other: Decoration, offset = 0) {
|
||||
return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to
|
||||
}
|
||||
|
||||
/// @internal
|
||||
map(mapping: Mappable, offset: number, oldOffset: number) {
|
||||
return this.type.map(mapping, this, offset, oldOffset)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
static widget(pos: number, toDOM: WidgetConstructor, spec?: {
|
||||
/// 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.
|
||||
side?: number
|
||||
|
||||
/// The precise set of marks to draw around the widget.
|
||||
marks?: readonly Mark[]
|
||||
|
||||
/// Can be used to control which DOM events, when they bubble out
|
||||
/// of this widget, the editor view should ignore.
|
||||
stopEvent?: (event: Event) => boolean
|
||||
|
||||
/// 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.
|
||||
ignoreSelection?: boolean
|
||||
|
||||
/// 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.
|
||||
key?: string
|
||||
|
||||
/// Called when the widget decoration is removed or the editor is
|
||||
/// destroyed.
|
||||
destroy?: (node: DOMNode) => void
|
||||
|
||||
/// Specs allow arbitrary additional properties.
|
||||
[key: string]: any
|
||||
}): Decoration {
|
||||
return new Decoration(pos, pos, new WidgetType(toDOM, spec))
|
||||
}
|
||||
|
||||
/// Creates an inline decoration, which adds the given attributes to
|
||||
/// each inline node between `from` and `to`.
|
||||
static inline(from: number, to: number, attrs: DecorationAttrs, spec?: {
|
||||
/// 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.
|
||||
inclusiveStart?: boolean
|
||||
|
||||
/// Determines how the right side of the decoration is mapped.
|
||||
/// See
|
||||
/// [`inclusiveStart`](#view.Decoration^inline^spec.inclusiveStart).
|
||||
inclusiveEnd?: boolean
|
||||
|
||||
/// Specs may have arbitrary additional properties.
|
||||
[key: string]: any
|
||||
}) {
|
||||
return new Decoration(from, to, new InlineType(attrs, spec))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
static node(from: number, to: number, attrs: DecorationAttrs, spec?: any) {
|
||||
return new Decoration(from, to, new NodeType(attrs, spec))
|
||||
}
|
||||
|
||||
/// 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 }
|
||||
|
||||
/// @internal
|
||||
get inline() { return this.type instanceof InlineType }
|
||||
|
||||
/// @internal
|
||||
get widget() { return this.type instanceof WidgetType }
|
||||
}
|
||||
|
||||
/// 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:
|
||||
export type DecorationAttrs = {
|
||||
/// When non-null, the target node is wrapped in a DOM element of
|
||||
/// this type (and the other attributes are applied to this element).
|
||||
nodeName?: string
|
||||
|
||||
/// A CSS class name or a space-separated set of class names to be
|
||||
/// _added_ to the classes that the node already had.
|
||||
class?: string
|
||||
|
||||
/// A string of CSS to be _added_ to the node's existing `style` property.
|
||||
style?: string
|
||||
|
||||
/// Any other properties are treated as regular DOM attributes.
|
||||
[attribute: string]: string | undefined
|
||||
}
|
||||
|
||||
const none: readonly any[] = [], noSpec = {}
|
||||
|
||||
/// An object that can [provide](#view.EditorProps.decorations)
|
||||
/// decorations. Implemented by [`DecorationSet`](#view.DecorationSet),
|
||||
/// and passed to [node views](#view.EditorProps.nodeViews).
|
||||
export interface DecorationSource {
|
||||
/// Map the set of decorations in response to a change in the
|
||||
/// document.
|
||||
map: (mapping: Mapping, node: Node) => DecorationSource
|
||||
/// @internal
|
||||
locals(node: Node): readonly Decoration[]
|
||||
/// Extract a DecorationSource containing decorations for the given child node at the given offset.
|
||||
forChild(offset: number, child: Node): DecorationSource
|
||||
/// @internal
|
||||
eq(other: DecorationSource): boolean
|
||||
}
|
||||
|
||||
/// 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 implements DecorationSource {
|
||||
/// @internal
|
||||
local: readonly Decoration[]
|
||||
/// @internal
|
||||
children: readonly (number | DecorationSet)[]
|
||||
|
||||
/// @internal
|
||||
constructor(local: readonly Decoration[], children: readonly (number | DecorationSet)[]) {
|
||||
this.local = local.length ? local : none
|
||||
this.children = children.length ? children : none
|
||||
}
|
||||
|
||||
/// Create a set of decorations, using the structure of the given
|
||||
/// document. This will consume (modify) the `decorations` array, so
|
||||
/// you must make a copy if you want need to preserve that.
|
||||
static create(doc: Node, decorations: Decoration[]) {
|
||||
return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty
|
||||
}
|
||||
|
||||
/// 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?: number, end?: number, predicate?: (spec: any) => boolean): Decoration[] {
|
||||
let result: Decoration[] = []
|
||||
this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate)
|
||||
return result
|
||||
}
|
||||
|
||||
private findInner(start: number, end: number, result: Decoration[], offset: number, predicate?: (spec: any) => boolean) {
|
||||
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] as number) < end && (this.children[i + 1] as number) > start) {
|
||||
let childOff = (this.children[i] as number) + 1
|
||||
;(this.children[i + 2] as DecorationSet).findInner(start - childOff, end - childOff,
|
||||
result, offset + childOff, predicate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the set of decorations in response to a change in the
|
||||
/// document.
|
||||
map(mapping: Mapping, doc: Node, options?: {
|
||||
/// 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.
|
||||
onRemove?: (decorationSpec: any) => void
|
||||
}) {
|
||||
if (this == empty || mapping.maps.length == 0) return this
|
||||
return this.mapInner(mapping, doc, 0, 0, options || noSpec)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
mapInner(mapping: Mapping, node: Node, offset: number, oldOffset: number, options: {
|
||||
onRemove?: (decorationSpec: any) => void
|
||||
}) {
|
||||
let newLocal: Decoration[] | undefined
|
||||
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), none) : empty
|
||||
}
|
||||
|
||||
/// Add the given array of decorations to the ones in the set,
|
||||
/// producing a new set. Consumes the `decorations` array. Needs
|
||||
/// access to the current document to create the appropriate tree
|
||||
/// structure.
|
||||
add(doc: Node, decorations: Decoration[]) {
|
||||
if (!decorations.length) return this
|
||||
if (this == empty) return DecorationSet.create(doc, decorations)
|
||||
return this.addInner(doc, decorations, 0)
|
||||
}
|
||||
|
||||
private addInner(doc: Node, decorations: Decoration[], offset: number) {
|
||||
let children: (number | DecorationSet)[] | undefined, 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] as number) < childOffset) childIndex += 3
|
||||
if (children[childIndex] == childOffset)
|
||||
children[childIndex + 2] = (children[childIndex + 2] as DecorationSet).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)
|
||||
}
|
||||
|
||||
/// Create a new set that contains the decorations in this set, minus
|
||||
/// the ones in the given array.
|
||||
remove(decorations: Decoration[]) {
|
||||
if (decorations.length == 0 || this == empty) return this
|
||||
return this.removeInner(decorations, 0)
|
||||
}
|
||||
|
||||
private removeInner(decorations: (Decoration | null)[], offset: number) {
|
||||
let children = this.children as (number | DecorationSet)[], local = this.local as Decoration[]
|
||||
for (let i = 0; i < children.length; i += 3) {
|
||||
let found: Decoration[] | undefined
|
||||
let from = (children[i] as number) + offset, to = (children[i + 1] as number) + 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] as DecorationSet).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: number, node: Node): DecorationSet | DecorationGroup {
|
||||
if (this == empty) return this
|
||||
if (node.isLeaf) return DecorationSet.empty
|
||||
|
||||
let child, local: Decoration[] | undefined
|
||||
for (let i = 0; i < this.children.length; i += 3) if ((this.children[i] as number) >= offset) {
|
||||
if (this.children[i] == offset) child = this.children[i + 2] as DecorationSet
|
||||
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), none)
|
||||
return child ? new DecorationGroup([localSet, child]) : localSet
|
||||
}
|
||||
return child || empty
|
||||
}
|
||||
|
||||
/// @internal
|
||||
eq(other: DecorationSet) {
|
||||
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] as DecorationSet).eq(other.children[i + 2] as DecorationSet))
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// @internal
|
||||
locals(node: Node) {
|
||||
return removeOverlap(this.localsInner(node))
|
||||
}
|
||||
|
||||
/// @internal
|
||||
localsInner(node: Node): readonly Decoration[] {
|
||||
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
|
||||
}
|
||||
|
||||
/// The empty set of decorations.
|
||||
static empty: DecorationSet = new DecorationSet([], [])
|
||||
|
||||
/// @internal
|
||||
static removeOverlap = removeOverlap
|
||||
}
|
||||
|
||||
const empty = DecorationSet.empty
|
||||
|
||||
// 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 implements DecorationSource {
|
||||
constructor(readonly members: readonly DecorationSet[]) {}
|
||||
|
||||
map(mapping: Mapping, doc: Node) {
|
||||
const mappedDecos = this.members.map(
|
||||
member => member.map(mapping, doc, noSpec)
|
||||
)
|
||||
return DecorationGroup.from(mappedDecos)
|
||||
}
|
||||
|
||||
forChild(offset: number, child: Node) {
|
||||
if (child.isLeaf) return DecorationSet.empty
|
||||
let found: DecorationSet[] = []
|
||||
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: DecorationGroup) {
|
||||
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: Node) {
|
||||
let result: Decoration[] | undefined, 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 as Decoration[]
|
||||
} 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
|
||||
}
|
||||
|
||||
// Create a group for the given array of decoration sets, or return
|
||||
// a single set when possible.
|
||||
static from(members: readonly DecorationSource[]): DecorationSource {
|
||||
switch (members.length) {
|
||||
case 0: return empty
|
||||
case 1: return members[0]
|
||||
default: return new DecorationGroup(
|
||||
members.every(m => m instanceof DecorationSet) ? members as DecorationSet[] :
|
||||
members.reduce((r, m) => r.concat(m instanceof DecorationSet ? m : (m as DecorationGroup).members),
|
||||
[] as DecorationSet[]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapChildren(
|
||||
oldChildren: readonly (number | DecorationSet)[],
|
||||
newLocal: Decoration[],
|
||||
mapping: Mapping,
|
||||
node: Node,
|
||||
offset: number,
|
||||
oldOffset: number,
|
||||
options: {onRemove?: (decorationSpec: any) => void}
|
||||
) {
|
||||
let children = oldChildren.slice() as (number | DecorationSet)[]
|
||||
|
||||
// Mark the children that are directly touched by changes, and
|
||||
// move those that are after the changes.
|
||||
for (let i = 0, baseOffset = oldOffset; i < mapping.maps.length; i++) {
|
||||
let moved = 0
|
||||
mapping.maps[i].forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
|
||||
let dSize = (newEnd - newStart) - (oldEnd - oldStart)
|
||||
for (let i = 0; i < children.length; i += 3) {
|
||||
let end = children[i + 1] as number
|
||||
if (end < 0 || oldStart > end + baseOffset - moved) continue
|
||||
let start = (children[i] as number) + baseOffset - moved
|
||||
if (oldEnd >= start) {
|
||||
children[i + 1] = oldStart <= start ? -2 : -1
|
||||
} else if (oldStart >= baseOffset && dSize) {
|
||||
;(children[i] as number) += dSize
|
||||
;(children[i + 1] as number) += dSize
|
||||
}
|
||||
}
|
||||
moved += dSize
|
||||
})
|
||||
baseOffset = mapping.maps[i].map(baseOffset, -1)
|
||||
}
|
||||
|
||||
// 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] as number) < 0) { // Touched nodes
|
||||
if (children[i + 1] == -2) {
|
||||
mustRebuild = true
|
||||
children[i + 1] = -1
|
||||
continue
|
||||
}
|
||||
let from = mapping.map((oldChildren[i] as number) + 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] as number) + 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] as DecorationSet)
|
||||
.mapInner(mapping, childNode, from + 1, (oldChildren[i] as number) + 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 as Decoration[]
|
||||
for (let i = 0; i < children.length; i += 3) if ((children[i + 1] as number) < 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.sort(byPos), children)
|
||||
}
|
||||
|
||||
function moveSpans(spans: Decoration[], offset: number) {
|
||||
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: (number | DecorationSet)[],
|
||||
oldChildren: readonly (number | DecorationSet)[],
|
||||
decorations: Decoration[],
|
||||
mapping: Mapping,
|
||||
offset: number,
|
||||
oldOffset: number,
|
||||
options: {onRemove?: (decorationSpec: any) => void}
|
||||
) {
|
||||
// Gather all decorations from the remaining marked children
|
||||
function gather(set: DecorationSet, oldOffset: number) {
|
||||
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] as DecorationSet, set.children[i] as number + oldOffset + 1)
|
||||
}
|
||||
for (let i = 0; i < children.length; i += 3) if (children[i + 1] == -1)
|
||||
gather(children[i + 2] as DecorationSet, oldChildren[i] as number + oldOffset + 1)
|
||||
|
||||
return decorations
|
||||
}
|
||||
|
||||
function takeSpansForNode(spans: (Decoration | null)[], node: Node, offset: number): Decoration[] | null {
|
||||
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<T>(array: readonly (T | null)[]): T[] {
|
||||
let result: T[] = []
|
||||
for (let i = 0; i < array.length; i++)
|
||||
if (array[i] != null) result.push(array[i]!)
|
||||
return result
|
||||
}
|
||||
|
||||
// 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: Decoration[],
|
||||
node: Node,
|
||||
offset: number,
|
||||
options: {onRemove?: (decorationSpec: any) => void}
|
||||
) {
|
||||
let children: (DecorationSet | number)[] = [], 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
|
||||
}
|
||||
|
||||
// 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: Decoration, b: Decoration) {
|
||||
return a.from - b.from || a.to - b.to
|
||||
}
|
||||
|
||||
// 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: readonly Decoration[]): Decoration[] {
|
||||
let working: Decoration[] = spans as Decoration[]
|
||||
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: Decoration[], i: number, deco: Decoration) {
|
||||
while (i < array.length && byPos(deco, array[i]) > 0) i++
|
||||
array.splice(i, 0, deco)
|
||||
}
|
||||
|
||||
// Get the decorations associated with the current props of a view.
|
||||
export function viewDecorations(view: EditorView): DecorationSource {
|
||||
let found: DecorationSource[] = []
|
||||
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)
|
||||
}
|
||||
151
resources/app/node_modules/prosemirror-view/src/dom.ts
generated
vendored
Normal file
151
resources/app/node_modules/prosemirror-view/src/dom.ts
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
export type DOMNode = InstanceType<typeof window.Node>
|
||||
export type DOMSelection = InstanceType<typeof window.Selection>
|
||||
export type DOMSelectionRange = {
|
||||
focusNode: DOMNode | null, focusOffset: number,
|
||||
anchorNode: DOMNode | null, anchorOffset: number
|
||||
}
|
||||
|
||||
export const domIndex = function(node: Node) {
|
||||
for (var index = 0;; index++) {
|
||||
node = node.previousSibling!
|
||||
if (!node) return index
|
||||
}
|
||||
}
|
||||
|
||||
export const parentNode = function(node: Node): Node | null {
|
||||
let parent = (node as HTMLSlotElement).assignedSlot || node.parentNode
|
||||
return parent && parent.nodeType == 11 ? (parent as ShadowRoot).host : parent
|
||||
}
|
||||
|
||||
let reusedRange: Range | null = 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: Text, from?: number, to?: number) {
|
||||
let range = reusedRange || (reusedRange = document.createRange())
|
||||
range.setEnd(node, to == null ? node.nodeValue!.length : to)
|
||||
range.setStart(node, from || 0)
|
||||
return range
|
||||
}
|
||||
|
||||
export const clearReusedRange = function() {
|
||||
reusedRange = null;
|
||||
}
|
||||
|
||||
// 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: Node, off: number, targetNode: Node, targetOff: number) {
|
||||
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: Node, off: number, targetNode: Node, targetOff: number, dir: number) {
|
||||
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 as HTMLElement).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 as HTMLElement).contentEditable == "false") return false
|
||||
off = dir < 0 ? nodeSize(node) : 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function nodeSize(node: Node) {
|
||||
return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length
|
||||
}
|
||||
|
||||
export function textNodeBefore(node: Node, offset: number) {
|
||||
for (;;) {
|
||||
if (node.nodeType == 3 && offset) return node as Text
|
||||
if (node.nodeType == 1 && offset > 0) {
|
||||
if ((node as HTMLElement).contentEditable == "false") return null
|
||||
node = node.childNodes[offset - 1]
|
||||
offset = nodeSize(node)
|
||||
} else if (node.parentNode && !hasBlockDesc(node)) {
|
||||
offset = domIndex(node)
|
||||
node = node.parentNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function textNodeAfter(node: Node, offset: number) {
|
||||
for (;;) {
|
||||
if (node.nodeType == 3 && offset < node.nodeValue!.length) return node as Text
|
||||
if (node.nodeType == 1 && offset < node.childNodes.length) {
|
||||
if ((node as HTMLElement).contentEditable == "false") return null
|
||||
node = node.childNodes[offset]
|
||||
offset = 0
|
||||
} else if (node.parentNode && !hasBlockDesc(node)) {
|
||||
offset = domIndex(node) + 1
|
||||
node = node.parentNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isOnEdge(node: Node, offset: number, parent: Node) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export function hasBlockDesc(dom: Node) {
|
||||
let desc
|
||||
for (let cur: Node | null = 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: DOMSelectionRange) {
|
||||
return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset,
|
||||
domSel.anchorNode!, domSel.anchorOffset)
|
||||
}
|
||||
|
||||
export function keyEvent(keyCode: number, key: string) {
|
||||
let event = document.createEvent("Event") as KeyboardEvent
|
||||
event.initEvent("keydown", true, true)
|
||||
;(event as any).keyCode = keyCode
|
||||
;(event as any).key = (event as any).code = key
|
||||
return event
|
||||
}
|
||||
|
||||
export function deepActiveElement(doc: Document) {
|
||||
let elt = doc.activeElement
|
||||
while (elt && elt.shadowRoot) elt = elt.shadowRoot.activeElement
|
||||
return elt
|
||||
}
|
||||
|
||||
export function caretFromPoint(doc: Document, x: number, y: number): {node: Node, offset: number} | undefined {
|
||||
if ((doc as any).caretPositionFromPoint) {
|
||||
try { // Firefox throws for this call in hard-to-predict circumstances (#994)
|
||||
let pos = (doc as any).caretPositionFromPoint(x, y)
|
||||
if (pos) return {node: pos.offsetNode, offset: pos.offset}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (doc.caretRangeFromPoint) {
|
||||
let range = doc.caretRangeFromPoint(x, y)
|
||||
if (range) return {node: range.startContainer, offset: range.startOffset}
|
||||
}
|
||||
}
|
||||
375
resources/app/node_modules/prosemirror-view/src/domchange.ts
generated
vendored
Normal file
375
resources/app/node_modules/prosemirror-view/src/domchange.ts
generated
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
import {Fragment, DOMParser, TagParseRule, Node, Mark, ResolvedPos} from "prosemirror-model"
|
||||
import {Selection, TextSelection} from "prosemirror-state"
|
||||
|
||||
import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection"
|
||||
import {selectionCollapsed, keyEvent, DOMNode} from "./dom"
|
||||
import * as browser from "./browser"
|
||||
import {EditorView} from "./index"
|
||||
|
||||
// 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: EditorView, from_: number, to_: number) {
|
||||
let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
|
||||
|
||||
let domSel = view.domSelectionRange()
|
||||
let find: {node: DOMNode, offset: number, pos?: number}[] | undefined
|
||||
let 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.input.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,
|
||||
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: DOMNode): Omit<TagParseRule, "tag"> | null {
|
||||
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} as any
|
||||
} else if (dom.parentNode.lastChild == dom || browser.safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
|
||||
return {ignore: true}
|
||||
}
|
||||
} else if (dom.nodeName == "IMG" && (dom as HTMLElement).getAttribute("mark-placeholder")) {
|
||||
return {ignore: true}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i
|
||||
|
||||
export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) {
|
||||
let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0)
|
||||
view.input.compositionPendingChanges = 0
|
||||
|
||||
if (from < 0) {
|
||||
let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null
|
||||
let newSel = selectionFromDOM(view, origin)
|
||||
if (newSel && !view.state.selection.eq(newSel)) {
|
||||
if (browser.chrome && browser.android &&
|
||||
view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))))
|
||||
return
|
||||
let tr = view.state.tr.setSelection(newSel)
|
||||
if (origin == "pointer") tr.setMeta("pointer", true)
|
||||
else if (origin == "key") tr.scrollIntoView()
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
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)
|
||||
|
||||
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to)
|
||||
let preferredPos, preferredSide: "start" | "end"
|
||||
// Prefer anchoring to end when Backspace is pressed
|
||||
if (view.input.lastKeyCode === 8 && Date.now() - 100 < view.input.lastKeyCodeTime) {
|
||||
preferredPos = view.state.selection.to
|
||||
preferredSide = "end"
|
||||
} else {
|
||||
preferredPos = view.state.selection.from
|
||||
preferredSide = "start"
|
||||
}
|
||||
view.input.lastKeyCode = null
|
||||
|
||||
let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide)
|
||||
if ((browser.ios && view.input.lastIOSEnter > Date.now() - 225 || browser.android) &&
|
||||
addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
|
||||
(!change || change.endA >= change.endB) &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
|
||||
view.input.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)) {
|
||||
let tr = view.state.tr.setSelection(sel)
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
view.input.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 $fromA = doc.resolve(change.start)
|
||||
let inlineChange = $from.sameParent($to) && $from.parent.inlineContent && $fromA.end() >= change.endA
|
||||
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.input.lastIOSEnter > Date.now() - 225 &&
|
||||
(!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
|
||||
(!inlineChange && $from.pos < parse.doc.content.size && !$from.sameParent($to) &&
|
||||
(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.input.lastIOSEnter = 0
|
||||
return
|
||||
}
|
||||
// Same for backspace
|
||||
if (view.state.selection.anchor > change.start &&
|
||||
looksLikeBackspace(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.endB == change.start)
|
||||
view.input.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
|
||||
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 &&
|
||||
(markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset),
|
||||
$fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.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.input.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)
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
view.dispatch(tr.scrollIntoView())
|
||||
}
|
||||
|
||||
function resolveSelection(view: EditorView, doc: Node, parsedSel: {anchor: number, head: number}) {
|
||||
if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null
|
||||
return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head))
|
||||
}
|
||||
|
||||
// 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: Fragment, prev: Fragment) {
|
||||
let curMarks = cur.firstChild!.marks, prevMarks = prev.firstChild!.marks
|
||||
let added = curMarks, removed = prevMarks, type, mark: Mark | undefined, 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) => node.mark(mark!.addToSet(node.marks))
|
||||
} else if (added.length == 0 && removed.length == 1) {
|
||||
mark = removed[0]
|
||||
type = "remove"
|
||||
update = (node: 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 looksLikeBackspace(old: Node, start: number, end: number, $newStart: ResolvedPos, $newEnd: ResolvedPos) {
|
||||
if (// 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)
|
||||
|
||||
// Handle the case where, rather than joining blocks, the change just removed an entire block
|
||||
if (!$newStart.parent.isTextblock) {
|
||||
let after = $start.nodeAfter
|
||||
return after != null && end == start + after.nodeSize
|
||||
}
|
||||
|
||||
// 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: ResolvedPos, fromEnd: boolean, mayOpen: boolean) {
|
||||
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: Fragment, b: Fragment, pos: number, preferredPos: number, preferredSide: "start" | "end") {
|
||||
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
|
||||
if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1)))
|
||||
start += move ? 1 : -1
|
||||
endB = start + (endB - endA)
|
||||
endA = start
|
||||
} else if (endB < start) {
|
||||
let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0
|
||||
start -= move
|
||||
if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1)))
|
||||
start += move ? 1 : -1
|
||||
endA = start + (endA - endB)
|
||||
endB = start
|
||||
}
|
||||
return {start, endA, endB}
|
||||
}
|
||||
|
||||
function isSurrogatePair(str: string) {
|
||||
if (str.length != 2) return false
|
||||
let a = str.charCodeAt(0), b = str.charCodeAt(1)
|
||||
return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF
|
||||
}
|
||||
507
resources/app/node_modules/prosemirror-view/src/domcoords.ts
generated
vendored
Normal file
507
resources/app/node_modules/prosemirror-view/src/domcoords.ts
generated
vendored
Normal file
@@ -0,0 +1,507 @@
|
||||
import {EditorState} from "prosemirror-state"
|
||||
import {nodeSize, textRange, parentNode, caretFromPoint} from "./dom"
|
||||
import * as browser from "./browser"
|
||||
import {EditorView} from "./index"
|
||||
|
||||
export type Rect = {left: number, right: number, top: number, bottom: number}
|
||||
|
||||
function windowRect(doc: Document): Rect {
|
||||
let vp = doc.defaultView && doc.defaultView.visualViewport
|
||||
if (vp) return {
|
||||
left: 0, right: vp.width,
|
||||
top: 0, bottom: vp.height
|
||||
}
|
||||
return {left: 0, right: doc.documentElement.clientWidth,
|
||||
top: 0, bottom: doc.documentElement.clientHeight}
|
||||
}
|
||||
|
||||
function getSide(value: number | Rect, side: keyof Rect): number {
|
||||
return typeof value == "number" ? value : value[side]
|
||||
}
|
||||
|
||||
function clientRect(node: HTMLElement): Rect {
|
||||
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: EditorView, rect: Rect, startDOM: Node) {
|
||||
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5
|
||||
let doc = view.dom.ownerDocument
|
||||
for (let parent: Node | null = startDOM || view.dom;; parent = parentNode(parent)) {
|
||||
if (!parent) break
|
||||
if (parent.nodeType != 1) continue
|
||||
let elt = parent as HTMLElement
|
||||
let atTop = elt == doc.body
|
||||
let bounding = atTop ? windowRect(doc) : clientRect(elt as HTMLElement)
|
||||
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 - rect.top > bounding.bottom - bounding.top
|
||||
? rect.top + getSide(scrollMargin, "top") - bounding.top
|
||||
: 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 = elt.scrollLeft, startY = elt.scrollTop
|
||||
if (moveY) elt.scrollTop += moveY
|
||||
if (moveX) elt.scrollLeft += moveX
|
||||
let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY
|
||||
rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY}
|
||||
}
|
||||
}
|
||||
if (atTop || /^(fixed|sticky)$/.test(getComputedStyle(parent as HTMLElement).position)) 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: EditorView): {
|
||||
refDOM: HTMLElement,
|
||||
refTop: number,
|
||||
stack: {dom: HTMLElement, top: number, left: number}[]
|
||||
} {
|
||||
let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top)
|
||||
let refDOM: HTMLElement, refTop: number
|
||||
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 || dom == view.dom || !view.dom.contains(dom)) continue
|
||||
let localRect = (dom as HTMLElement).getBoundingClientRect()
|
||||
if (localRect.top >= startY - 20) {
|
||||
refDOM = dom as HTMLElement
|
||||
refTop = localRect.top
|
||||
break
|
||||
}
|
||||
}
|
||||
return {refDOM: refDOM!, refTop: refTop!, stack: scrollStack(view.dom)}
|
||||
}
|
||||
|
||||
function scrollStack(dom: Node): {dom: HTMLElement, top: number, left: number}[] {
|
||||
let stack = [], doc = dom.ownerDocument
|
||||
for (let cur: Node | null = dom; cur; cur = parentNode(cur)) {
|
||||
stack.push({dom: cur as HTMLElement, top: (cur as HTMLElement).scrollTop, left: (cur as HTMLElement).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}: {
|
||||
refDOM: HTMLElement,
|
||||
refTop: number,
|
||||
stack: {dom: HTMLElement, top: number, left: number}[]
|
||||
}) {
|
||||
let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0
|
||||
restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop)
|
||||
}
|
||||
|
||||
function restoreScrollStack(stack: {dom: HTMLElement, top: number, left: number}[], dTop: number) {
|
||||
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: false | null | {preventScroll: boolean} = null
|
||||
// Feature-detects support for .focus({preventScroll: true}), and uses
|
||||
// a fallback kludge when not supported.
|
||||
export function focusPreventScroll(dom: HTMLElement) {
|
||||
if ((dom as any).setActive) return (dom as any).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: HTMLElement, coords: {top: number, left: number}): {node: Node, offset: number} {
|
||||
let closest, dxClosest = 2e8, coordsClosest: {left: number, top: number} | undefined, offset = 0
|
||||
let rowBot = coords.top, rowTop = coords.top
|
||||
let firstBelow: Node | undefined, coordsBelow: {left: number, top: number} | undefined
|
||||
for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) {
|
||||
let rects
|
||||
if (child.nodeType == 1) rects = (child as HTMLElement).getClientRects()
|
||||
else if (child.nodeType == 3) rects = textRange(child as Text).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
|
||||
}
|
||||
} else if (rect.top > coords.top && !firstBelow && rect.left <= coords.left && rect.right >= coords.left) {
|
||||
firstBelow = child
|
||||
coordsBelow = {left: Math.max(rect.left, Math.min(rect.right, coords.left)), top: rect.top}
|
||||
}
|
||||
if (!closest && (coords.left >= rect.right && coords.top >= rect.top ||
|
||||
coords.left >= rect.left && coords.top >= rect.bottom))
|
||||
offset = childIndex + 1
|
||||
}
|
||||
}
|
||||
if (!closest && firstBelow) { closest = firstBelow; coordsClosest = coordsBelow; dxClosest = 0 }
|
||||
if (closest && closest.nodeType == 3) return findOffsetInText(closest as Text, coordsClosest!)
|
||||
if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset}
|
||||
return findOffsetInNode(closest as HTMLElement, coordsClosest!)
|
||||
}
|
||||
|
||||
function findOffsetInText(node: Text, coords: {top: number, left: number}) {
|
||||
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: {top: number, left: number}, rect: 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: HTMLElement, coords: {top: number, left: number}) {
|
||||
let parent = dom.parentNode
|
||||
if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left)
|
||||
return parent as HTMLElement
|
||||
return dom
|
||||
}
|
||||
|
||||
function posFromElement(view: EditorView, elt: HTMLElement, coords: {top: number, left: number}) {
|
||||
let {node, offset} = findOffsetInNode(elt, coords), bias = -1
|
||||
if (node.nodeType == 1 && !node.firstChild) {
|
||||
let rect = (node as HTMLElement).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: EditorView, node: Node, offset: number, coords: {top: number, left: number}) {
|
||||
// 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 outsideBlock = -1
|
||||
for (let cur = node, sawBlock = false;;) {
|
||||
if (cur == view.dom) break
|
||||
let desc = view.docView.nearestDesc(cur, true)
|
||||
if (!desc) return null
|
||||
if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
|
||||
let rect = (desc.dom as HTMLElement).getBoundingClientRect()
|
||||
if (desc.node.isBlock && desc.parent && !sawBlock) {
|
||||
sawBlock = true
|
||||
if (rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
|
||||
else if (rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter
|
||||
}
|
||||
if (!desc.contentDOM && outsideBlock < 0 && !desc.node.isText) {
|
||||
// If we are inside a leaf, return the side of the leaf closer to the coords
|
||||
let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
|
||||
: coords.left < (rect.left + rect.right) / 2
|
||||
return before ? desc.posBefore : desc.posAfter
|
||||
}
|
||||
}
|
||||
cur = desc.dom.parentNode!
|
||||
}
|
||||
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1)
|
||||
}
|
||||
|
||||
function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement {
|
||||
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 as HTMLElement).getClientRects()
|
||||
for (let j = 0; j < rects.length; j++) {
|
||||
let rect = rects[j]
|
||||
if (inRect(coords, rect)) return elementFromPoint(child as HTMLElement, 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: EditorView, coords: {top: number, left: number}) {
|
||||
let doc = view.dom.ownerDocument, node: Node | undefined, offset = 0
|
||||
let caret = caretFromPoint(doc, coords.left, coords.top)
|
||||
if (caret) ({node, offset} = caret)
|
||||
|
||||
let elt = ((view.root as any).elementFromPoint ? view.root : doc)
|
||||
.elementFromPoint(coords.left, coords.top) as HTMLElement
|
||||
let 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: Node | null = elt; node && p; p = parentNode(p))
|
||||
if ((p as HTMLElement).draggable) node = undefined
|
||||
}
|
||||
elt = targetKludge(elt, coords)
|
||||
if (node) {
|
||||
if (browser.gecko && node.nodeType == 1) {
|
||||
// Firefox will sometimes return offsets into <input> 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 as HTMLElement).getBoundingClientRect()).right <= coords.left &&
|
||||
box.bottom > coords.top)
|
||||
offset++
|
||||
}
|
||||
}
|
||||
let prev
|
||||
// When clicking above the right side of an uneditable node, Chrome will report a cursor position after that node.
|
||||
if (browser.webkit && offset && node.nodeType == 1 && (prev = node.childNodes[offset - 1]).nodeType == 1 &&
|
||||
(prev as HTMLElement).contentEditable == "false" && (prev as HTMLElement).getBoundingClientRect().top >= 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 as HTMLElement).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 nonZero(rect: DOMRect) {
|
||||
return rect.top < rect.bottom || rect.left < rect.right
|
||||
}
|
||||
|
||||
function singleRect(target: HTMLElement | Range, bias: number): DOMRect {
|
||||
let rects = target.getClientRects()
|
||||
if (rects.length) {
|
||||
let first = rects[bias < 0 ? 0 : rects.length - 1]
|
||||
if (nonZero(first)) return first
|
||||
}
|
||||
return Array.prototype.find.call(rects, nonZero) || target.getBoundingClientRect()
|
||||
}
|
||||
|
||||
const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/
|
||||
|
||||
// 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: EditorView, pos: number, side: number): Rect {
|
||||
let {node, offset, atom} = 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 as Text, 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 as Text, offset - 1, offset - 1), -1)
|
||||
if (rectBefore.top == rect.top) {
|
||||
let rectAfter = singleRect(textRange(node as Text, 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 as Text, from, to), takeSide), takeSide < 0)
|
||||
}
|
||||
}
|
||||
|
||||
let $dom = view.state.doc.resolve(pos - (atom || 0))
|
||||
// Return a horizontal line in block context
|
||||
if (!$dom.parent.inlineContent) {
|
||||
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
|
||||
let before = node.childNodes[offset - 1]
|
||||
if (before.nodeType == 1) return flattenH((before as HTMLElement).getBoundingClientRect(), false)
|
||||
}
|
||||
if (atom == null && offset < nodeSize(node)) {
|
||||
let after = node.childNodes[offset]
|
||||
if (after.nodeType == 1) return flattenH((after as HTMLElement).getBoundingClientRect(), true)
|
||||
}
|
||||
return flattenH((node as HTMLElement).getBoundingClientRect(), side >= 0)
|
||||
}
|
||||
|
||||
// Inline, not in text node (this is not Bidi-safe)
|
||||
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
|
||||
let before = node.childNodes[offset - 1]
|
||||
let target = before.nodeType == 3 ? textRange(before as Text, 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 as Range | HTMLElement, 1), false)
|
||||
}
|
||||
if (atom == null && 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 as Text, 0, (supportEmptyRange ? 0 : 1))
|
||||
: after.nodeType == 1 ? after : null
|
||||
if (target) return flattenV(singleRect(target as Range | HTMLElement, -1), true)
|
||||
}
|
||||
// All else failed, just try to get a rectangle for the target node
|
||||
return flattenV(singleRect(node.nodeType == 3 ? textRange(node as Text) : node as HTMLElement, -side), side >= 0)
|
||||
}
|
||||
|
||||
function flattenV(rect: DOMRect, left: boolean) {
|
||||
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: DOMRect, top: boolean) {
|
||||
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<T>(view: EditorView, state: EditorState, f: () => T): T {
|
||||
let viewState = view.state, active = view.root.activeElement as HTMLElement
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Whether vertical position motion in a given direction
|
||||
// from a position would leave a text block.
|
||||
function endOfTextblockVertical(view: EditorView, state: EditorState, dir: "up" | "down") {
|
||||
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.contentDOM || 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 as HTMLElement).getClientRects()
|
||||
else if (child.nodeType == 3) boxes = textRange(child as Text, 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: EditorView, state: EditorState, dir: "left" | "right" | "forward" | "backward") {
|
||||
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.domSelection()
|
||||
// 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 as any).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 {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange()
|
||||
let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox
|
||||
;(sel as any).modify("move", dir, "character")
|
||||
let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom
|
||||
let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange()
|
||||
let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
|
||||
(oldNode == newNode && oldOff == newOff)
|
||||
// Restore the previous selection
|
||||
try {
|
||||
sel.collapse(anchorNode, anchorOffset)
|
||||
if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff)
|
||||
} catch (_) {}
|
||||
if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export type TextblockDir = "up" | "down" | "left" | "right" | "forward" | "backward"
|
||||
|
||||
let cachedState: EditorState | null = null
|
||||
let cachedDir: TextblockDir | null = null
|
||||
let cachedResult: boolean = false
|
||||
export function endOfTextblock(view: EditorView, state: EditorState, dir: TextblockDir) {
|
||||
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)
|
||||
}
|
||||
319
resources/app/node_modules/prosemirror-view/src/domobserver.ts
generated
vendored
Normal file
319
resources/app/node_modules/prosemirror-view/src/domobserver.ts
generated
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
import {Selection} from "prosemirror-state"
|
||||
import * as browser from "./browser"
|
||||
import {domIndex, isEquivalentPosition, selectionCollapsed, parentNode, DOMSelectionRange, DOMNode, DOMSelection} from "./dom"
|
||||
import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection"
|
||||
import {EditorView} from "./index"
|
||||
|
||||
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 {
|
||||
anchorNode: Node | null = null
|
||||
anchorOffset: number = 0
|
||||
focusNode: Node | null = null
|
||||
focusOffset: number = 0
|
||||
|
||||
set(sel: DOMSelectionRange) {
|
||||
this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
|
||||
this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.anchorNode = this.focusNode = null
|
||||
}
|
||||
|
||||
eq(sel: DOMSelectionRange) {
|
||||
return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
|
||||
sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
|
||||
}
|
||||
}
|
||||
|
||||
export class DOMObserver {
|
||||
queue: MutationRecord[] = []
|
||||
flushingSoon = -1
|
||||
observer: MutationObserver | null = null
|
||||
currentSelection = new SelectionState
|
||||
onCharData: ((e: Event) => void) | null = null
|
||||
suppressingSelectionUpdates = false
|
||||
|
||||
constructor(
|
||||
readonly view: EditorView,
|
||||
readonly handleDOMChange: (from: number, to: number, typeOver: boolean, added: Node[]) => void
|
||||
) {
|
||||
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()
|
||||
})
|
||||
if (useCharData) {
|
||||
this.onCharData = e => {
|
||||
this.queue.push({target: e.target as Node, type: "characterData", oldValue: (e as any).prevValue} as MutationRecord)
|
||||
this.flushSoon()
|
||||
}
|
||||
}
|
||||
this.onSelectionChange = this.onSelectionChange.bind(this)
|
||||
}
|
||||
|
||||
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.takeRecords()
|
||||
this.observer.observe(this.view.dom, observeOptions)
|
||||
}
|
||||
if (this.onCharData)
|
||||
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 (this.onCharData) 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.domSelectionRange()
|
||||
// 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.domSelectionRange())
|
||||
}
|
||||
|
||||
ignoreSelectionChange(sel: DOMSelectionRange) {
|
||||
if (!sel.focusNode) return true
|
||||
let ancestors: Set<Node> = new Set, container: DOMNode | undefined
|
||||
for (let scan: DOMNode | null = sel.focusNode; scan; scan = parentNode(scan)) ancestors.add(scan)
|
||||
for (let scan = sel.anchorNode; scan; scan = parentNode(scan)) if (ancestors.has(scan)) {
|
||||
container = scan
|
||||
break
|
||||
}
|
||||
let desc = container && this.view.docView.nearestDesc(container)
|
||||
if (desc && desc.ignoreMutation({
|
||||
type: "selection",
|
||||
target: container!.nodeType == 3 ? container!.parentNode : container
|
||||
} as any)) {
|
||||
this.setCurSelection()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
pendingRecords() {
|
||||
if (this.observer) for (let mut of this.observer.takeRecords()) this.queue.push(mut)
|
||||
return this.queue
|
||||
}
|
||||
|
||||
flush() {
|
||||
let {view} = this
|
||||
if (!view.docView || this.flushingSoon > -1) return
|
||||
let mutations = this.pendingRecords()
|
||||
if (mutations.length) this.queue = []
|
||||
|
||||
let sel = view.domSelectionRange()
|
||||
let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)
|
||||
|
||||
let from = -1, to = -1, typeOver = false, added: Node[] = []
|
||||
if (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 = brs[0] as HTMLElement, b = brs[1] as HTMLElement
|
||||
if (a.parentNode && a.parentNode.parentNode == b.parentNode) b.remove()
|
||||
else a.remove()
|
||||
}
|
||||
}
|
||||
|
||||
let readSel: Selection | null = null
|
||||
// If it looks like the browser has reset the selection to the
|
||||
// start of the document after focus, restore the selection from
|
||||
// the state
|
||||
if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
|
||||
Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 &&
|
||||
selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
|
||||
readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
|
||||
view.input.lastFocus = 0
|
||||
selectionToDOM(view)
|
||||
this.currentSelection.set(sel)
|
||||
view.scrollToSelection()
|
||||
} else if (from > -1 || newSel) {
|
||||
if (from > -1) {
|
||||
view.docView.markDirty(from, to)
|
||||
checkCSS(view)
|
||||
}
|
||||
this.handleDOMChange(from, to, typeOver, added)
|
||||
if (view.docView && view.docView.dirty) view.updateState(view.state)
|
||||
else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
|
||||
this.currentSelection.set(sel)
|
||||
}
|
||||
}
|
||||
|
||||
registerMutation(mut: MutationRecord, added: Node[]) {
|
||||
// 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 as HTMLElement).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: WeakMap<EditorView, null> = new WeakMap()
|
||||
let cssCheckWarned: boolean = false
|
||||
|
||||
function checkCSS(view: EditorView) {
|
||||
if (cssChecked.has(view)) return
|
||||
cssChecked.set(view, null)
|
||||
if (['normal', 'nowrap', 'pre-line'].indexOf(getComputedStyle(view.dom).whiteSpace) !== -1) {
|
||||
view.requiresGeckoHackNode = browser.gecko
|
||||
if (cssCheckWarned) return
|
||||
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.")
|
||||
cssCheckWarned = true
|
||||
}
|
||||
}
|
||||
|
||||
function rangeToSelectionRange(view: EditorView, range: StaticRange) {
|
||||
let anchorNode = range.startContainer, anchorOffset = range.startOffset
|
||||
let focusNode = range.endContainer, focusOffset = range.endOffset
|
||||
|
||||
let currentAnchor = view.domAtPos(view.state.selection.anchor)
|
||||
// Since such a range doesn't distinguish between anchor and head,
|
||||
// use a heuristic that flips it around if its end matches the
|
||||
// current anchor.
|
||||
if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset))
|
||||
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset]
|
||||
return {anchorNode, anchorOffset, focusNode, focusOffset}
|
||||
}
|
||||
|
||||
// Used to work around a Safari Selection/shadow DOM bug
|
||||
// Based on https://github.com/codemirror/dev/issues/414 fix
|
||||
export function safariShadowSelectionRange(view: EditorView, selection: DOMSelection): DOMSelectionRange | null {
|
||||
if ((selection as any).getComposedRanges) {
|
||||
let range = (selection as any).getComposedRanges(view.root)[0] as StaticRange
|
||||
if (range) return rangeToSelectionRange(view, range)
|
||||
}
|
||||
|
||||
let found: StaticRange | undefined
|
||||
function read(event: InputEvent) {
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
found = event.getTargetRanges()[0]
|
||||
}
|
||||
|
||||
// Because Safari (at least in 2018-2022) doesn't provide regular
|
||||
// access to the selection inside a shadowRoot, we have to perform a
|
||||
// ridiculous hack to get at it—using `execCommand` to trigger a
|
||||
// `beforeInput` event so that we can read the target range from the
|
||||
// event.
|
||||
view.dom.addEventListener("beforeinput", read, true)
|
||||
document.execCommand("indent")
|
||||
view.dom.removeEventListener("beforeinput", read, true)
|
||||
|
||||
return found ? rangeToSelectionRange(view, found) : null
|
||||
}
|
||||
805
resources/app/node_modules/prosemirror-view/src/index.ts
generated
vendored
Normal file
805
resources/app/node_modules/prosemirror-view/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,805 @@
|
||||
import {NodeSelection, EditorState, Plugin, PluginView, Transaction, Selection} from "prosemirror-state"
|
||||
import {Slice, ResolvedPos, DOMParser, DOMSerializer, Node, Mark} from "prosemirror-model"
|
||||
|
||||
import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos,
|
||||
resetScrollPos, focusPreventScroll} from "./domcoords"
|
||||
import {docViewDesc, ViewDesc, NodeView, NodeViewDesc} from "./viewdesc"
|
||||
import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition,
|
||||
InputState, doPaste, Dragging, findCompositionNode} from "./input"
|
||||
import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
|
||||
import {Decoration, viewDecorations, DecorationSource} from "./decoration"
|
||||
import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
|
||||
import {readDOMChange} from "./domchange"
|
||||
import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReusedRange} from "./dom"
|
||||
import * as browser from "./browser"
|
||||
|
||||
export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
|
||||
export {NodeView} from "./viewdesc"
|
||||
|
||||
// Exported for testing
|
||||
import {serializeForClipboard, parseFromClipboard} from "./clipboard"
|
||||
import {endComposition} from "./input"
|
||||
/// @internal
|
||||
export const __serializeForClipboard = serializeForClipboard
|
||||
/// @internal
|
||||
export const __parseFromClipboard = parseFromClipboard
|
||||
/// @internal
|
||||
export const __endComposition = endComposition
|
||||
|
||||
/// 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 {
|
||||
private _props: DirectEditorProps
|
||||
private directPlugins: readonly Plugin[]
|
||||
private _root: Document | ShadowRoot | null = null
|
||||
/// @internal
|
||||
focused = false
|
||||
/// Kludge used to work around a Chrome bug @internal
|
||||
trackWrites: DOMNode | null = null
|
||||
private mounted = false
|
||||
/// @internal
|
||||
markCursor: readonly Mark[] | null = null
|
||||
/// @internal
|
||||
cursorWrapper: {dom: DOMNode, deco: Decoration} | null = null
|
||||
/// @internal
|
||||
nodeViews: NodeViewSet
|
||||
/// @internal
|
||||
lastSelectedViewDesc: ViewDesc | undefined = undefined
|
||||
/// @internal
|
||||
docView: NodeViewDesc
|
||||
/// @internal
|
||||
input = new InputState
|
||||
private prevDirectPlugins: readonly Plugin[] = []
|
||||
private pluginViews: PluginView[] = []
|
||||
/// @internal
|
||||
domObserver!: DOMObserver
|
||||
/// Holds `true` when a hack node is needed in Firefox to prevent the
|
||||
/// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
|
||||
/// @internal
|
||||
requiresGeckoHackNode: boolean = false
|
||||
|
||||
/// The view's current [state](#state.EditorState).
|
||||
public state: EditorState
|
||||
|
||||
/// 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: null | DOMNode | ((editor: HTMLElement) => void) | {mount: HTMLElement}, props: DirectEditorProps) {
|
||||
this._props = props
|
||||
this.state = props.state
|
||||
this.directPlugins = props.plugins || []
|
||||
this.directPlugins.forEach(checkStateComponent)
|
||||
|
||||
this.dispatch = this.dispatch.bind(this)
|
||||
|
||||
this.dom = (place && (place as {mount: HTMLElement}).mount) || document.createElement("div")
|
||||
if (place) {
|
||||
if ((place as DOMNode).appendChild) (place as DOMNode).appendChild(this.dom)
|
||||
else if (typeof place == "function") place(this.dom)
|
||||
else if ((place as {mount: HTMLElement}).mount) this.mounted = true
|
||||
}
|
||||
|
||||
this.editable = getEditable(this)
|
||||
updateCursorWrapper(this)
|
||||
this.nodeViews = buildNodeViews(this)
|
||||
this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this)
|
||||
|
||||
this.domObserver = new DOMObserver(this, (from, to, typeOver, added) => readDOMChange(this, from, to, typeOver, added))
|
||||
this.domObserver.start()
|
||||
initInput(this)
|
||||
this.updatePluginViews()
|
||||
}
|
||||
|
||||
/// An editable DOM node containing the document. (You probably
|
||||
/// should not directly interfere with its content.)
|
||||
readonly dom: HTMLElement
|
||||
|
||||
/// Indicates whether the editor is currently [editable](#view.EditorProps.editable).
|
||||
editable: boolean
|
||||
|
||||
/// 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.
|
||||
dragging: null | {slice: Slice, move: boolean} = null
|
||||
|
||||
/// Holds `true` when a
|
||||
/// [composition](https://w3c.github.io/uievents/#events-compositionevents)
|
||||
/// is active.
|
||||
get composing() { return this.input.composing }
|
||||
|
||||
/// The view's current [props](#view.EditorProps).
|
||||
get props() {
|
||||
if (this._props.state != this.state) {
|
||||
let prev = this._props
|
||||
this._props = {} as any
|
||||
for (let name in prev) (this._props as any)[name] = (prev as any)[name]
|
||||
this._props.state = this.state
|
||||
}
|
||||
return this._props
|
||||
}
|
||||
|
||||
/// Update the view's props. Will immediately cause an update to
|
||||
/// the DOM.
|
||||
update(props: DirectEditorProps) {
|
||||
if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this)
|
||||
let prevProps = this._props
|
||||
this._props = props
|
||||
if (props.plugins) {
|
||||
props.plugins.forEach(checkStateComponent)
|
||||
this.directPlugins = props.plugins
|
||||
}
|
||||
this.updateStateInner(props.state, prevProps)
|
||||
}
|
||||
|
||||
/// 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: Partial<DirectEditorProps>) {
|
||||
let updated = {} as DirectEditorProps
|
||||
for (let name in this._props) (updated as any)[name] = (this._props as any)[name]
|
||||
updated.state = this.state
|
||||
for (let name in props) (updated as any)[name] = (props as any)[name]
|
||||
this.update(updated)
|
||||
}
|
||||
|
||||
/// Update the editor's `state` prop, without touching any of the
|
||||
/// other props.
|
||||
updateState(state: EditorState) {
|
||||
this.updateStateInner(state, this._props)
|
||||
}
|
||||
|
||||
private updateStateInner(state: EditorState, prevProps: DirectEditorProps) {
|
||||
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
|
||||
let pluginsChanged = prev.plugins != state.plugins || this._props.plugins != prevProps.plugins
|
||||
if (pluginsChanged || this._props.plugins != prevProps.plugins || this._props.nodeViews != prevProps.nodeViews) {
|
||||
let nodeViews = buildNodeViews(this)
|
||||
if (changedNodeViews(nodeViews, this.nodeViews)) {
|
||||
this.nodeViews = nodeViews
|
||||
redraw = true
|
||||
}
|
||||
}
|
||||
if (pluginsChanged || prevProps.handleDOMEvents != this._props.handleDOMEvents) {
|
||||
ensureListeners(this)
|
||||
}
|
||||
|
||||
this.editable = getEditable(this)
|
||||
updateCursorWrapper(this)
|
||||
let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this)
|
||||
|
||||
let scroll = prev.plugins != state.plugins && !prev.doc.eq(state.doc) ? "reset"
|
||||
: (state as any).scrollToSelection > (prev as any).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.domSelectionRange().focusNode) : null
|
||||
if (this.composing) this.input.compositionNode = findCompositionNode(this)
|
||||
if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
|
||||
this.docView.updateOuterDeco(outerDeco)
|
||||
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.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
|
||||
anchorInRightPlace(this))) {
|
||||
selectionToDOM(this, forceSelUpdate)
|
||||
} else {
|
||||
syncNodeSelection(this, state.selection)
|
||||
this.domObserver.setCurSelection()
|
||||
}
|
||||
this.domObserver.start()
|
||||
}
|
||||
|
||||
this.updatePluginViews(prev)
|
||||
if ((this.dragging as Dragging)?.node && !prev.doc.eq(state.doc))
|
||||
this.updateDraggedNode(this.dragging as Dragging, prev)
|
||||
|
||||
if (scroll == "reset") {
|
||||
this.dom.scrollTop = 0
|
||||
} else if (scroll == "to selection") {
|
||||
this.scrollToSelection()
|
||||
} else if (oldScrollPos) {
|
||||
resetScrollPos(oldScrollPos)
|
||||
}
|
||||
}
|
||||
|
||||
/// @internal
|
||||
scrollToSelection() {
|
||||
let startDOM = this.domSelectionRange().focusNode!
|
||||
if (this.someProp("handleScrollToSelection", f => f(this))) {
|
||||
// Handled
|
||||
} else if (this.state.selection instanceof NodeSelection) {
|
||||
let target = this.docView.domAfterPos(this.state.selection.from)
|
||||
if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
|
||||
} else {
|
||||
scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM)
|
||||
}
|
||||
}
|
||||
|
||||
private destroyPluginViews() {
|
||||
let view
|
||||
while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
|
||||
}
|
||||
|
||||
private updatePluginViews(prevState?: EditorState) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateDraggedNode(dragging: Dragging, prev: EditorState) {
|
||||
let sel = dragging.node!, found = -1
|
||||
if (this.state.doc.nodeAt(sel.from) == sel.node) {
|
||||
found = sel.from
|
||||
} else {
|
||||
let movedPos = sel.from + (this.state.doc.content.size - prev.doc.content.size)
|
||||
let moved = movedPos > 0 && this.state.doc.nodeAt(movedPos)
|
||||
if (moved == sel.node) found = movedPos
|
||||
}
|
||||
this.dragging = new Dragging(dragging.slice, dragging.move,
|
||||
found < 0 ? undefined : NodeSelection.create(this.state.doc, found))
|
||||
}
|
||||
|
||||
/// 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 extends keyof EditorProps, Result>(
|
||||
propName: PropName,
|
||||
f: (value: NonNullable<EditorProps[PropName]>) => Result
|
||||
): Result | undefined
|
||||
someProp<PropName extends keyof EditorProps>(propName: PropName): NonNullable<EditorProps[PropName]> | undefined
|
||||
someProp<PropName extends keyof EditorProps, Result>(
|
||||
propName: PropName,
|
||||
f?: (value: NonNullable<EditorProps[PropName]>) => Result
|
||||
): Result | undefined {
|
||||
let prop = this._props && this._props[propName], value
|
||||
if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
|
||||
for (let i = 0; i < this.directPlugins.length; i++) {
|
||||
let prop = this.directPlugins[i].props[propName]
|
||||
if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
|
||||
}
|
||||
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 as any) : prop)) return value as any
|
||||
}
|
||||
}
|
||||
|
||||
/// Query whether the view has focus.
|
||||
hasFocus() {
|
||||
// Work around IE not handling focus correctly if resize handles are shown.
|
||||
// If the cursor is inside an element with resize handles, activeElement
|
||||
// will be that element instead of this.dom.
|
||||
if (browser.ie) {
|
||||
// If activeElement is within this.dom, and there are no other elements
|
||||
// setting `contenteditable` to false in between, treat it as focused.
|
||||
let node = this.root.activeElement
|
||||
if (node == this.dom) return true
|
||||
if (!node || !this.dom.contains(node)) return false
|
||||
while (node && this.dom != node && this.dom.contains(node)) {
|
||||
if ((node as HTMLElement).contentEditable == 'false') return false
|
||||
node = node.parentElement
|
||||
}
|
||||
return true
|
||||
}
|
||||
return this.root.activeElement == this.dom
|
||||
}
|
||||
|
||||
/// Focus the editor.
|
||||
focus() {
|
||||
this.domObserver.stop()
|
||||
if (this.editable) focusPreventScroll(this.dom)
|
||||
selectionToDOM(this)
|
||||
this.domObserver.start()
|
||||
}
|
||||
|
||||
/// 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(): Document | ShadowRoot {
|
||||
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 as any).host)) {
|
||||
if (!(search as any).getSelection)
|
||||
Object.getPrototypeOf(search).getSelection = () => (search as DOMNode).ownerDocument!.getSelection()
|
||||
return this._root = search as Document | ShadowRoot
|
||||
}
|
||||
}
|
||||
return cached || document
|
||||
}
|
||||
|
||||
/// When an existing editor view is moved to a new document or
|
||||
/// shadow tree, call this to make it recompute its root.
|
||||
updateRoot() {
|
||||
this._root = null
|
||||
}
|
||||
|
||||
/// 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: {left: number, top: number}): {pos: number, inside: number} | null {
|
||||
return posAtCoords(this, coords)
|
||||
}
|
||||
|
||||
/// 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: number, side = 1): {left: number, right: number, top: number, bottom: number} {
|
||||
return coordsAtPos(this, pos, side)
|
||||
}
|
||||
|
||||
/// 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: number, side = 0): {node: DOMNode, offset: number} {
|
||||
return this.docView.domFromPos(pos, side)
|
||||
}
|
||||
|
||||
/// 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: number): DOMNode | null {
|
||||
let desc = this.docView.descAt(pos)
|
||||
return desc ? (desc as NodeViewDesc).nodeDOM : null
|
||||
}
|
||||
|
||||
/// 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: DOMNode, offset: number, bias = -1): number {
|
||||
let pos = this.docView.posFromDOM(node, offset, bias)
|
||||
if (pos == null) throw new RangeError("DOM position not inside the editor")
|
||||
return pos
|
||||
}
|
||||
|
||||
/// 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: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean {
|
||||
return endOfTextblock(this, state || this.state, dir)
|
||||
}
|
||||
|
||||
/// Run the editor's paste logic with the given HTML string. The
|
||||
/// `event`, if given, will be passed to the
|
||||
/// [`handlePaste`](#view.EditorProps.handlePaste) hook.
|
||||
pasteHTML(html: string, event?: ClipboardEvent) {
|
||||
return doPaste(this, "", html, false, event || new ClipboardEvent("paste"))
|
||||
}
|
||||
|
||||
/// Run the editor's paste logic with the given plain-text input.
|
||||
pasteText(text: string, event?: ClipboardEvent) {
|
||||
return doPaste(this, text, null, true, event || new ClipboardEvent("paste"))
|
||||
}
|
||||
|
||||
/// 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 as any).docView = null
|
||||
clearReusedRange();
|
||||
}
|
||||
|
||||
/// 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: Event) {
|
||||
return dispatchEvent(this, event)
|
||||
}
|
||||
|
||||
/// 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: Transaction) {
|
||||
let dispatchTransaction = this._props.dispatchTransaction
|
||||
if (dispatchTransaction) dispatchTransaction.call(this, tr)
|
||||
else this.updateState(this.state.apply(tr))
|
||||
}
|
||||
|
||||
/// @internal
|
||||
domSelectionRange(): DOMSelectionRange {
|
||||
let sel = this.domSelection()
|
||||
return browser.safari && this.root.nodeType === 11 &&
|
||||
deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel
|
||||
}
|
||||
|
||||
/// @internal
|
||||
domSelection(): DOMSelection {
|
||||
return (this.root as Document).getSelection()!
|
||||
}
|
||||
}
|
||||
|
||||
function computeDocDeco(view: EditorView) {
|
||||
let attrs = Object.create(null)
|
||||
attrs.class = "ProseMirror"
|
||||
attrs.contenteditable = String(view.editable)
|
||||
|
||||
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]
|
||||
else if (attr == "style")
|
||||
attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]
|
||||
else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName")
|
||||
attrs[attr] = String(value[attr])
|
||||
}
|
||||
})
|
||||
if (!attrs.translate) attrs.translate = "no"
|
||||
|
||||
return [Decoration.node(0, view.state.doc.content.size, attrs)]
|
||||
}
|
||||
|
||||
function updateCursorWrapper(view: EditorView) {
|
||||
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} as any)}
|
||||
} else {
|
||||
view.cursorWrapper = null
|
||||
}
|
||||
}
|
||||
|
||||
function getEditable(view: EditorView) {
|
||||
return !view.someProp("editable", value => value(view.state) === false)
|
||||
}
|
||||
|
||||
function selectionContextChanged(sel1: Selection, sel2: Selection) {
|
||||
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: EditorView) {
|
||||
let result: NodeViewSet = Object.create(null)
|
||||
function add(obj: NodeViewSet) {
|
||||
for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop))
|
||||
result[prop] = obj[prop]
|
||||
}
|
||||
view.someProp("nodeViews", add)
|
||||
view.someProp("markViews", add)
|
||||
return result
|
||||
}
|
||||
|
||||
function changedNodeViews(a: NodeViewSet, b: NodeViewSet) {
|
||||
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: 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")
|
||||
}
|
||||
|
||||
/// The type of function [provided](#view.EditorProps.nodeViews) to
|
||||
/// create [node views](#view.NodeView).
|
||||
export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined,
|
||||
decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView
|
||||
|
||||
/// The function types [used](#view.EditorProps.markViews) to create
|
||||
/// mark views.
|
||||
export type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => {dom: HTMLElement, contentDOM?: HTMLElement}
|
||||
|
||||
type NodeViewSet = {[name: string]: NodeViewConstructor | MarkViewConstructor}
|
||||
|
||||
/// Helper type that maps event names to event object types, but
|
||||
/// includes events that TypeScript's HTMLElementEventMap doesn't know
|
||||
/// about.
|
||||
export interface DOMEventMap extends HTMLElementEventMap {
|
||||
[event: string]: any
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The optional type parameter refers to the type of `this` in prop
|
||||
/// functions, and is used to pass in the plugin type when defining a
|
||||
/// [plugin](#state.Plugin).
|
||||
export interface EditorProps<P = any> {
|
||||
/// 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).
|
||||
handleDOMEvents?: {
|
||||
[event in keyof DOMEventMap]?: (this: P, view: EditorView, event: DOMEventMap[event]) => boolean | void
|
||||
}
|
||||
|
||||
/// Called when the editor receives a `keydown` event.
|
||||
handleKeyDown?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
|
||||
|
||||
/// Handler for `keypress` events.
|
||||
handleKeyPress?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
|
||||
|
||||
/// 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.
|
||||
handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void
|
||||
|
||||
/// Called for each node around a click, from the inside out. The
|
||||
/// `direct` flag will be true for the inner node.
|
||||
handleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
|
||||
|
||||
/// Called when the editor is clicked, after `handleClickOn` handlers
|
||||
/// have been called.
|
||||
handleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
|
||||
|
||||
/// Called for each node around a double click.
|
||||
handleDoubleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
|
||||
|
||||
/// Called when the editor is double-clicked, after `handleDoubleClickOn`.
|
||||
handleDoubleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
|
||||
|
||||
/// Called for each node around a triple click.
|
||||
handleTripleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
|
||||
|
||||
/// Called when the editor is triple-clicked, after `handleTripleClickOn`.
|
||||
handleTripleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
|
||||
|
||||
/// 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.
|
||||
handlePaste?: (this: P, view: EditorView, event: ClipboardEvent, slice: Slice) => boolean | void
|
||||
|
||||
/// 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).
|
||||
handleDrop?: (this: P, view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => boolean | void
|
||||
|
||||
/// 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.
|
||||
handleScrollToSelection?: (this: P, view: EditorView) => boolean
|
||||
|
||||
/// Can be used to override the way a selection is created when
|
||||
/// reading a DOM selection between the given anchor and head.
|
||||
createSelectionBetween?: (this: P, view: EditorView, anchor: ResolvedPos, head: ResolvedPos) => Selection | null
|
||||
|
||||
/// 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.
|
||||
domParser?: DOMParser
|
||||
|
||||
/// Can be used to transform pasted HTML text, _before_ it is parsed,
|
||||
/// for example to clean it up.
|
||||
transformPastedHTML?: (this: P, html: string, view: EditorView) => string
|
||||
|
||||
/// 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.
|
||||
clipboardParser?: DOMParser
|
||||
|
||||
/// Transform pasted plain text. The `plain` flag will be true when
|
||||
/// the text is pasted as plain text.
|
||||
transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string
|
||||
|
||||
/// 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 `<p>` tags, and call
|
||||
/// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
|
||||
/// The `plain` flag will be true when the text is pasted as plain text.
|
||||
clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
|
||||
|
||||
/// Can be used to transform pasted or dragged-and-dropped content
|
||||
/// before it is applied to the document.
|
||||
transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice
|
||||
|
||||
/// Can be used to transform copied or cut content before it is
|
||||
/// serialized to the clipboard.
|
||||
transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice
|
||||
|
||||
/// Allows you to pass custom rendering and behavior logic for
|
||||
/// nodes. Should map node names to constructor functions that
|
||||
/// produce a [`NodeView`](#view.NodeView) object implementing the
|
||||
/// node's display behavior. 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.
|
||||
/// Note that if the node is not in the document, the position
|
||||
/// returned by this function will be `undefined`.
|
||||
///
|
||||
/// `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.
|
||||
///
|
||||
/// (For backwards compatibility reasons, [mark
|
||||
/// views](#view.EditorProps.markViews) can also be included in this
|
||||
/// object.)
|
||||
nodeViews?: {[node: string]: NodeViewConstructor}
|
||||
|
||||
/// Pass custom mark rendering functions. Note that these cannot
|
||||
/// provide the kind of dynamic behavior that [node
|
||||
/// views](#view.NodeView) can—they just provide custom rendering
|
||||
/// logic. The third argument indicates whether the mark's content
|
||||
/// is inline.
|
||||
markViews?: {[mark: string]: MarkViewConstructor}
|
||||
|
||||
/// 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.
|
||||
clipboardSerializer?: DOMSerializer
|
||||
|
||||
/// 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.
|
||||
clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string
|
||||
|
||||
/// A set of [document decorations](#view.Decoration) to show in the
|
||||
/// view.
|
||||
decorations?: (this: P, state: EditorState) => DecorationSource | null | undefined
|
||||
|
||||
/// When this returns false, the content of the view is not directly
|
||||
/// editable.
|
||||
editable?: (this: P, state: EditorState) => boolean
|
||||
|
||||
/// Control the DOM attributes of the editable element. May be either
|
||||
/// an object or a function going from an editor state to an object.
|
||||
/// By default, the element will get a class `"ProseMirror"`, and
|
||||
/// will have its `contentEditable` attribute determined by the
|
||||
/// [`editable` prop](#view.EditorProps.editable). Additional classes
|
||||
/// provided here will be added to the class. For other attributes,
|
||||
/// the value provided first (as in
|
||||
/// [`someProp`](#view.EditorView.someProp)) will be used.
|
||||
attributes?: {[name: string]: string} | ((state: EditorState) => {[name: string]: string})
|
||||
|
||||
/// Determines the distance (in pixels) between the cursor and the
|
||||
/// end of the visible viewport at which point, when scrolling the
|
||||
/// cursor into view, scrolling takes place. Defaults to 0.
|
||||
scrollThreshold?: number | {top: number, right: number, bottom: number, left: number}
|
||||
|
||||
/// Determines the extra space (in pixels) that is left above or
|
||||
/// below the cursor when it is scrolled into view. Defaults to 5.
|
||||
scrollMargin?: number | {top: number, right: number, bottom: number, left: number}
|
||||
}
|
||||
|
||||
/// The props object given directly to the editor view supports some
|
||||
/// fields that can't be used in plugins:
|
||||
export interface DirectEditorProps extends EditorProps {
|
||||
/// The current state of the editor.
|
||||
state: EditorState
|
||||
|
||||
/// A set of plugins to use in the view, applying their [plugin
|
||||
/// view](#state.PluginSpec.view) and
|
||||
/// [props](#state.PluginSpec.props). Passing plugins with a state
|
||||
/// component (a [state field](#state.PluginSpec.state) field or a
|
||||
/// [transaction](#state.PluginSpec.filterTransaction) filter or
|
||||
/// appender) will result in an error, since such plugins must be
|
||||
/// present in the state to work.
|
||||
plugins?: readonly Plugin[]
|
||||
|
||||
/// The callback over which to send transactions (state updates)
|
||||
/// produced by the view. If you specify this, you probably want to
|
||||
/// make sure this ends up calling the view's
|
||||
/// [`updateState`](#view.EditorView.updateState) method with a new
|
||||
/// state that has the transaction
|
||||
/// [applied](#state.EditorState.apply). The callback will be bound to have
|
||||
/// the view instance as its `this` binding.
|
||||
dispatchTransaction?: (tr: Transaction) => void
|
||||
}
|
||||
803
resources/app/node_modules/prosemirror-view/src/input.ts
generated
vendored
Normal file
803
resources/app/node_modules/prosemirror-view/src/input.ts
generated
vendored
Normal file
@@ -0,0 +1,803 @@
|
||||
import {Selection, NodeSelection, TextSelection} from "prosemirror-state"
|
||||
import {dropPoint} from "prosemirror-transform"
|
||||
import {Slice, Node} from "prosemirror-model"
|
||||
|
||||
import * as browser from "./browser"
|
||||
import {captureKeyDown} from "./capturekeys"
|
||||
import {parseFromClipboard, serializeForClipboard} from "./clipboard"
|
||||
import {selectionBetween, selectionToDOM, selectionFromDOM} from "./selection"
|
||||
import {keyEvent, DOMNode, textNodeBefore, textNodeAfter} from "./dom"
|
||||
import {EditorView} from "./index"
|
||||
import {ViewDesc} from "./viewdesc"
|
||||
|
||||
// A collection of DOM events that occur within the editor, and callback functions
|
||||
// to invoke when the event fires.
|
||||
const handlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
|
||||
const editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
|
||||
const passiveHandlers: Record<string, boolean> = {touchstart: true, touchmove: true}
|
||||
|
||||
export class InputState {
|
||||
shiftKey = false
|
||||
mouseDown: MouseDown | null = null
|
||||
lastKeyCode: number | null = null
|
||||
lastKeyCodeTime = 0
|
||||
lastClick = {time: 0, x: 0, y: 0, type: ""}
|
||||
lastSelectionOrigin: string | null = null
|
||||
lastSelectionTime = 0
|
||||
lastIOSEnter = 0
|
||||
lastIOSEnterFallbackTimeout = -1
|
||||
lastFocus = 0
|
||||
lastTouch = 0
|
||||
lastAndroidDelete = 0
|
||||
composing = false
|
||||
compositionNode: Text | null = null
|
||||
composingTimeout = -1
|
||||
compositionNodes: ViewDesc[] = []
|
||||
compositionEndedAt = -2e8
|
||||
compositionID = 1
|
||||
// Set to a composition ID when there are pending changes at compositionend
|
||||
compositionPendingChanges = 0
|
||||
domChangeCount = 0
|
||||
eventHandlers: {[event: string]: (event: Event) => void} = Object.create(null)
|
||||
hideSelectionGuard: (() => void) | null = null
|
||||
}
|
||||
|
||||
export function initInput(view: EditorView) {
|
||||
for (let event in handlers) {
|
||||
let handler = handlers[event]
|
||||
view.dom.addEventListener(event, view.input.eventHandlers[event] = (event: Event) => {
|
||||
if (eventBelongsToView(view, event) && !runCustomHandler(view, event) &&
|
||||
(view.editable || !(event.type in editHandlers)))
|
||||
handler(view, event)
|
||||
}, passiveHandlers[event] ? {passive: true} : undefined)
|
||||
}
|
||||
// On Safari, for reasons beyond my understanding, adding an input
|
||||
// event handler makes an issue where the composition vanishes when
|
||||
// you press enter go away.
|
||||
if (browser.safari) view.dom.addEventListener("input", () => null)
|
||||
|
||||
ensureListeners(view)
|
||||
}
|
||||
|
||||
function setSelectionOrigin(view: EditorView, origin: string) {
|
||||
view.input.lastSelectionOrigin = origin
|
||||
view.input.lastSelectionTime = Date.now()
|
||||
}
|
||||
|
||||
export function destroyInput(view: EditorView) {
|
||||
view.domObserver.stop()
|
||||
for (let type in view.input.eventHandlers)
|
||||
view.dom.removeEventListener(type, view.input.eventHandlers[type])
|
||||
clearTimeout(view.input.composingTimeout)
|
||||
clearTimeout(view.input.lastIOSEnterFallbackTimeout)
|
||||
}
|
||||
|
||||
export function ensureListeners(view: EditorView) {
|
||||
view.someProp("handleDOMEvents", currentHandlers => {
|
||||
for (let type in currentHandlers) if (!view.input.eventHandlers[type])
|
||||
view.dom.addEventListener(type, view.input.eventHandlers[type] = event => runCustomHandler(view, event))
|
||||
})
|
||||
}
|
||||
|
||||
function runCustomHandler(view: EditorView, event: Event) {
|
||||
return view.someProp("handleDOMEvents", handlers => {
|
||||
let handler = handlers[event.type]
|
||||
return handler ? handler(view, event) || event.defaultPrevented : false
|
||||
})
|
||||
}
|
||||
|
||||
function eventBelongsToView(view: EditorView, event: Event) {
|
||||
if (!event.bubbles) return true
|
||||
if (event.defaultPrevented) return false
|
||||
for (let node = event.target as DOMNode; node != view.dom; node = node.parentNode!)
|
||||
if (!node || node.nodeType == 11 ||
|
||||
(node.pmViewDesc && node.pmViewDesc.stopEvent(event)))
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function dispatchEvent(view: EditorView, event: Event) {
|
||||
if (!runCustomHandler(view, event) && handlers[event.type] &&
|
||||
(view.editable || !(event.type in editHandlers)))
|
||||
handlers[event.type](view, event)
|
||||
}
|
||||
|
||||
editHandlers.keydown = (view: EditorView, _event: Event) => {
|
||||
let event = _event as KeyboardEvent
|
||||
view.input.shiftKey = event.keyCode == 16 || event.shiftKey
|
||||
if (inOrNearComposition(view, event)) return
|
||||
view.input.lastKeyCode = event.keyCode
|
||||
view.input.lastKeyCodeTime = Date.now()
|
||||
// Suppress enter key events on Chrome Android, because those tend
|
||||
// to be part of a confused sequence of composition events fired,
|
||||
// and handling them eagerly tends to corrupt the input.
|
||||
if (browser.android && browser.chrome && event.keyCode == 13) return
|
||||
if (event.keyCode != 229) view.domObserver.forceFlush()
|
||||
|
||||
// On iOS, if we preventDefault enter key presses, the virtual
|
||||
// keyboard gets confused. So the hack here is to set a flag that
|
||||
// makes the DOM change code recognize that what just happens should
|
||||
// be replaced by whatever the Enter key handlers do.
|
||||
if (browser.ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
let now = Date.now()
|
||||
view.input.lastIOSEnter = now
|
||||
view.input.lastIOSEnterFallbackTimeout = setTimeout(() => {
|
||||
if (view.input.lastIOSEnter == now) {
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))
|
||||
view.input.lastIOSEnter = 0
|
||||
}
|
||||
}, 200)
|
||||
} else if (view.someProp("handleKeyDown", f => f(view, event)) || captureKeyDown(view, event)) {
|
||||
event.preventDefault()
|
||||
} else {
|
||||
setSelectionOrigin(view, "key")
|
||||
}
|
||||
}
|
||||
|
||||
editHandlers.keyup = (view, event) => {
|
||||
if ((event as KeyboardEvent).keyCode == 16) view.input.shiftKey = false
|
||||
}
|
||||
|
||||
editHandlers.keypress = (view, _event) => {
|
||||
let event = _event as KeyboardEvent
|
||||
if (inOrNearComposition(view, event) || !event.charCode ||
|
||||
event.ctrlKey && !event.altKey || browser.mac && event.metaKey) return
|
||||
|
||||
if (view.someProp("handleKeyPress", f => f(view, event))) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
let sel = view.state.selection
|
||||
if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
|
||||
let text = String.fromCharCode(event.charCode)
|
||||
if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
|
||||
view.dispatch(view.state.tr.insertText(text).scrollIntoView())
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function eventCoords(event: MouseEvent) { return {left: event.clientX, top: event.clientY} }
|
||||
|
||||
function isNear(event: MouseEvent, click: {x: number, y: number}) {
|
||||
let dx = click.x - event.clientX, dy = click.y - event.clientY
|
||||
return dx * dx + dy * dy < 100
|
||||
}
|
||||
|
||||
function runHandlerOnContext(
|
||||
view: EditorView,
|
||||
propName: "handleClickOn" | "handleDoubleClickOn" | "handleTripleClickOn",
|
||||
pos: number,
|
||||
inside: number,
|
||||
event: MouseEvent
|
||||
) {
|
||||
if (inside == -1) return false
|
||||
let $pos = view.state.doc.resolve(inside)
|
||||
for (let i = $pos.depth + 1; i > 0; i--) {
|
||||
if (view.someProp(propName, f => i > $pos.depth ? f(view, pos, $pos.nodeAfter!, $pos.before(i), event, true)
|
||||
: f(view, pos, $pos.node(i), $pos.before(i), event, false)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function updateSelection(view: EditorView, selection: Selection, origin: string) {
|
||||
if (!view.focused) view.focus()
|
||||
let tr = view.state.tr.setSelection(selection)
|
||||
if (origin == "pointer") tr.setMeta("pointer", true)
|
||||
view.dispatch(tr)
|
||||
}
|
||||
|
||||
function selectClickedLeaf(view: EditorView, inside: number) {
|
||||
if (inside == -1) return false
|
||||
let $pos = view.state.doc.resolve(inside), node = $pos.nodeAfter
|
||||
if (node && node.isAtom && NodeSelection.isSelectable(node)) {
|
||||
updateSelection(view, new NodeSelection($pos), "pointer")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function selectClickedNode(view: EditorView, inside: number) {
|
||||
if (inside == -1) return false
|
||||
let sel = view.state.selection, selectedNode, selectAt
|
||||
if (sel instanceof NodeSelection) selectedNode = sel.node
|
||||
|
||||
let $pos = view.state.doc.resolve(inside)
|
||||
for (let i = $pos.depth + 1; i > 0; i--) {
|
||||
let node = i > $pos.depth ? $pos.nodeAfter! : $pos.node(i)
|
||||
if (NodeSelection.isSelectable(node)) {
|
||||
if (selectedNode && sel.$from.depth > 0 &&
|
||||
i >= sel.$from.depth && $pos.before(sel.$from.depth + 1) == sel.$from.pos)
|
||||
selectAt = $pos.before(sel.$from.depth)
|
||||
else
|
||||
selectAt = $pos.before(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (selectAt != null) {
|
||||
updateSelection(view, NodeSelection.create(view.state.doc, selectAt), "pointer")
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSingleClick(view: EditorView, pos: number, inside: number, event: MouseEvent, selectNode: boolean) {
|
||||
return runHandlerOnContext(view, "handleClickOn", pos, inside, event) ||
|
||||
view.someProp("handleClick", f => f(view, pos, event)) ||
|
||||
(selectNode ? selectClickedNode(view, inside) : selectClickedLeaf(view, inside))
|
||||
}
|
||||
|
||||
function handleDoubleClick(view: EditorView, pos: number, inside: number, event: MouseEvent) {
|
||||
return runHandlerOnContext(view, "handleDoubleClickOn", pos, inside, event) ||
|
||||
view.someProp("handleDoubleClick", f => f(view, pos, event))
|
||||
}
|
||||
|
||||
function handleTripleClick(view: EditorView, pos: number, inside: number, event: MouseEvent) {
|
||||
return runHandlerOnContext(view, "handleTripleClickOn", pos, inside, event) ||
|
||||
view.someProp("handleTripleClick", f => f(view, pos, event)) ||
|
||||
defaultTripleClick(view, inside, event)
|
||||
}
|
||||
|
||||
function defaultTripleClick(view: EditorView, inside: number, event: MouseEvent) {
|
||||
if (event.button != 0) return false
|
||||
let doc = view.state.doc
|
||||
if (inside == -1) {
|
||||
if (doc.inlineContent) {
|
||||
updateSelection(view, TextSelection.create(doc, 0, doc.content.size), "pointer")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let $pos = doc.resolve(inside)
|
||||
for (let i = $pos.depth + 1; i > 0; i--) {
|
||||
let node = i > $pos.depth ? $pos.nodeAfter! : $pos.node(i)
|
||||
let nodePos = $pos.before(i)
|
||||
if (node.inlineContent)
|
||||
updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size), "pointer")
|
||||
else if (NodeSelection.isSelectable(node))
|
||||
updateSelection(view, NodeSelection.create(doc, nodePos), "pointer")
|
||||
else
|
||||
continue
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function forceDOMFlush(view: EditorView) {
|
||||
return endComposition(view)
|
||||
}
|
||||
|
||||
const selectNodeModifier: keyof MouseEvent = browser.mac ? "metaKey" : "ctrlKey"
|
||||
|
||||
handlers.mousedown = (view, _event) => {
|
||||
let event = _event as MouseEvent
|
||||
view.input.shiftKey = event.shiftKey
|
||||
let flushed = forceDOMFlush(view)
|
||||
let now = Date.now(), type = "singleClick"
|
||||
if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier]) {
|
||||
if (view.input.lastClick.type == "singleClick") type = "doubleClick"
|
||||
else if (view.input.lastClick.type == "doubleClick") type = "tripleClick"
|
||||
}
|
||||
view.input.lastClick = {time: now, x: event.clientX, y: event.clientY, type}
|
||||
|
||||
let pos = view.posAtCoords(eventCoords(event))
|
||||
if (!pos) return
|
||||
|
||||
if (type == "singleClick") {
|
||||
if (view.input.mouseDown) view.input.mouseDown.done()
|
||||
view.input.mouseDown = new MouseDown(view, pos, event, !!flushed)
|
||||
} else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) {
|
||||
event.preventDefault()
|
||||
} else {
|
||||
setSelectionOrigin(view, "pointer")
|
||||
}
|
||||
}
|
||||
|
||||
class MouseDown {
|
||||
startDoc: Node
|
||||
selectNode: boolean
|
||||
allowDefault: boolean
|
||||
delayedSelectionSync = false
|
||||
mightDrag: {node: Node, pos: number, addAttr: boolean, setUneditable: boolean} | null = null
|
||||
target: HTMLElement | null
|
||||
|
||||
constructor(
|
||||
readonly view: EditorView,
|
||||
readonly pos: {pos: number, inside: number},
|
||||
readonly event: MouseEvent,
|
||||
readonly flushed: boolean
|
||||
) {
|
||||
this.startDoc = view.state.doc
|
||||
this.selectNode = !!event[selectNodeModifier]
|
||||
this.allowDefault = event.shiftKey
|
||||
|
||||
let targetNode: Node, targetPos
|
||||
if (pos.inside > -1) {
|
||||
targetNode = view.state.doc.nodeAt(pos.inside)!
|
||||
targetPos = pos.inside
|
||||
} else {
|
||||
let $pos = view.state.doc.resolve(pos.pos)
|
||||
targetNode = $pos.parent
|
||||
targetPos = $pos.depth ? $pos.before() : 0
|
||||
}
|
||||
|
||||
const target = flushed ? null : event.target as HTMLElement
|
||||
const targetDesc = target ? view.docView.nearestDesc(target, true) : null
|
||||
this.target = targetDesc ? targetDesc.dom as HTMLElement : null
|
||||
|
||||
let {selection} = view.state
|
||||
if (event.button == 0 &&
|
||||
targetNode.type.spec.draggable && targetNode.type.spec.selectable !== false ||
|
||||
selection instanceof NodeSelection && selection.from <= targetPos && selection.to > targetPos)
|
||||
this.mightDrag = {
|
||||
node: targetNode,
|
||||
pos: targetPos,
|
||||
addAttr: !!(this.target && !this.target.draggable),
|
||||
setUneditable: !!(this.target && browser.gecko && !this.target.hasAttribute("contentEditable"))
|
||||
}
|
||||
|
||||
if (this.target && this.mightDrag && (this.mightDrag.addAttr || this.mightDrag.setUneditable)) {
|
||||
this.view.domObserver.stop()
|
||||
if (this.mightDrag.addAttr) this.target.draggable = true
|
||||
if (this.mightDrag.setUneditable)
|
||||
setTimeout(() => {
|
||||
if (this.view.input.mouseDown == this) this.target!.setAttribute("contentEditable", "false")
|
||||
}, 20)
|
||||
this.view.domObserver.start()
|
||||
}
|
||||
|
||||
view.root.addEventListener("mouseup", this.up = this.up.bind(this) as any)
|
||||
view.root.addEventListener("mousemove", this.move = this.move.bind(this) as any)
|
||||
setSelectionOrigin(view, "pointer")
|
||||
}
|
||||
|
||||
done() {
|
||||
this.view.root.removeEventListener("mouseup", this.up as any)
|
||||
this.view.root.removeEventListener("mousemove", this.move as any)
|
||||
if (this.mightDrag && this.target) {
|
||||
this.view.domObserver.stop()
|
||||
if (this.mightDrag.addAttr) this.target.removeAttribute("draggable")
|
||||
if (this.mightDrag.setUneditable) this.target.removeAttribute("contentEditable")
|
||||
this.view.domObserver.start()
|
||||
}
|
||||
if (this.delayedSelectionSync) setTimeout(() => selectionToDOM(this.view))
|
||||
this.view.input.mouseDown = null
|
||||
}
|
||||
|
||||
up(event: MouseEvent) {
|
||||
this.done()
|
||||
|
||||
if (!this.view.dom.contains(event.target as HTMLElement))
|
||||
return
|
||||
|
||||
let pos: {pos: number, inside: number} | null = this.pos
|
||||
if (this.view.state.doc != this.startDoc) pos = this.view.posAtCoords(eventCoords(event))
|
||||
|
||||
this.updateAllowDefault(event)
|
||||
if (this.allowDefault || !pos) {
|
||||
setSelectionOrigin(this.view, "pointer")
|
||||
} else if (handleSingleClick(this.view, pos.pos, pos.inside, event, this.selectNode)) {
|
||||
event.preventDefault()
|
||||
} else if (event.button == 0 &&
|
||||
(this.flushed ||
|
||||
// Safari ignores clicks on draggable elements
|
||||
(browser.safari && this.mightDrag && !this.mightDrag.node.isAtom) ||
|
||||
// Chrome will sometimes treat a node selection as a
|
||||
// cursor, but still report that the node is selected
|
||||
// when asked through getSelection. You'll then get a
|
||||
// situation where clicking at the point where that
|
||||
// (hidden) cursor is doesn't change the selection, and
|
||||
// thus doesn't get a reaction from ProseMirror. This
|
||||
// works around that.
|
||||
(browser.chrome && !this.view.state.selection.visible &&
|
||||
Math.min(Math.abs(pos.pos - this.view.state.selection.from),
|
||||
Math.abs(pos.pos - this.view.state.selection.to)) <= 2))) {
|
||||
updateSelection(this.view, Selection.near(this.view.state.doc.resolve(pos.pos)), "pointer")
|
||||
event.preventDefault()
|
||||
} else {
|
||||
setSelectionOrigin(this.view, "pointer")
|
||||
}
|
||||
}
|
||||
|
||||
move(event: MouseEvent) {
|
||||
this.updateAllowDefault(event)
|
||||
setSelectionOrigin(this.view, "pointer")
|
||||
if (event.buttons == 0) this.done()
|
||||
}
|
||||
|
||||
updateAllowDefault(event: MouseEvent) {
|
||||
if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 ||
|
||||
Math.abs(this.event.y - event.clientY) > 4))
|
||||
this.allowDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
handlers.touchstart = view => {
|
||||
view.input.lastTouch = Date.now()
|
||||
forceDOMFlush(view)
|
||||
setSelectionOrigin(view, "pointer")
|
||||
}
|
||||
|
||||
handlers.touchmove = view => {
|
||||
view.input.lastTouch = Date.now()
|
||||
setSelectionOrigin(view, "pointer")
|
||||
}
|
||||
|
||||
handlers.contextmenu = view => forceDOMFlush(view)
|
||||
|
||||
function inOrNearComposition(view: EditorView, event: Event) {
|
||||
if (view.composing) return true
|
||||
// See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/.
|
||||
// On Japanese input method editors (IMEs), the Enter key is used to confirm character
|
||||
// selection. On Safari, when Enter is pressed, compositionend and keydown events are
|
||||
// emitted. The keydown event triggers newline insertion, which we don't want.
|
||||
// This method returns true if the keydown event should be ignored.
|
||||
// We only ignore it once, as pressing Enter a second time *should* insert a newline.
|
||||
// Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp.
|
||||
// This guards against the case where compositionend is triggered without the keyboard
|
||||
// (e.g. character confirmation may be done with the mouse), and keydown is triggered
|
||||
// afterwards- we wouldn't want to ignore the keydown event in this case.
|
||||
if (browser.safari && Math.abs(event.timeStamp - view.input.compositionEndedAt) < 500) {
|
||||
view.input.compositionEndedAt = -2e8
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Drop active composition after 5 seconds of inactivity on Android
|
||||
const timeoutComposition = browser.android ? 5000 : -1
|
||||
|
||||
editHandlers.compositionstart = editHandlers.compositionupdate = view => {
|
||||
if (!view.composing) {
|
||||
view.domObserver.flush()
|
||||
let {state} = view, $pos = state.selection.$from
|
||||
if (state.selection.empty &&
|
||||
(state.storedMarks ||
|
||||
(!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore!.marks.some(m => m.type.spec.inclusive === false)))) {
|
||||
// Need to wrap the cursor in mark nodes different from the ones in the DOM context
|
||||
view.markCursor = view.state.storedMarks || $pos.marks()
|
||||
endComposition(view, true)
|
||||
view.markCursor = null
|
||||
} else {
|
||||
endComposition(view)
|
||||
// In firefox, if the cursor is after but outside a marked node,
|
||||
// the inserted text won't inherit the marks. So this moves it
|
||||
// inside if necessary.
|
||||
if (browser.gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore!.marks.length) {
|
||||
let sel = view.domSelectionRange()
|
||||
for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
|
||||
let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1]
|
||||
if (!before) break
|
||||
if (before.nodeType == 3) {
|
||||
view.domSelection().collapse(before, before.nodeValue!.length)
|
||||
break
|
||||
} else {
|
||||
node = before
|
||||
offset = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
view.input.composing = true
|
||||
}
|
||||
scheduleComposeEnd(view, timeoutComposition)
|
||||
}
|
||||
|
||||
editHandlers.compositionend = (view, event) => {
|
||||
if (view.composing) {
|
||||
view.input.composing = false
|
||||
view.input.compositionEndedAt = event.timeStamp
|
||||
view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0
|
||||
view.input.compositionNode = null
|
||||
if (view.input.compositionPendingChanges) Promise.resolve().then(() => view.domObserver.flush())
|
||||
view.input.compositionID++
|
||||
scheduleComposeEnd(view, 20)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleComposeEnd(view: EditorView, delay: number) {
|
||||
clearTimeout(view.input.composingTimeout)
|
||||
if (delay > -1) view.input.composingTimeout = setTimeout(() => endComposition(view), delay)
|
||||
}
|
||||
|
||||
export function clearComposition(view: EditorView) {
|
||||
if (view.composing) {
|
||||
view.input.composing = false
|
||||
view.input.compositionEndedAt = timestampFromCustomEvent()
|
||||
}
|
||||
while (view.input.compositionNodes.length > 0) view.input.compositionNodes.pop()!.markParentsDirty()
|
||||
}
|
||||
|
||||
export function findCompositionNode(view: EditorView) {
|
||||
let sel = view.domSelectionRange()
|
||||
if (!sel.focusNode) return null
|
||||
let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset)
|
||||
let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset)
|
||||
if (textBefore && textAfter && textBefore != textAfter) {
|
||||
let descAfter = textAfter.pmViewDesc
|
||||
if (!descAfter || !descAfter.isText(textAfter.nodeValue!)) {
|
||||
return textAfter
|
||||
} else if (view.input.compositionNode == textAfter) {
|
||||
let descBefore = textBefore.pmViewDesc
|
||||
if (!(!descBefore || !descBefore.isText(textBefore.nodeValue!)))
|
||||
return textAfter
|
||||
}
|
||||
}
|
||||
return textBefore || textAfter
|
||||
}
|
||||
|
||||
function timestampFromCustomEvent() {
|
||||
let event = document.createEvent("Event")
|
||||
event.initEvent("event", true, true)
|
||||
return event.timeStamp
|
||||
}
|
||||
|
||||
/// @internal
|
||||
export function endComposition(view: EditorView, forceUpdate = false) {
|
||||
if (browser.android && view.domObserver.flushingSoon >= 0) return
|
||||
view.domObserver.forceFlush()
|
||||
clearComposition(view)
|
||||
if (forceUpdate || view.docView && view.docView.dirty) {
|
||||
let sel = selectionFromDOM(view)
|
||||
if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel))
|
||||
else view.updateState(view.state)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function captureCopy(view: EditorView, dom: HTMLElement) {
|
||||
// The extra wrapper is somehow necessary on IE/Edge to prevent the
|
||||
// content from being mangled when it is put onto the clipboard
|
||||
if (!view.dom.parentNode) return
|
||||
let wrap = view.dom.parentNode.appendChild(document.createElement("div"))
|
||||
wrap.appendChild(dom)
|
||||
wrap.style.cssText = "position: fixed; left: -10000px; top: 10px"
|
||||
let sel = getSelection()!, range = document.createRange()
|
||||
range.selectNodeContents(dom)
|
||||
// Done because IE will fire a selectionchange moving the selection
|
||||
// to its start when removeAllRanges is called and the editor still
|
||||
// has focus (which will mess up the editor's selection state).
|
||||
view.dom.blur()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
setTimeout(() => {
|
||||
if (wrap.parentNode) wrap.parentNode.removeChild(wrap)
|
||||
view.focus()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// This is very crude, but unfortunately both these browsers _pretend_
|
||||
// that they have a clipboard API—all the objects and methods are
|
||||
// there, they just don't work, and they are hard to test.
|
||||
const brokenClipboardAPI = (browser.ie && browser.ie_version < 15) ||
|
||||
(browser.ios && browser.webkit_version < 604)
|
||||
|
||||
handlers.copy = editHandlers.cut = (view, _event) => {
|
||||
let event = _event as ClipboardEvent
|
||||
let sel = view.state.selection, cut = event.type == "cut"
|
||||
if (sel.empty) return
|
||||
|
||||
// IE and Edge's clipboard interface is completely broken
|
||||
let data = brokenClipboardAPI ? null : event.clipboardData
|
||||
let slice = sel.content(), {dom, text} = serializeForClipboard(view, slice)
|
||||
if (data) {
|
||||
event.preventDefault()
|
||||
data.clearData()
|
||||
data.setData("text/html", dom.innerHTML)
|
||||
data.setData("text/plain", text)
|
||||
} else {
|
||||
captureCopy(view, dom)
|
||||
}
|
||||
if (cut) view.dispatch(view.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent", "cut"))
|
||||
}
|
||||
|
||||
function sliceSingleNode(slice: Slice) {
|
||||
return slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 ? slice.content.firstChild : null
|
||||
}
|
||||
|
||||
function capturePaste(view: EditorView, event: ClipboardEvent) {
|
||||
if (!view.dom.parentNode) return
|
||||
let plainText = view.input.shiftKey || view.state.selection.$from.parent.type.spec.code
|
||||
let target = view.dom.parentNode.appendChild(document.createElement(plainText ? "textarea" : "div"))
|
||||
if (!plainText) target.contentEditable = "true"
|
||||
target.style.cssText = "position: fixed; left: -10000px; top: 10px"
|
||||
target.focus()
|
||||
let plain = view.input.shiftKey && view.input.lastKeyCode != 45
|
||||
setTimeout(() => {
|
||||
view.focus()
|
||||
if (target.parentNode) target.parentNode.removeChild(target)
|
||||
if (plainText) doPaste(view, (target as HTMLTextAreaElement).value, null, plain, event)
|
||||
else doPaste(view, target.textContent!, target.innerHTML, plain, event)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
export function doPaste(view: EditorView, text: string, html: string | null, preferPlain: boolean, event: ClipboardEvent) {
|
||||
let slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from)
|
||||
if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty))) return true
|
||||
if (!slice) return false
|
||||
|
||||
let singleNode = sliceSingleNode(slice)
|
||||
let tr = singleNode
|
||||
? view.state.tr.replaceSelectionWith(singleNode, preferPlain)
|
||||
: view.state.tr.replaceSelection(slice)
|
||||
view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"))
|
||||
return true
|
||||
}
|
||||
|
||||
function getText(clipboardData: DataTransfer) {
|
||||
let text = clipboardData.getData("text/plain") || clipboardData.getData("Text")
|
||||
if (text) return text
|
||||
let uris = clipboardData.getData("text/uri-list")
|
||||
return uris ? uris.replace(/\r?\n/g, " ") : ""
|
||||
}
|
||||
|
||||
editHandlers.paste = (view, _event) => {
|
||||
let event = _event as ClipboardEvent
|
||||
// Handling paste from JavaScript during composition is very poorly
|
||||
// handled by browsers, so as a dodgy but preferable kludge, we just
|
||||
// let the browser do its native thing there, except on Android,
|
||||
// where the editor is almost always composing.
|
||||
if (view.composing && !browser.android) return
|
||||
let data = brokenClipboardAPI ? null : event.clipboardData
|
||||
let plain = view.input.shiftKey && view.input.lastKeyCode != 45
|
||||
if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event))
|
||||
event.preventDefault()
|
||||
else
|
||||
capturePaste(view, event)
|
||||
}
|
||||
|
||||
export class Dragging {
|
||||
constructor(readonly slice: Slice, readonly move: boolean, readonly node?: NodeSelection) {}
|
||||
}
|
||||
|
||||
const dragCopyModifier: keyof DragEvent = browser.mac ? "altKey" : "ctrlKey"
|
||||
|
||||
handlers.dragstart = (view, _event) => {
|
||||
let event = _event as DragEvent
|
||||
let mouseDown = view.input.mouseDown
|
||||
if (mouseDown) mouseDown.done()
|
||||
if (!event.dataTransfer) return
|
||||
|
||||
let sel = view.state.selection
|
||||
let pos = sel.empty ? null : view.posAtCoords(eventCoords(event))
|
||||
let node: undefined | NodeSelection
|
||||
if (pos && pos.pos >= sel.from && pos.pos <= (sel instanceof NodeSelection ? sel.to - 1: sel.to)) {
|
||||
// In selection
|
||||
} else if (mouseDown && mouseDown.mightDrag) {
|
||||
node = NodeSelection.create(view.state.doc, mouseDown.mightDrag.pos)
|
||||
} else if (event.target && (event.target as HTMLElement).nodeType == 1) {
|
||||
let desc = view.docView.nearestDesc(event.target as HTMLElement, true)
|
||||
if (desc && desc.node.type.spec.draggable && desc != view.docView)
|
||||
node = NodeSelection.create(view.state.doc, desc.posBefore)
|
||||
}
|
||||
let draggedSlice = (node || view.state.selection).content()
|
||||
let {dom, text, slice} = serializeForClipboard(view, draggedSlice)
|
||||
event.dataTransfer.clearData()
|
||||
event.dataTransfer.setData(brokenClipboardAPI ? "Text" : "text/html", dom.innerHTML)
|
||||
// See https://github.com/ProseMirror/prosemirror/issues/1156
|
||||
event.dataTransfer.effectAllowed = "copyMove"
|
||||
if (!brokenClipboardAPI) event.dataTransfer.setData("text/plain", text)
|
||||
view.dragging = new Dragging(slice, !event[dragCopyModifier], node)
|
||||
}
|
||||
|
||||
handlers.dragend = view => {
|
||||
let dragging = view.dragging
|
||||
window.setTimeout(() => {
|
||||
if (view.dragging == dragging) view.dragging = null
|
||||
}, 50)
|
||||
}
|
||||
|
||||
editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault()
|
||||
|
||||
editHandlers.drop = (view, _event) => {
|
||||
let event = _event as DragEvent
|
||||
let dragging = view.dragging
|
||||
view.dragging = null
|
||||
|
||||
if (!event.dataTransfer) return
|
||||
|
||||
let eventPos = view.posAtCoords(eventCoords(event))
|
||||
if (!eventPos) return
|
||||
let $mouse = view.state.doc.resolve(eventPos.pos)
|
||||
let slice = dragging && dragging.slice
|
||||
if (slice) {
|
||||
view.someProp("transformPasted", f => { slice = f(slice!, view) })
|
||||
} else {
|
||||
slice = parseFromClipboard(view, getText(event.dataTransfer),
|
||||
brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse)
|
||||
}
|
||||
let move = !!(dragging && !event[dragCopyModifier])
|
||||
if (view.someProp("handleDrop", f => f(view, event, slice || Slice.empty, move))) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (!slice) return
|
||||
|
||||
event.preventDefault()
|
||||
let insertPos = slice ? dropPoint(view.state.doc, $mouse.pos, slice) : $mouse.pos
|
||||
if (insertPos == null) insertPos = $mouse.pos
|
||||
|
||||
let tr = view.state.tr
|
||||
if (move) {
|
||||
let {node} = dragging as Dragging
|
||||
if (node) node.replace(tr)
|
||||
else tr.deleteSelection()
|
||||
}
|
||||
|
||||
let pos = tr.mapping.map(insertPos)
|
||||
let isNode = slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1
|
||||
let beforeInsert = tr.doc
|
||||
if (isNode)
|
||||
tr.replaceRangeWith(pos, pos, slice.content.firstChild!)
|
||||
else
|
||||
tr.replaceRange(pos, pos, slice)
|
||||
if (tr.doc.eq(beforeInsert)) return
|
||||
|
||||
let $pos = tr.doc.resolve(pos)
|
||||
if (isNode && NodeSelection.isSelectable(slice.content.firstChild!) &&
|
||||
$pos.nodeAfter && $pos.nodeAfter.sameMarkup(slice.content.firstChild!)) {
|
||||
tr.setSelection(new NodeSelection($pos))
|
||||
} else {
|
||||
let end = tr.mapping.map(insertPos)
|
||||
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo)
|
||||
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)))
|
||||
}
|
||||
view.focus()
|
||||
view.dispatch(tr.setMeta("uiEvent", "drop"))
|
||||
}
|
||||
|
||||
handlers.focus = view => {
|
||||
view.input.lastFocus = Date.now()
|
||||
if (!view.focused) {
|
||||
view.domObserver.stop()
|
||||
view.dom.classList.add("ProseMirror-focused")
|
||||
view.domObserver.start()
|
||||
view.focused = true
|
||||
setTimeout(() => {
|
||||
if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange()))
|
||||
selectionToDOM(view)
|
||||
}, 20)
|
||||
}
|
||||
}
|
||||
|
||||
handlers.blur = (view, _event) => {
|
||||
let event = _event as FocusEvent
|
||||
if (view.focused) {
|
||||
view.domObserver.stop()
|
||||
view.dom.classList.remove("ProseMirror-focused")
|
||||
view.domObserver.start()
|
||||
if (event.relatedTarget && view.dom.contains(event.relatedTarget as HTMLElement))
|
||||
view.domObserver.currentSelection.clear()
|
||||
view.focused = false
|
||||
}
|
||||
}
|
||||
|
||||
handlers.beforeinput = (view, _event: Event) => {
|
||||
let event = _event as InputEvent
|
||||
// We should probably do more with beforeinput events, but support
|
||||
// is so spotty that I'm still waiting to see where they are going.
|
||||
|
||||
// Very specific hack to deal with backspace sometimes failing on
|
||||
// Chrome Android when after an uneditable node.
|
||||
if (browser.chrome && browser.android && event.inputType == "deleteContentBackward") {
|
||||
view.domObserver.flushSoon()
|
||||
let {domChangeCount} = view.input
|
||||
setTimeout(() => {
|
||||
if (view.input.domChangeCount != domChangeCount) return // Event already had some effect
|
||||
// This bug tends to close the virtual keyboard, so we refocus
|
||||
view.dom.blur()
|
||||
view.focus()
|
||||
if (view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) return
|
||||
let {$cursor} = view.state.selection as TextSelection
|
||||
// Crude approximation of backspace behavior when no command handled it
|
||||
if ($cursor && $cursor.pos > 0) view.dispatch(view.state.tr.delete($cursor.pos - 1, $cursor.pos).scrollIntoView())
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all handlers get registered
|
||||
for (let prop in editHandlers) handlers[prop] = editHandlers[prop]
|
||||
206
resources/app/node_modules/prosemirror-view/src/selection.ts
generated
vendored
Normal file
206
resources/app/node_modules/prosemirror-view/src/selection.ts
generated
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
import {TextSelection, NodeSelection, Selection} from "prosemirror-state"
|
||||
import {ResolvedPos} from "prosemirror-model"
|
||||
|
||||
import * as browser from "./browser"
|
||||
import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom"
|
||||
import {EditorView} from "./index"
|
||||
import {NodeViewDesc} from "./viewdesc"
|
||||
|
||||
export function selectionFromDOM(view: EditorView, origin: string | null = null) {
|
||||
let domSel = view.domSelectionRange(), doc = view.state.doc
|
||||
if (!domSel.focusNode) return null
|
||||
let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0
|
||||
let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1)
|
||||
if (head < 0) return null
|
||||
let $head = doc.resolve(head), $anchor, selection
|
||||
if (selectionCollapsed(domSel)) {
|
||||
$anchor = $head
|
||||
while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent
|
||||
let nearestDescNode = (nearestDesc as NodeViewDesc).node
|
||||
if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent
|
||||
&& !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) {
|
||||
let pos = nearestDesc.posBefore
|
||||
selection = new NodeSelection(head == pos ? $head : doc.resolve(pos))
|
||||
}
|
||||
} else {
|
||||
let anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1)
|
||||
if (anchor < 0) return null
|
||||
$anchor = doc.resolve(anchor)
|
||||
}
|
||||
|
||||
if (!selection) {
|
||||
let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1
|
||||
selection = selectionBetween(view, $anchor, $head, bias)
|
||||
}
|
||||
return selection
|
||||
}
|
||||
|
||||
function editorOwnsSelection(view: EditorView) {
|
||||
return view.editable ? view.hasFocus() :
|
||||
hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom)
|
||||
}
|
||||
|
||||
export function selectionToDOM(view: EditorView, force = false) {
|
||||
let sel = view.state.selection
|
||||
syncNodeSelection(view, sel)
|
||||
|
||||
if (!editorOwnsSelection(view)) return
|
||||
|
||||
// The delayed drag selection causes issues with Cell Selections
|
||||
// in Safari. And the drag selection delay is to workarond issues
|
||||
// which only present in Chrome.
|
||||
if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) {
|
||||
let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection
|
||||
if (domSel.anchorNode && curSel.anchorNode &&
|
||||
isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset,
|
||||
curSel.anchorNode, curSel.anchorOffset)) {
|
||||
view.input.mouseDown.delayedSelectionSync = true
|
||||
view.domObserver.setCurSelection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
view.domObserver.disconnectSelection()
|
||||
|
||||
if (view.cursorWrapper) {
|
||||
selectCursorWrapper(view)
|
||||
} else {
|
||||
let {anchor, head} = sel, resetEditableFrom, resetEditableTo
|
||||
if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) {
|
||||
if (!sel.$from.parent.inlineContent)
|
||||
resetEditableFrom = temporarilyEditableNear(view, sel.from)
|
||||
if (!sel.empty && !sel.$from.parent.inlineContent)
|
||||
resetEditableTo = temporarilyEditableNear(view, sel.to)
|
||||
}
|
||||
view.docView.setSelection(anchor, head, view.root, force)
|
||||
if (brokenSelectBetweenUneditable) {
|
||||
if (resetEditableFrom) resetEditable(resetEditableFrom)
|
||||
if (resetEditableTo) resetEditable(resetEditableTo)
|
||||
}
|
||||
if (sel.visible) {
|
||||
view.dom.classList.remove("ProseMirror-hideselection")
|
||||
} else {
|
||||
view.dom.classList.add("ProseMirror-hideselection")
|
||||
if ("onselectionchange" in document) removeClassOnSelectionChange(view)
|
||||
}
|
||||
}
|
||||
|
||||
view.domObserver.setCurSelection()
|
||||
view.domObserver.connectSelection()
|
||||
}
|
||||
|
||||
// Kludge to work around Webkit not allowing a selection to start/end
|
||||
// between non-editable block nodes. We briefly make something
|
||||
// editable, set the selection, then set it uneditable again.
|
||||
|
||||
const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63
|
||||
|
||||
function temporarilyEditableNear(view: EditorView, pos: number) {
|
||||
let {node, offset} = view.docView.domFromPos(pos, 0)
|
||||
let after = offset < node.childNodes.length ? node.childNodes[offset] : null
|
||||
let before = offset ? node.childNodes[offset - 1] : null
|
||||
if (browser.safari && after && (after as HTMLElement).contentEditable == "false") return setEditable(after as HTMLElement)
|
||||
if ((!after || (after as HTMLElement).contentEditable == "false") &&
|
||||
(!before || (before as HTMLElement).contentEditable == "false")) {
|
||||
if (after) return setEditable(after as HTMLElement)
|
||||
else if (before) return setEditable(before as HTMLElement)
|
||||
}
|
||||
}
|
||||
|
||||
function setEditable(element: HTMLElement) {
|
||||
element.contentEditable = "true"
|
||||
if (browser.safari && element.draggable) { element.draggable = false; (element as any).wasDraggable = true }
|
||||
return element
|
||||
}
|
||||
|
||||
function resetEditable(element: HTMLElement) {
|
||||
element.contentEditable = "false"
|
||||
if ((element as any).wasDraggable) { element.draggable = true; (element as any).wasDraggable = null }
|
||||
}
|
||||
|
||||
function removeClassOnSelectionChange(view: EditorView) {
|
||||
let doc = view.dom.ownerDocument
|
||||
doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
|
||||
let domSel = view.domSelectionRange()
|
||||
let node = domSel.anchorNode, offset = domSel.anchorOffset
|
||||
doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
|
||||
if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
|
||||
doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
|
||||
setTimeout(() => {
|
||||
if (!editorOwnsSelection(view) || view.state.selection.visible)
|
||||
view.dom.classList.remove("ProseMirror-hideselection")
|
||||
}, 20)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectCursorWrapper(view: EditorView) {
|
||||
let domSel = view.domSelection(), range = document.createRange()
|
||||
let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG"
|
||||
if (img) range.setEnd(node.parentNode!, domIndex(node) + 1)
|
||||
else range.setEnd(node, 0)
|
||||
range.collapse(false)
|
||||
domSel.removeAllRanges()
|
||||
domSel.addRange(range)
|
||||
// Kludge to kill 'control selection' in IE11 when selecting an
|
||||
// invisible cursor wrapper, since that would result in those weird
|
||||
// resize handles and a selection that considers the absolutely
|
||||
// positioned wrapper, rather than the root editable node, the
|
||||
// focused element.
|
||||
if (!img && !view.state.selection.visible && browser.ie && browser.ie_version <= 11) {
|
||||
;(node as any).disabled = true
|
||||
;(node as any).disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
export function syncNodeSelection(view: EditorView, sel: Selection) {
|
||||
if (sel instanceof NodeSelection) {
|
||||
let desc = view.docView.descAt(sel.from)
|
||||
if (desc != view.lastSelectedViewDesc) {
|
||||
clearNodeSelection(view)
|
||||
if (desc) (desc as NodeViewDesc).selectNode()
|
||||
view.lastSelectedViewDesc = desc
|
||||
}
|
||||
} else {
|
||||
clearNodeSelection(view)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all DOM statefulness of the last node selection.
|
||||
function clearNodeSelection(view: EditorView) {
|
||||
if (view.lastSelectedViewDesc) {
|
||||
if (view.lastSelectedViewDesc.parent)
|
||||
(view.lastSelectedViewDesc as NodeViewDesc).deselectNode()
|
||||
view.lastSelectedViewDesc = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function selectionBetween(view: EditorView, $anchor: ResolvedPos, $head: ResolvedPos, bias?: number) {
|
||||
return view.someProp("createSelectionBetween", f => f(view, $anchor, $head))
|
||||
|| TextSelection.between($anchor, $head, bias)
|
||||
}
|
||||
|
||||
export function hasFocusAndSelection(view: EditorView) {
|
||||
if (view.editable && !view.hasFocus()) return false
|
||||
return hasSelection(view)
|
||||
}
|
||||
|
||||
export function hasSelection(view: EditorView) {
|
||||
let sel = view.domSelectionRange()
|
||||
if (!sel.anchorNode) return false
|
||||
try {
|
||||
// Firefox will raise 'permission denied' errors when accessing
|
||||
// properties of `sel.anchorNode` when it's in a generated CSS
|
||||
// element.
|
||||
return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) &&
|
||||
(view.editable || view.dom.contains(sel.focusNode!.nodeType == 3 ? sel.focusNode!.parentNode : sel.focusNode))
|
||||
} catch(_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function anchorInRightPlace(view: EditorView) {
|
||||
let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0)
|
||||
let domSel = view.domSelectionRange()
|
||||
return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset)
|
||||
}
|
||||
1524
resources/app/node_modules/prosemirror-view/src/viewdesc.ts
generated
vendored
Normal file
1524
resources/app/node_modules/prosemirror-view/src/viewdesc.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
54
resources/app/node_modules/prosemirror-view/style/prosemirror.css
generated
vendored
Normal file
54
resources/app/node_modules/prosemirror-view/style/prosemirror.css
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
white-space: break-spaces;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection { background: transparent; }
|
||||
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
|
||||
.ProseMirror-hideselection { caret-color: transparent; }
|
||||
|
||||
/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
|
||||
.ProseMirror [draggable][contenteditable=false] { user-select: text }
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px; top: -2px; bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Protect against generic img rules */
|
||||
|
||||
img.ProseMirror-separator {
|
||||
display: inline !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
Reference in New Issue
Block a user