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

View File

@@ -0,0 +1,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.

View File

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

View File

@@ -0,0 +1,295 @@
'use strict';
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
Object.defineProperty(exports, '__esModule', {
value: true
});
var prosemirrorKeymap = require('prosemirror-keymap');
var prosemirrorState = require('prosemirror-state');
var prosemirrorModel = require('prosemirror-model');
var prosemirrorView = require('prosemirror-view');
var GapCursor = function (_prosemirrorState$Sel) {
_inherits(GapCursor, _prosemirrorState$Sel);
var _super = _createSuper(GapCursor);
function GapCursor($pos) {
_classCallCheck(this, GapCursor);
return _super.call(this, $pos, $pos);
}
_createClass(GapCursor, [{
key: "map",
value: function map(doc, mapping) {
var $pos = doc.resolve(mapping.map(this.head));
return GapCursor.valid($pos) ? new GapCursor($pos) : prosemirrorState.Selection.near($pos);
}
}, {
key: "content",
value: function content() {
return prosemirrorModel.Slice.empty;
}
}, {
key: "eq",
value: function eq(other) {
return other instanceof GapCursor && other.head == this.head;
}
}, {
key: "toJSON",
value: function toJSON() {
return {
type: "gapcursor",
pos: this.head
};
}
}, {
key: "getBookmark",
value: function getBookmark() {
return new GapBookmark(this.anchor);
}
}], [{
key: "fromJSON",
value: function fromJSON(doc, json) {
if (typeof json.pos != "number") throw new RangeError("Invalid input for GapCursor.fromJSON");
return new GapCursor(doc.resolve(json.pos));
}
}, {
key: "valid",
value: function valid($pos) {
var parent = $pos.parent;
if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) return false;
var override = parent.type.spec.allowGapCursor;
if (override != null) return override;
var deflt = parent.contentMatchAt($pos.index()).defaultType;
return deflt && deflt.isTextblock;
}
}, {
key: "findGapCursorFrom",
value: function findGapCursorFrom($pos, dir) {
var mustMove = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
search: for (;;) {
if (!mustMove && GapCursor.valid($pos)) return $pos;
var pos = $pos.pos,
next = null;
for (var d = $pos.depth;; d--) {
var parent = $pos.node(d);
if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1);
break;
} else if (d == 0) {
return null;
}
pos += dir;
var $cur = $pos.doc.resolve(pos);
if (GapCursor.valid($cur)) return $cur;
}
for (;;) {
var inside = dir > 0 ? next.firstChild : next.lastChild;
if (!inside) {
if (next.isAtom && !next.isText && !prosemirrorState.NodeSelection.isSelectable(next)) {
$pos = $pos.doc.resolve(pos + next.nodeSize * dir);
mustMove = false;
continue search;
}
break;
}
next = inside;
pos += dir;
var _$cur = $pos.doc.resolve(pos);
if (GapCursor.valid(_$cur)) return _$cur;
}
return null;
}
}
}]);
return GapCursor;
}(prosemirrorState.Selection);
GapCursor.prototype.visible = false;
GapCursor.findFrom = GapCursor.findGapCursorFrom;
prosemirrorState.Selection.jsonID("gapcursor", GapCursor);
var GapBookmark = function () {
function GapBookmark(pos) {
_classCallCheck(this, GapBookmark);
this.pos = pos;
}
_createClass(GapBookmark, [{
key: "map",
value: function map(mapping) {
return new GapBookmark(mapping.map(this.pos));
}
}, {
key: "resolve",
value: function resolve(doc) {
var $pos = doc.resolve(this.pos);
return GapCursor.valid($pos) ? new GapCursor($pos) : prosemirrorState.Selection.near($pos);
}
}]);
return GapBookmark;
}();
function closedBefore($pos) {
for (var d = $pos.depth; d >= 0; d--) {
var index = $pos.index(d),
parent = $pos.node(d);
if (index == 0) {
if (parent.type.spec.isolating) return true;
continue;
}
for (var before = parent.child(index - 1);; before = before.lastChild) {
if (before.childCount == 0 && !before.inlineContent || before.isAtom || before.type.spec.isolating) return true;
if (before.inlineContent) return false;
}
}
return true;
}
function closedAfter($pos) {
for (var d = $pos.depth; d >= 0; d--) {
var index = $pos.indexAfter(d),
parent = $pos.node(d);
if (index == parent.childCount) {
if (parent.type.spec.isolating) return true;
continue;
}
for (var after = parent.child(index);; after = after.firstChild) {
if (after.childCount == 0 && !after.inlineContent || after.isAtom || after.type.spec.isolating) return true;
if (after.inlineContent) return false;
}
}
return true;
}
function gapCursor() {
return new prosemirrorState.Plugin({
props: {
decorations: drawGapCursor,
createSelectionBetween: function createSelectionBetween(_view, $anchor, $head) {
return $anchor.pos == $head.pos && GapCursor.valid($head) ? new GapCursor($head) : null;
},
handleClick: handleClick,
handleKeyDown: handleKeyDown,
handleDOMEvents: {
beforeinput: beforeinput
}
}
});
}
var handleKeyDown = prosemirrorKeymap.keydownHandler({
"ArrowLeft": arrow("horiz", -1),
"ArrowRight": arrow("horiz", 1),
"ArrowUp": arrow("vert", -1),
"ArrowDown": arrow("vert", 1)
});
function arrow(axis, dir) {
var dirStr = axis == "vert" ? dir > 0 ? "down" : "up" : dir > 0 ? "right" : "left";
return function (state, dispatch, view) {
var sel = state.selection;
var $start = dir > 0 ? sel.$to : sel.$from,
mustMove = sel.empty;
if (sel instanceof prosemirrorState.TextSelection) {
if (!view.endOfTextblock(dirStr) || $start.depth == 0) return false;
mustMove = false;
$start = state.doc.resolve(dir > 0 ? $start.after() : $start.before());
}
var $found = GapCursor.findGapCursorFrom($start, dir, mustMove);
if (!$found) return false;
if (dispatch) dispatch(state.tr.setSelection(new GapCursor($found)));
return true;
};
}
function handleClick(view, pos, event) {
if (!view || !view.editable) return false;
var $pos = view.state.doc.resolve(pos);
if (!GapCursor.valid($pos)) return false;
var clickPos = view.posAtCoords({
left: event.clientX,
top: event.clientY
});
if (clickPos && clickPos.inside > -1 && prosemirrorState.NodeSelection.isSelectable(view.state.doc.nodeAt(clickPos.inside))) return false;
view.dispatch(view.state.tr.setSelection(new GapCursor($pos)));
return true;
}
function beforeinput(view, event) {
if (event.inputType != "insertCompositionText" || !(view.state.selection instanceof GapCursor)) return false;
var $from = view.state.selection.$from;
var insert = $from.parent.contentMatchAt($from.index()).findWrapping(view.state.schema.nodes.text);
if (!insert) return false;
var frag = prosemirrorModel.Fragment.empty;
for (var i = insert.length - 1; i >= 0; i--) {
frag = prosemirrorModel.Fragment.from(insert[i].createAndFill(null, frag));
}
var tr = view.state.tr.replace($from.pos, $from.pos, new prosemirrorModel.Slice(frag, 0, 0));
tr.setSelection(prosemirrorState.TextSelection.near(tr.doc.resolve($from.pos + 1)));
view.dispatch(tr);
return false;
}
function drawGapCursor(state) {
if (!(state.selection instanceof GapCursor)) return null;
var node = document.createElement("div");
node.className = "ProseMirror-gapcursor";
return prosemirrorView.DecorationSet.create(state.doc, [prosemirrorView.Decoration.widget(state.selection.head, node, {
key: "gapcursor"
})]);
}
exports.GapCursor = GapCursor;
exports.gapCursor = gapCursor;

