import { Node, ResolvedPos, Slice } from "prosemirror-model"; import { Selection, SelectionBookmark } from "prosemirror-state"; import { Mapping } from "prosemirror-transform"; import { Bias, NodeTypeSpec } from "../types"; export interface WedgeSelectionJson { type: "wedge"; pos: number; } const wedgeJsonType: WedgeSelectionJson["type"] = "wedge"; /** * Wedge cursor selections are represented using this class. Its `$anchor` and * `$head` properties both point at the cursor position. */ export class WedgeSelection extends Selection { public readonly visible = false; constructor($pos: ResolvedPos) { super($pos, $pos); } public map(doc: Node, mapping: Mapping): Selection { const $pos = doc.resolve(mapping.map(this.head)); return WedgeSelection.valid($pos) ? new WedgeSelection($pos) : Selection.near($pos); } public content() { return Slice.empty; } public eq(other: Selection): boolean { return other instanceof WedgeSelection && other.head == this.head; } public toJSON(): WedgeSelectionJson { return { type: wedgeJsonType, pos: this.head }; } public static fromJSON(doc: Node, json: WedgeSelectionJson) { return new WedgeSelection(doc.resolve(json.pos)); } public getBookmark() { return new WedgeBookmark(this.anchor); } public static valid($pos: ResolvedPos): boolean { const parent = $pos.parent; if (parent.isTextblock || !cursorBarrierBefore($pos) || !cursorBarrierAfter($pos)) { return false; } const override = (parent.type.spec as NodeTypeSpec).allowWedge; if (override !== undefined) { return override; } const deflt = parent.contentMatchAt($pos.index()).defaultType; return deflt !== undefined && deflt.isTextblock; } public static findFrom($pos: ResolvedPos, dir: number, _ = false, mustMove = false) { const bias = dir < 0 ? Bias.BACKWARD : Bias.FORWARD; if (!mustMove && WedgeSelection.valid($pos)) { return new WedgeSelection($pos); } let pos = $pos.pos; let next = null; // Scan up from this position for (let d = $pos.depth; ; d--) { const parent = $pos.node(d); if (bias == Bias.FORWARD ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) { next = parent.maybeChild(bias == Bias.FORWARD ? $pos.indexAfter(d) : $pos.index(d) - 1); break; } else if (d == 0) { return; } pos += bias == Bias.FORWARD ? 1 : -1; const $cur = $pos.doc.resolve(pos); if (WedgeSelection.valid($cur)) { return new WedgeSelection($cur); } } // And then down into the next node for (; next != null; ) { next = bias == Bias.FORWARD ? next.firstChild : next.lastChild; if (next == null) { break; } pos += bias == Bias.FORWARD ? 1 : -1; const $cur = $pos.doc.resolve(pos); if (WedgeSelection.valid($cur)) { return new WedgeSelection($cur); } } return null; } } Selection.jsonID(wedgeJsonType, WedgeSelection); export class WedgeBookmark implements SelectionBookmark { constructor(private readonly pos: number) {} public map(mapping: Mapping) { return new WedgeBookmark(mapping.map(this.pos)); } public resolve(doc: Node): Selection { const $pos = doc.resolve(this.pos); return WedgeSelection.valid($pos) ? new WedgeSelection($pos) : Selection.near($pos); } } function cursorBarrierBefore($pos: ResolvedPos) { for (let d = $pos.depth; d >= 0; d--) { const index = $pos.index(d); // At the start of this parent, look at next one if (index == 0) { continue; } // See if the node before (or its first ancestor) is closed for (let before = $pos.node(d).child(index - 1); ; before = before.lastChild!) { if (before.isTextblock) { return false; } if (before.childCount == 0 || before.isAtom || before.type.spec.isolating === true) { return true; } } } // Hit start of document return true; } function cursorBarrierAfter($pos: ResolvedPos) { for (let d = $pos.depth; d >= 0; d--) { const index = $pos.indexAfter(d); const parent = $pos.node(d); if (index == parent.childCount) { if (parent.type.spec.isolating === true) { break; } else { continue; } } for (let after = parent.child(index); ; after = after.firstChild!) { if (after.isTextblock) { return false; } if (after.childCount == 0 || after.isAtom || after.type.spec.isolating === true) { return true; } } } return true; }