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);
});