View File

@@ -0,0 +1,31 @@
import { Selection, Plugin } from 'prosemirror-state';
import { ResolvedPos, Node, Slice } from 'prosemirror-model';
import { Mappable } from 'prosemirror-transform';
/**
Gap cursor selections are represented using this class. Its
`$anchor` and `$head` properties both point at the cursor position.
*/
declare class GapCursor extends Selection {
/**
Create a gap cursor.
*/
constructor($pos: ResolvedPos);
map(doc: Node, mapping: Mappable): Selection;
content(): Slice;
eq(other: Selection): boolean;
toJSON(): any;
}
/**
Create a gap cursor plugin. When enabled, this will capture clicks
near and arrow-key-motion past places that don't have a normally
selectable position nearby, and create a gap cursor selection for
them. The cursor is drawn as an element with class
`ProseMirror-gapcursor`. You can either include
`style/gapcursor.css` from the package's directory or add your own
styles to make it visible.
*/
declare function gapCursor(): Plugin;
export { GapCursor, gapCursor };

View File

@@ -0,0 +1,236 @@
import { keydownHandler } from 'prosemirror-keymap';
import { Selection, NodeSelection, TextSelection, Plugin } from 'prosemirror-state';
import { Slice, Fragment } from 'prosemirror-model';
import { DecorationSet, Decoration } from 'prosemirror-view';
/**
Gap cursor selections are represented using this class. Its
`$anchor` and `$head` properties both point at the cursor position.
*/
class GapCursor extends Selection {
/**
Create a gap cursor.
*/
constructor($pos) {
super($pos, $pos);
}
map(doc, mapping) {
let $pos = doc.resolve(mapping.map(this.head));
return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
}
content() { return Slice.empty; }
eq(other) {
return other instanceof GapCursor && other.head == this.head;
}
toJSON() {
return { type: "gapcursor", pos: this.head };
}
/**
@internal
*/
static fromJSON(doc, json) {
if (typeof json.pos != "number")
throw new RangeError("Invalid input for GapCursor.fromJSON");
return new GapCursor(doc.resolve(json.pos));
}
/**
@internal
*/
getBookmark() { return new GapBookmark(this.anchor); }
/**
@internal
*/
static valid($pos) {
let parent = $pos.parent;
if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos))
return false;
let override = parent.type.spec.allowGapCursor;
if (override != null)
return override;
let deflt = parent.contentMatchAt($pos.index()).defaultType;
return deflt && deflt.isTextblock;
}
/**
@internal
*/
static findGapCursorFrom($pos, dir, mustMove = false) {
search: for (;;) {
if (!mustMove && GapCursor.valid($pos))
return $pos;
let pos = $pos.pos, next = null;
// Scan up from this position
for (let d = $pos.depth;; d--) {
let parent = $pos.node(d);
if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1);
break;
}
else if (d == 0) {
return null;
}
pos += dir;
let $cur = $pos.doc.resolve(pos);
if (GapCursor.valid($cur))
return $cur;
}
// And then down into the next node
for (;;) {
let inside = dir > 0 ? next.firstChild : next.lastChild;
if (!inside) {
if (next.isAtom && !next.isText && !NodeSelection.isSelectable(next)) {
$pos = $pos.doc.resolve(pos + next.nodeSize * dir);
mustMove = false;
continue search;
}
break;
}
next = inside;
pos += dir;
let $cur = $pos.doc.resolve(pos);
if (GapCursor.valid($cur))
return $cur;
}
return null;
}
}
}
GapCursor.prototype.visible = false;
GapCursor.findFrom = GapCursor.findGapCursorFrom;
Selection.jsonID("gapcursor", GapCursor);
class GapBookmark {
constructor(pos) {
this.pos = pos;
}
map(mapping) {
return new GapBookmark(mapping.map(this.pos));
}
resolve(doc) {
let $pos = doc.resolve(this.pos);
return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
}
}
function closedBefore($pos) {
for (let d = $pos.depth; d >= 0; d--) {
let index = $pos.index(d), parent = $pos.node(d);
// At the start of this parent, look at next one
if (index == 0) {
if (parent.type.spec.isolating)
return true;
continue;
}
// See if the node before (or its first ancestor) is closed
for (let before = parent.child(index - 1);; before = before.lastChild) {
if ((before.childCount == 0 && !before.inlineContent) || before.isAtom || before.type.spec.isolating)
return true;
if (before.inlineContent)
return false;
}
}
// Hit start of document
return true;
}
function closedAfter($pos) {
for (let d = $pos.depth; d >= 0; d--) {
let index = $pos.indexAfter(d), parent = $pos.node(d);
if (index == parent.childCount) {
if (parent.type.spec.isolating)
return true;
continue;
}
for (let after = parent.child(index);; after = after.firstChild) {
if ((after.childCount == 0 && !after.inlineContent) || after.isAtom || after.type.spec.isolating)
return true;
if (after.inlineContent)
return false;
}
}
return true;
}
/**
Create a gap cursor plugin. When enabled, this will capture clicks
near and arrow-key-motion past places that don't have a normally
selectable position nearby, and create a gap cursor selection for
them. The cursor is drawn as an element with class
`ProseMirror-gapcursor`. You can either include
`style/gapcursor.css` from the package's directory or add your own
styles to make it visible.
*/
function gapCursor() {
return new Plugin({
props: {
decorations: drawGapCursor,
createSelectionBetween(_view, $anchor, $head) {
return $anchor.pos == $head.pos && GapCursor.valid($head) ? new GapCursor($head) : null;
},
handleClick,
handleKeyDown,
handleDOMEvents: { beforeinput: beforeinput }
}
});
}
const handleKeyDown = keydownHandler({
"ArrowLeft": arrow("horiz", -1),
"ArrowRight": arrow("horiz", 1),
"ArrowUp": arrow("vert", -1),
"ArrowDown": arrow("vert", 1)
});
function arrow(axis, dir) {
const dirStr = axis == "vert" ? (dir > 0 ? "down" : "up") : (dir > 0 ? "right" : "left");
return function (state, dispatch, view) {
let sel = state.selection;
let $start = dir > 0 ? sel.$to : sel.$from, mustMove = sel.empty;
if (sel instanceof TextSelection) {
if (!view.endOfTextblock(dirStr) || $start.depth == 0)
return false;
mustMove = false;
$start = state.doc.resolve(dir > 0 ? $start.after() : $start.before());
}
let $found = GapCursor.findGapCursorFrom($start, dir, mustMove);
if (!$found)
return false;
if (dispatch)
dispatch(state.tr.setSelection(new GapCursor($found)));
return true;
};
}
function handleClick(view, pos, event) {
if (!view || !view.editable)
return false;
let $pos = view.state.doc.resolve(pos);
if (!GapCursor.valid($pos))
return false;
let clickPos = view.posAtCoords({ left: event.clientX, top: event.clientY });
if (clickPos && clickPos.inside > -1 && NodeSelection.isSelectable(view.state.doc.nodeAt(clickPos.inside)))
return false;
view.dispatch(view.state.tr.setSelection(new GapCursor($pos)));
return true;
}
// This is a hack that, when a composition starts while a gap cursor
// is active, quickly creates an inline context for the composition to
// happen in, to avoid it being aborted by the DOM selection being
// moved into a valid position.
function beforeinput(view, event) {
if (event.inputType != "insertCompositionText" || !(view.state.selection instanceof GapCursor))
return false;
let { $from } = view.state.selection;
let insert = $from.parent.contentMatchAt($from.index()).findWrapping(view.state.schema.nodes.text);
if (!insert)
return false;
let frag = Fragment.empty;
for (let i = insert.length - 1; i >= 0; i--)
frag = Fragment.from(insert[i].createAndFill(null, frag));
let tr = view.state.tr.replace($from.pos, $from.pos, new Slice(frag, 0, 0));
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
view.dispatch(tr);
return false;
}
function drawGapCursor(state) {
if (!(state.selection instanceof GapCursor))
return null;
let node = document.createElement("div");
node.className = "ProseMirror-gapcursor";
return DecorationSet.create(state.doc, [Decoration.widget(state.selection.head, node, { key: "gapcursor" })]);
}
export { GapCursor, gapCursor };

