You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
4.2 KiB
138 lines
4.2 KiB
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
|
|
}
|