import { Adt, Fun, Optional, Thunk } from '@ephox/katamari'; import { SugarElement } from '../../api/node/SugarElement'; import type { SimSelection } from '../../api/selection/SimSelection'; import * as NativeRange from './NativeRange'; type SelectionDirectionHandler = (start: SugarElement, soffset: number, finish: SugarElement, foffset: number) => U; export interface SelectionDirection { readonly fold: ( ltr: SelectionDirectionHandler, rtl: SelectionDirectionHandler ) => U; readonly match: (branches: { ltr: SelectionDirectionHandler; rtl: SelectionDirectionHandler; }) => U; readonly log: (label: string) => void; } type SelectionDirectionConstructor = (start: SugarElement, soffset: number, finish: SugarElement, foffset: number) => SelectionDirection; interface LtrRtlRanges { readonly ltr: () => Range; readonly rtl: () => Optional; } const adt: { readonly ltr: SelectionDirectionConstructor; readonly rtl: SelectionDirectionConstructor; } = Adt.generate([ { ltr: [ 'start', 'soffset', 'finish', 'foffset' ] }, { rtl: [ 'start', 'soffset', 'finish', 'foffset' ] } ]); const fromRange = (win: Window, type: SelectionDirectionConstructor, range: Range): SelectionDirection => type(SugarElement.fromDom(range.startContainer), range.startOffset, SugarElement.fromDom(range.endContainer), range.endOffset); const getRanges = (win: Window, selection: SimSelection): LtrRtlRanges => selection.match({ domRange: (rng) => { return { ltr: Fun.constant(rng), rtl: Optional.none }; }, relative: (startSitu, finishSitu) => { return { ltr: Thunk.cached(() => NativeRange.relativeToNative(win, startSitu, finishSitu)), rtl: Thunk.cached(() => Optional.some(NativeRange.relativeToNative(win, finishSitu, startSitu))) }; }, exact: (start: SugarElement, soffset: number, finish: SugarElement, foffset: number) => { return { ltr: Thunk.cached(() => NativeRange.exactToNative(win, start, soffset, finish, foffset)), rtl: Thunk.cached(() => Optional.some(NativeRange.exactToNative(win, finish, foffset, start, soffset))) }; } }); const doDiagnose = (win: Window, ranges: LtrRtlRanges): SelectionDirection => { // If we cannot create a ranged selection from start > finish, it could be RTL const rng = ranges.ltr(); if (rng.collapsed) { // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed const reversed = ranges.rtl().filter((rev) => rev.collapsed === false); return reversed.map((rev) => // We need to use "reversed" here, because the original only has one point (collapsed) adt.rtl( SugarElement.fromDom(rev.endContainer), rev.endOffset, SugarElement.fromDom(rev.startContainer), rev.startOffset ) ).getOrThunk(() => fromRange(win, adt.ltr, rng)); } else { return fromRange(win, adt.ltr, rng); } }; const diagnose = (win: Window, selection: SimSelection): SelectionDirection => { const ranges = getRanges(win, selection); return doDiagnose(win, ranges); }; const asLtrRange = (win: Window, selection: SimSelection): Range => { const diagnosis = diagnose(win, selection); return diagnosis.match({ ltr: (start, soffset, finish, foffset): Range => { const rng = win.document.createRange(); rng.setStart(start.dom, soffset); rng.setEnd(finish.dom, foffset); return rng; }, rtl: (start, soffset, finish, foffset) => { // NOTE: Reversing start and finish const rng = win.document.createRange(); rng.setStart(finish.dom, foffset); rng.setEnd(start.dom, soffset); return rng; } }); }; const ltr = adt.ltr; const rtl = adt.rtl; export { ltr, rtl, diagnose, asLtrRange };