View File

@@ -0,0 +1,41 @@
{
"name": "prosemirror-gapcursor",
"version": "1.3.2",
"description": "ProseMirror plugin for cursors at normally impossible-to-reach positions",
"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/gapcursor.css": "./style/gapcursor.css"
},
"sideEffects": [
"./style/gapcursor.css"
],
"style": "style/gapcursor.css",
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijn@haverbeke.berlin",
"web": "http://marijnhaverbeke.nl"
}
],
"repository": {
"type": "git",
"url": "git://github.com/prosemirror/prosemirror-gapcursor.git"
},
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
},
"devDependencies": {
"@prosemirror/buildhelper": "^0.1.5"
}
}

View File

@@ -0,0 +1,18 @@
This is a plugin that adds a type of selection for focusing places
that don't allow regular selection (such as positions that have a leaf
block node, table, or the end of the document both before and after
them).
You'll probably want to load `style/gapcursor.css`, which contains
basic styling for the simulated cursor (as a short, blinking
horizontal stripe).
By default, gap cursor are only allowed in places where the default
content node (in the schema content constraints) is a textblock node.
You can customize this by adding an `allowGapCursor` property to your
node specs—if it's true, gap cursor are allowed everywhere in that
node, if it's `false` they are never allowed.
@gapCursor
@GapCursor

