import { StructureSchema } from '@ephox/boulder'; import { Arr, Fun, Optional, Optionals } from '@ephox/katamari'; import { Css, SugarLocation } from '@ephox/sugar'; import type * as Boxes from '../../alien/Boxes'; import type { AlloyComponent } from '../../api/component/ComponentApi'; import * as AriaFocus from '../../aria/AriaFocus'; import * as Anchor from '../../positioning/layout/Anchor'; import type { PlacerResult } from '../../positioning/layout/LayoutTypes'; import * as Origins from '../../positioning/layout/Origins'; import * as Placement from '../../positioning/layout/Placement'; import * as SimpleLayout from '../../positioning/layout/SimpleLayout'; import type { Anchoring } from '../../positioning/mode/Anchoring'; import type { Transition } from '../../positioning/view/Transitions'; import type { PlacementDetail, PlacementSpec, PositioningConfig, PositioningState } from './PositioningTypes'; import { PlacementSchema } from './PositionSchema'; const getFixedOrigin = (): Origins.OriginAdt => { // Don't use window.innerWidth/innerHeight here, as we don't want to include scrollbars // since the right/bottom position is based on the edge of the scrollbar not the window const html = document.documentElement; return Origins.fixed(0, 0, html.clientWidth, html.clientHeight); }; const getRelativeOrigin = (component: AlloyComponent): Origins.OriginAdt => { const position = SugarLocation.absolute(component.element); const bounds = component.element.dom.getBoundingClientRect(); // We think that this just needs to be kept consistent with Boxes.win. If we remove the scroll values from Boxes.win, we // should change this to just bounds.left and bounds.top from getBoundingClientRect return Origins.relative(position.left, position.top, bounds.width, bounds.height); }; const place = (origin: Origins.OriginAdt, anchoring: Anchoring, optBounds: Optional, placee: AlloyComponent, lastPlace: Optional, transition: Optional): PlacerResult => { const anchor = Anchor.box(anchoring.anchorBox, origin); return SimpleLayout.simple(anchor, placee.element, anchoring.bubble, anchoring.layouts, lastPlace, optBounds, anchoring.overrides, transition); }; const position = (component: AlloyComponent, posConfig: PositioningConfig, posState: PositioningState, placee: AlloyComponent, placementSpec: PlacementSpec): void => { const optWithinBounds = Optional.none(); positionWithinBounds(component, posConfig, posState, placee, placementSpec, optWithinBounds); }; const positionWithinBounds = (component: AlloyComponent, posConfig: PositioningConfig, posState: PositioningState, placee: AlloyComponent, placementSpec: PlacementSpec, optWithinBounds: Optional): void => { const placeeDetail: PlacementDetail = StructureSchema.asRawOrDie('placement.info', StructureSchema.objOf(PlacementSchema), placementSpec); const anchorage = placeeDetail.anchor; const element = placee.element; const placeeState = posState.get(placee.uid); // Preserve the focus as IE 11 loses it when setting visibility to hidden AriaFocus.preserve(() => { // We set it to be fixed, so that it doesn't interfere with the layout of anything // when calculating anchors Css.set(element, 'position', 'fixed'); const oldVisibility = Css.getRaw(element, 'visibility'); Css.set(element, 'visibility', 'hidden'); // We need to calculate the origin (esp. the bounding client rect) *after* we have done // all the preprocessing of the component and placee. Otherwise, the relative positions // (bottom and right) will be using the wrong dimensions const origin = posConfig.useFixed() ? getFixedOrigin() : getRelativeOrigin(component); anchorage.placement(component, anchorage, origin).each((anchoring) => { // If "within bounds" is specified, it overrides any Positioning config. Otherwise, we // use the Positioning config. We don't try to combine automatically here because they are // sometimes serving different purposes. If the Positioning config getBounds needs to be // combined with the optWithinBounds bounds, then it is the responsibility of the calling // code to combine them, and pass in the combined value as optWithinBounds. The optWithinBounds // will *always* override the Positioning config. const optBounds: Optional = optWithinBounds.orThunk( () => posConfig.getBounds.map(Fun.apply) ); // Place the element and then update the state for the placee const newState = place(origin, anchoring, optBounds, placee, placeeState, placeeDetail.transition); posState.set(placee.uid, newState); }); oldVisibility.fold(() => { Css.remove(element, 'visibility'); }, (vis) => { Css.set(element, 'visibility', vis); }); // We need to remove position: fixed put on by above code if it is not needed. if ( Css.getRaw(element, 'left').isNone() && Css.getRaw(element, 'top').isNone() && Css.getRaw(element, 'right').isNone() && Css.getRaw(element, 'bottom').isNone() && Optionals.is(Css.getRaw(element, 'position'), 'fixed') ) { Css.remove(element, 'position'); } }, element); }; const getMode = (component: AlloyComponent, pConfig: PositioningConfig, _pState: PositioningState): string => pConfig.useFixed() ? 'fixed' : 'absolute'; const reset = (component: AlloyComponent, pConfig: PositioningConfig, posState: PositioningState, placee: AlloyComponent): void => { const element = placee.element; Arr.each([ 'position', 'left', 'right', 'top', 'bottom' ], (prop) => Css.remove(element, prop)); Placement.reset(element); posState.clear(placee.uid); }; export { position, positionWithinBounds, getMode, reset };