import { Chain, Cursors, Guard, NamedChain } from '@ephox/agar'; import { UnitTest } from '@ephox/bedrock-client'; import { Optional, Result } from '@ephox/katamari'; import { Css, DomEvent, Scroll, SelectorFind, type SimRange, SugarElement, SugarNode, type SugarPosition, Traverse, WindowSelection } from '@ephox/sugar'; import { assert } from 'chai'; import * as Boxes from 'ephox/alloy/alien/Boxes'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import { Container } from 'ephox/alloy/api/ui/Container'; import * as ChainUtils from 'ephox/alloy/test/ChainUtils'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; import * as PositionTestUtils from 'ephox/alloy/test/PositionTestUtils'; import { toDomRange } from 'ephox/alloy/test/RangeUtils'; import * as Sinks from 'ephox/alloy/test/Sinks'; import * as Frames from '../../../../demo/ts/ephox/alloy/demo/frames/Frames'; UnitTest.asynctest('SelectionInFramePositionTest', (success, failure) => { const frame = SugarElement.fromTag('iframe'); GuiSetup.setup((_store, _doc, _body) => { let content = ''; for (let i = 0; i < 20; i++) { content += '

paragraph ' + i + '

'; } const onload = DomEvent.bind(frame, 'load', () => { onload.unbind(); Frames.write(frame, '' + content + ''); }); const classicEditor = GuiFactory.build( GuiFactory.external({ uid: 'classic-editor', element: frame }) ); Css.set(classicEditor.element, 'margin-top', '300px'); return GuiFactory.build( Container.sketch({ components: [ GuiFactory.premade(Sinks.fixedSink()), GuiFactory.premade(Sinks.relativeSink()), GuiFactory.premade(Sinks.popup()), GuiFactory.premade(classicEditor) ] }) ); }, (_doc, _body, gui, _component, _store) => { const cSetupAnchor = Chain.mapper((data: any) => ({ type: 'selection', root: SugarElement.fromDom(data.classic.element.dom.contentWindow.document.body) })); const cGetWin = Chain.mapper((frame: any) => frame.element.dom.contentWindow); const cSetPath = (rawPath: { startPath: number[]; soffset: number; finishPath: number[]; foffset: number }) => { const path = Cursors.path(rawPath); return Chain.binder((win: Window) => { const body = SugarElement.fromDom(win.document.body); const range = Cursors.calculate(body, path); WindowSelection.setExact( win, range.start, range.soffset, range.finish, range.foffset ); return WindowSelection.getExact(win).fold(() => Result.error('Could not retrieve the set selection'), Result.value); }); }; const cAssertBelowSelection = (pathLabel: string) => NamedChain.bundle((data: any) => { const frame = data.classic.element; const root = SugarElement.fromDom(frame.dom.contentWindow.document.body); const popup = data.popup; const path = data[pathLabel]; const range = toDomRange(Cursors.calculate(root, path)); const frameRect = frame.dom.getBoundingClientRect(); const selectionBox = range.getBoundingClientRect(); const popupBox = popup.element.dom.getBoundingClientRect(); assert.isAtLeast(popupBox.top, selectionBox.bottom + frameRect.top); assert.approximately(popupBox.top, selectionBox.bottom + frameRect.top, 5); return Result.value(data); }); const cScrollIntoView = (rangeLabel: string, scrollLabel: string, errorMessage: string): NamedChain => { return NamedChain.direct(rangeLabel, Chain.binder((range: SimRange): Result => { const start = range.start; // NOTE: Safari likes to select the text node. const optElement = SugarNode.isText(start) ? Traverse.parentNode(start) : Optional.some(start); return optElement.filter(SugarNode.isHTMLElement).map((elem) => { Scroll.intoView(elem, true); return Scroll.get(Traverse.owner(elem)); }).fold(() => Result.error(errorMessage), Result.value); }), scrollLabel); }; return [ Chain.asStep({}, [ NamedChain.asChain([ ChainUtils.cFindUids(gui, { fixed: 'fixed-sink', relative: 'relative-sink', popup: 'popup', classic: 'classic-editor' }), NamedChain.direct('classic', cGetWin, 'iWin'), // Wait until the content has loaded ChainUtils.cLogging( 'Waiting for iframe to load content.', [ Chain.control( Chain.binder((data: any) => { const root = SugarElement.fromDom(data.classic.element.dom.contentWindow.document.body); return SelectorFind.descendant(root, 'p').fold(() => Result.error('Could not find paragraph yet'), (_p) => Result.value(data)); }), Guard.tryUntil('Waiting for content to load in iframe', 10, 10000) ) ] ), ChainUtils.cLogging( 'Selecting 3rd paragraph', [ NamedChain.direct('iWin', cSetPath({ startPath: [ 2, 0 ], soffset: 0, finishPath: [ 3, 0 ], foffset: 0 }), 'range'), NamedChain.write('anchor', cSetupAnchor) ] ), PositionTestUtils.cTestSink( 'Relative, Selected: 3rd paragraph, no page scroll, no editor scroll', 'relative' ), PositionTestUtils.cTestSinkWithinBounds( 'Relative, Selected: 3rd paragraph, no page scroll, no editor scroll, positioned within frame', 'relative', () => Boxes.box(frame) ), PositionTestUtils.cTestSink( 'Fixed, Selected: 3rd paragraph, no page scroll, no editor scroll', 'fixed' ), PositionTestUtils.cTestSinkWithinBounds( 'Fixed, Selected: 3rd paragraph, no page scroll, no editor scroll, positioned within frame', 'fixed', () => Boxes.box(frame) ), PositionTestUtils.cScrollDown('classic', '2000px'), PositionTestUtils.cTestSink( 'Relative, Selected: 3rd paragraph, 2000px scroll, no editor scroll', 'relative' ), PositionTestUtils.cTestSinkWithinBounds( 'Relative, Selected: 3rd paragraph, 2000px scroll, no editor scroll, positioned within frame', 'relative', () => Boxes.box(frame) ), PositionTestUtils.cTestSink( 'Fixed, Selected: 3rd paragraph, 2000px scroll, no editor scroll', 'fixed' ), PositionTestUtils.cTestSinkWithinBounds( 'Fixed, Selected: 3rd paragraph, 2000px scroll, no editor scroll, positioned within frame', 'fixed', () => Boxes.box(frame) ), ChainUtils.cLogging( 'Selecting 13th paragraph and scrolling to it', [ NamedChain.direct('iWin', cSetPath({ startPath: [ 12 ], soffset: 0, finishPath: [ 13 ], foffset: 0 }), 'range2'), cScrollIntoView('range2', 'scroll2', 'Could not scroll to 13th paragraph'), NamedChain.write('anchor', cSetupAnchor) ] ), PositionTestUtils.cTestSink( 'Relative, Selected: 13rd paragraph, 2000px scroll, no editor scroll', 'relative' ), PositionTestUtils.cTestSink( 'Fixed, Selected: 13rd paragraph, 2000px scroll, no editor scroll', 'fixed' ), ChainUtils.cLogging( 'Setting selection to 5th-7th paragraph and scrolling there', [ NamedChain.writeValue('path3', Cursors.path({ startPath: [ 5 ], soffset: 0, finishPath: [ 7 ], foffset: 1 })), NamedChain.direct('iWin', cSetPath({ startPath: [ 5 ], soffset: 0, finishPath: [ 7 ], foffset: 1 }), 'range3'), cScrollIntoView('range3', 'scroll3', 'Could not scroll to 5th-7th paragraph selection'), NamedChain.write('anchor', cSetupAnchor) ] ), PositionTestUtils.cTestSink( 'Relative, Selected: 5th-7th paragraph, no scroll, some editor scroll', 'relative' ), ChainUtils.cLogging( 'Relative, Selected: 5th-7th paragraph, assert below selection', [ cAssertBelowSelection('path3') ] ), PositionTestUtils.cTestSink( 'Fixed, Selected: 5th-7th paragraph, no scroll, some editor scroll', 'fixed' ), ChainUtils.cLogging( 'Fixed, Selected: 5th-7th paragraph, assert below selection', [ cAssertBelowSelection('path3') ] ) ]) ]) ]; }, success, failure); });