View File

@@ -0,0 +1,137 @@
import {Selection, NodeSelection} from "prosemirror-state"
import {Slice, ResolvedPos, Node} from "prosemirror-model"
import {Mappable} from "prosemirror-transform"
/// Gap cursor selections are represented using this class. Its
/// `$anchor` and `$head` properties both point at the cursor position.
export class GapCursor extends Selection {
/// Create a gap cursor.
constructor($pos: ResolvedPos) {
super($pos, $pos)
}
map(doc: Node, mapping: Mappable): Selection {
let $pos = doc.resolve(mapping.map(this.head))
return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos)
}
content() { return Slice.empty }
eq(other: Selection): boolean {
return other instanceof GapCursor && other.head == this.head
}
toJSON(): any {
return {type: "gapcursor", pos: this.head}
}
/// @internal
static fromJSON(doc: Node, json: any): GapCursor {
if (typeof json.pos != "number") throw new RangeError("Invalid input for GapCursor.fromJSON")
return new GapCursor(doc.resolve(json.pos))
}
/// @internal
getBookmark() { return new GapBookmark(this.anchor) }
/// @internal
static valid($pos: ResolvedPos) {
let parent = $pos.parent
if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) return false
let override = parent.type.spec.allowGapCursor
if (override != null) return override
let deflt = parent.contentMatchAt($pos.index()).defaultType
return deflt && deflt.isTextblock
}
/// @internal
static findGapCursorFrom($pos: ResolvedPos, dir: number, mustMove = false) {
search: for (;;) {
if (!mustMove && GapCursor.valid($pos)) return $pos
let pos = $pos.pos, next = null
// Scan up from this position
for (let d = $pos.depth;; d--) {
let parent = $pos.node(d)
if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1)
break
} else if (d == 0) {
return null
}
pos += dir
let $cur = $pos.doc.resolve(pos)
if (GapCursor.valid($cur)) return $cur
}
// And then down into the next node
for (;;) {
let inside: Node | null = dir > 0 ? next.firstChild : next.lastChild
if (!inside) {
if (next.isAtom && !next.isText && !NodeSelection.isSelectable(next)) {
$pos = $pos.doc.resolve(pos + next.nodeSize * dir)
mustMove = false
continue search
}
break
}
next = inside
pos += dir
let $cur = $pos.doc.resolve(pos)
if (GapCursor.valid($cur)) return $cur
}
return null
}
}
}
GapCursor.prototype.visible = false
;(GapCursor as any).findFrom = GapCursor.findGapCursorFrom
Selection.jsonID("gapcursor", GapCursor)
class GapBookmark {
constructor(readonly pos: number) {}
map(mapping: Mappable) {
return new GapBookmark(mapping.map(this.pos))
}
resolve(doc: Node) {
let $pos = doc.resolve(this.pos)
return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos)
}
}
function closedBefore($pos: ResolvedPos) {
for (let d = $pos.depth; d >= 0; d--) {
let index = $pos.index(d), parent = $pos.node(d)
// At the start of this parent, look at next one
if (index == 0) {
if (parent.type.spec.isolating) return true
continue
}
// See if the node before (or its first ancestor) is closed
for (let before = parent.child(index - 1);; before = before.lastChild!) {
if ((before.childCount == 0 && !before.inlineContent) || before.isAtom || before.type.spec.isolating) return true
if (before.inlineContent) return false
}
}
// Hit start of document
return true
}
function closedAfter($pos: ResolvedPos) {
for (let d = $pos.depth; d >= 0; d--) {
let index = $pos.indexAfter(d), parent = $pos.node(d)
if (index == parent.childCount) {
if (parent.type.spec.isolating) return true
continue
}
for (let after = parent.child(index);; after = after.firstChild!) {
if ((after.childCount == 0 && !after.inlineContent) || after.isAtom || after.type.spec.isolating) return true
if (after.inlineContent) return false
}
}
return true
}

