import { Node, ResolvedPos, Schema } from "prosemirror-model"; import { EditorState, Selection, TextSelection } from "prosemirror-state"; import { MarkTypeName, NodeTypeName } from "./schema"; export function getCursor(state: EditorState): ResolvedPos | null | undefined { const { selection } = state; return selection instanceof TextSelection ? selection.$cursor : null; } export function getCursorIfEnd(state: EditorState): ResolvedPos | null { const $cursor = getCursor(state); if ($cursor != null && $cursor.parentOffset === $cursor.parent.nodeSize - 2) { return $cursor; } return null; } export function getCursorIfStart(state: EditorState): ResolvedPos | null { const $cursor = getCursor(state); if ($cursor != null && $cursor.parentOffset === 0) { return $cursor; } return null; } export function findCutAfter($pos: ResolvedPos): ResolvedPos | null { if ($pos.parent.type.spec.isolating !== true) { for (let i = $pos.depth - 1; i >= 0; i--) { const parent = $pos.node(i); if ($pos.index(i) + 1 < parent.childCount) { return $pos.doc.resolve($pos.after(i + 1)); } if (parent.type.spec.isolating === true) { break; } } } return null; } export function findCutBefore($pos: ResolvedPos) { if ($pos.parent.type.spec.isolating !== true) { for (let i = $pos.depth - 1; i >= 0; i--) { if ($pos.index(i) > 0) { return $pos.doc.resolve($pos.before(i + 1)); } if ($pos.node(i).type.spec.isolating === true) { break; } } } return null; } export function isType(maybeNode: Node | null | undefined, ...types: Array): boolean { return maybeNode != null && types.indexOf(maybeNode.type.name as NodeTypeName | MarkTypeName) > -1; } export function cutMatches( $cut: ResolvedPos, beforeTypes: ReadonlyArray, afterTypes: ReadonlyArray ): boolean { return isType($cut.nodeBefore, ...beforeTypes) && isType($cut.nodeAfter, ...afterTypes); } /** * Type-narrowing helper to use instead of the `x instanceof Selection` pattern. * * `instanceof` cannot be used due to the type parameters in the class. * @param selection */ export function isTextSelection(selection: Selection): selection is TextSelection { return selection instanceof TextSelection; } /** * Smooth Scroll PonyFill adabted from https://github.com/stipsan/smooth-scroll-into-view-if-needed/ */ const now = window !== undefined && "performance" in window ? window.performance.now.bind(window.performance) : Date.now; interface CustomEasing { (t: number): number; } type Context = { method: Function; startTime: number; startX: number; startY: number; x: number; y: number; duration: number; ease: CustomEasing; }; const step = (context: Context) => { const time = now(); const elapsed = Math.min((time - context.startTime) / context.duration, 1); // apply easing to elapsed time const value = context.ease(elapsed); const currentX = context.startX + (context.x - context.startX) * value; const currentY = context.startY + (context.y - context.startY) * value; context.method(currentX, currentY); // scroll more if we have not reached our destination if (currentX !== context.x || currentY !== context.y) { window!.requestAnimationFrame(() => step(context)); } else { // If nothing left to scroll return; } }; export const smoothScroll = (x: number, y: number, duration = 600, ease: CustomEasing = t => 1 + --t * t * t * t * t) => { const startX = window!.scrollX; const startY = window!.scrollY; const method = (x: number, y: number) => { window!.scrollTo(x, y); }; step({ method: method, startTime: now(), startX: startX, startY: startY, x: x, y: y, duration, ease }); };