prosemirror-gapcursor
Version:
ProseMirror plugin for cursors at normally impossible-to-reach positions
138 lines (119 loc) • 4.31 kB
text/typescript
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
}