View File

@@ -0,0 +1,91 @@
import {keydownHandler} from "prosemirror-keymap"
import {TextSelection, NodeSelection, Plugin, Command, EditorState} from "prosemirror-state"
import {Fragment, Slice} from "prosemirror-model"
import {Decoration, DecorationSet, EditorView} from "prosemirror-view"
import {GapCursor} from "./gapcursor"
/// Create a gap cursor plugin. When enabled, this will capture clicks
/// near and arrow-key-motion past places that don't have a normally
/// selectable position nearby, and create a gap cursor selection for
/// them. The cursor is drawn as an element with class
/// `ProseMirror-gapcursor`. You can either include
/// `style/gapcursor.css` from the package's directory or add your own
/// styles to make it visible.
export function gapCursor(): Plugin {
return new Plugin({
props: {
decorations: drawGapCursor,
createSelectionBetween(_view, $anchor, $head) {
return $anchor.pos == $head.pos && GapCursor.valid($head) ? new GapCursor($head) : null
},
handleClick,
handleKeyDown,
handleDOMEvents: {beforeinput: beforeinput as any}
}
})
}
export {GapCursor}
const handleKeyDown = keydownHandler({
"ArrowLeft": arrow("horiz", -1),
"ArrowRight": arrow("horiz", 1),
"ArrowUp": arrow("vert", -1),
"ArrowDown": arrow("vert", 1)
})
function arrow(axis: "vert" | "horiz", dir: number): Command {
const dirStr = axis == "vert" ? (dir > 0 ? "down" : "up") : (dir > 0 ? "right" : "left")
return function(state, dispatch, view) {
let sel = state.selection
let $start = dir > 0 ? sel.$to : sel.$from, mustMove = sel.empty
if (sel instanceof TextSelection) {
if (!view!.endOfTextblock(dirStr) || $start.depth == 0) return false
mustMove = false
$start = state.doc.resolve(dir > 0 ? $start.after() : $start.before())
}
let $found = GapCursor.findGapCursorFrom($start, dir, mustMove)
if (!$found) return false
if (dispatch) dispatch(state.tr.setSelection(new GapCursor($found)))
return true
}
}
function handleClick(view: EditorView, pos: number, event: MouseEvent) {
if (!view || !view.editable) return false
let $pos = view.state.doc.resolve(pos)
if (!GapCursor.valid($pos)) return false
let clickPos = view.posAtCoords({left: event.clientX, top: event.clientY})
if (clickPos && clickPos.inside > -1 && NodeSelection.isSelectable(view.state.doc.nodeAt(clickPos.inside)!)) return false
view.dispatch(view.state.tr.setSelection(new GapCursor($pos)))
return true
}
// This is a hack that, when a composition starts while a gap cursor
// is active, quickly creates an inline context for the composition to
// happen in, to avoid it being aborted by the DOM selection being
// moved into a valid position.
function beforeinput(view: EditorView, event: InputEvent) {
if (event.inputType != "insertCompositionText" || !(view.state.selection instanceof GapCursor)) return false
let {$from} = view.state.selection
let insert = $from.parent.contentMatchAt($from.index()).findWrapping(view.state.schema.nodes.text)
if (!insert) return false
let frag = Fragment.empty
for (let i = insert.length - 1; i >= 0; i--) frag = Fragment.from(insert[i].createAndFill(null, frag))
let tr = view.state.tr.replace($from.pos, $from.pos, new Slice(frag, 0, 0))
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)))
view.dispatch(tr)
return false
}
function drawGapCursor(state: EditorState) {
if (!(state.selection instanceof GapCursor)) return null
let node = document.createElement("div")
node.className = "ProseMirror-gapcursor"
return DecorationSet.create(state.doc, [Decoration.widget(state.selection.head, node, {key: "gapcursor"})])
}

View File

@@ -0,0 +1,25 @@
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}