Files
rio/frontend/code/components/popup.ts
T
2025-02-16 22:57:41 +01:00

193 lines
6.8 KiB
TypeScript

import { applySwitcheroo } from "../designApplication";
import { ColorSet, ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import {
DesktopDropdownPositioner,
getPositionerByName,
PopupManager,
} from "../popupManager";
import { stopPropagation } from "../eventHandling";
import { componentsById } from "../componentManagement";
export type PopupState = ComponentState & {
_type_: "Popup-builtin";
anchor?: ComponentId;
content?: ComponentId;
is_open?: boolean;
modal: boolean;
user_closable: boolean;
color?: ColorSet | "none";
corner_radius?: number | [number, number, number, number];
position?:
| "auto"
| "left"
| "top"
| "right"
| "bottom"
| "center"
| "fullscreen"
| "dropdown";
alignment?: number;
gap?: number;
};
export class PopupComponent extends ComponentBase {
declare state: Required<PopupState>;
private popupContentElement: HTMLElement;
private popupScrollerElement: HTMLElement;
private popupManager: PopupManager;
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-popup-anchor");
this.popupContentElement = document.createElement("div");
this.popupContentElement.classList.add("rio-popup-content");
this.popupScrollerElement = document.createElement("div");
this.popupScrollerElement.classList.add("rio-popup-scroller");
this.popupContentElement.appendChild(this.popupScrollerElement);
// Instantiate a PopupManager with a dummy anchor for now. To ensure
// correct interaction with alignment and margin, the popup manager must
// use the child's `element` as its anchor, which we can only do later,
// in `updateElement`.
this.popupManager = new PopupManager({
anchor: element,
content: this.popupContentElement,
positioner: getPositionerByName("center", 0, 0.5),
modal: false,
userClosable: false,
onUserClose: this.onUserClose.bind(this),
});
// Prevent clicking through the popup
["click", "pointerdown", "pointerup", "pointermove"].forEach(
(eventType) => {
this.popupContentElement.addEventListener(
eventType,
stopPropagation
);
}
);
return element;
}
updateElement(
deltaState: PopupState,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
// Update the children
if (deltaState.anchor !== undefined) {
this.replaceOnlyChild(
latentComponents,
deltaState.anchor,
this.element
);
// To ensure correct interaction with alignment and margin, the
// popup manager must use the child's `element` as its anchor
let child = componentsById[deltaState.anchor]!;
this.popupManager.anchor = child.element;
}
if (deltaState.content !== undefined) {
this.replaceOnlyChild(
latentComponents,
deltaState.content,
this.popupScrollerElement
);
}
// Update the popup manager
if (
deltaState.position !== undefined ||
deltaState.alignment !== undefined ||
deltaState.gap !== undefined
) {
let position = deltaState.position ?? this.state.position;
this.popupContentElement.dataset.position = position;
// Dropdowns on mobile have the problem that they want to be modal.
// The logic for setting `modal` and `user_closable` becomes a mess.
// So for now, we always use desktop mode for dropdowns.
if (position === "dropdown") {
this.popupManager.positioner = new DesktopDropdownPositioner();
} else {
this.popupManager.positioner = getPositionerByName(
position,
deltaState.gap ?? this.state.gap,
deltaState.alignment ?? this.state.alignment
);
}
this.popupManager.userClosable =
deltaState.user_closable ?? this.state.user_closable;
this.popupManager.modal = deltaState.modal ?? this.state.modal;
}
// Open / Close
if (deltaState.is_open !== undefined) {
// If the Popup was *created* with `is_open=True`, then our element
// isn't attached to the DOM yet and trying to open the popup will
// fail. Lazy workaround: Delay it.
//
// FIXME: This is still a problem if you have a Popup inside of
// another Popup.
requestAnimationFrame(() => {
this.popupManager.isOpen = deltaState.is_open!;
});
}
// Colorize
if (deltaState.color === "none") {
applySwitcheroo(this.popupContentElement, "keep");
this.popupContentElement.style.removeProperty("background-color");
this.popupContentElement.style.removeProperty("box-shadow");
} else if (deltaState.color !== undefined) {
applySwitcheroo(this.popupContentElement, deltaState.color);
this.popupContentElement.style.backgroundColor =
"var(--rio-local-bg)";
this.popupContentElement.style.boxShadow = `0 0 1rem var(--rio-global-shadow-color)`;
}
// Update the corner radius
if (deltaState.corner_radius !== undefined) {
if (typeof deltaState.corner_radius === "number") {
this.popupContentElement.style.borderRadius = `${deltaState.corner_radius}rem`;
} else {
this.popupContentElement.style.borderRadius = `${deltaState.corner_radius[0]}rem ${deltaState.corner_radius[1]}rem ${deltaState.corner_radius[2]}rem ${deltaState.corner_radius[3]}rem`;
}
}
// Modal
if (deltaState.modal !== undefined) {
this.popupManager.modal = deltaState.modal;
}
// User closable
if (deltaState.user_closable !== undefined) {
this.popupManager.userClosable = deltaState.user_closable;
}
}
onDestruction(): void {
super.onDestruction();
this.popupManager.destroy();
}
onUserClose(): void {
// The popup manager was closed by external user action. This doesn't
// need any updates to the popup manager, but Python should be notified
// of the state change.
this.setStateAndNotifyBackend({
is_open: false,
});
}
}