import { ButtonComponent, IconButtonComponent } from "./components/buttons"; import { CalendarComponent } from "./components/calendar"; import { callRemoteMethodDiscardResponse } from "./rpc"; import { CardComponent } from "./components/card"; import { CheckboxComponent } from "./components/checkbox"; import { ClassContainerComponent } from "./components/classContainer"; import { CodeBlockComponent } from "./components/codeBlock"; import { CodeExplorerComponent } from "./components/codeExplorer"; import { ColorPickerComponent } from "./components/colorPicker"; import { ColumnComponent, RowComponent } from "./components/linearContainers"; import { ComponentBase, ComponentState } from "./components/componentBase"; import { ComponentId } from "./dataModels"; import { ComponentPickerComponent } from "./components/componentPicker"; import { ComponentTreeComponent } from "./components/componentTree"; import { CustomListItemComponent } from "./components/customListItem"; import { devToolsConnector } from "./app"; import { DevToolsConnectorComponent } from "./components/devToolsConnector"; import { DialogContainerComponent } from "./components/dialogContainer"; import { DrawerComponent } from "./components/drawer"; import { DropdownComponent } from "./components/dropdown"; import { ErrorPlaceholderComponent } from "./components/errorPlaceholder"; import { FilePickerAreaComponent } from "./components/filePickerArea"; import { FlowComponent as FlowContainerComponent } from "./components/flowContainer"; import { FundamentalRootComponent } from "./components/fundamentalRootComponent"; import { GridComponent } from "./components/grid"; import { HeadingListItemComponent } from "./components/headingListItem"; import { HighLevelComponent as HighLevelComponent } from "./components/highLevelComponent"; import { IconComponent } from "./components/icon"; import { ImageComponent } from "./components/image"; import { KeyEventListenerComponent } from "./components/keyEventListener"; import { LayoutDisplayComponent } from "./components/layoutDisplay"; import { LinkComponent } from "./components/link"; import { ListViewComponent } from "./components/listView"; import { MarkdownComponent } from "./components/markdown"; import { MediaPlayerComponent } from "./components/mediaPlayer"; import { MouseEventListenerComponent } from "./components/mouseEventListener"; import { MultiLineTextInputComponent } from "./components/multiLineTextInput"; import { NodeInputComponent } from "./components/nodeInput"; import { NodeOutputComponent } from "./components/nodeOutput"; import { OverlayComponent } from "./components/overlay"; import { PlotComponent } from "./components/plot"; import { PointerEventListenerComponent } from "./components/pointerEventListener"; import { PopupComponent } from "./components/popup"; import { ProgressBarComponent } from "./components/progressBar"; import { ProgressCircleComponent } from "./components/progressCircle"; import { RectangleComponent } from "./components/rectangle"; import { reprElement, scrollToUrlFragment } from "./utils"; import { RevealerComponent } from "./components/revealer"; import { ScrollContainerComponent } from "./components/scrollContainer"; import { ScrollTargetComponent } from "./components/scrollTarget"; import { SeparatorComponent } from "./components/separator"; import { SeparatorListItemComponent } from "./components/separatorListItem"; import { SliderComponent } from "./components/slider"; import { SlideshowComponent } from "./components/slideshow"; import { StackComponent } from "./components/stack"; import { SwitchComponent } from "./components/switch"; import { SwitcherBarComponent } from "./components/switcherBar"; import { SwitcherComponent } from "./components/switcher"; import { TableComponent } from "./components/table"; import { TextComponent } from "./components/text"; import { TextInputComponent } from "./components/textInput"; import { ThemeContextSwitcherComponent } from "./components/themeContextSwitcher"; import { TooltipComponent } from "./components/tooltip"; import { WebviewComponent } from "./components/webview"; import { GraphEditorComponent } from "./components/graphEditor/graphEditor"; const COMPONENT_CLASSES = { "Button-builtin": ButtonComponent, "Calendar-builtin": CalendarComponent, "Card-builtin": CardComponent, "Checkbox-builtin": CheckboxComponent, "ClassContainer-builtin": ClassContainerComponent, "CodeBlock-builtin": CodeBlockComponent, "CodeExplorer-builtin": CodeExplorerComponent, "ColorPicker-builtin": ColorPickerComponent, "Column-builtin": ColumnComponent, "ComponentPicker-builtin": ComponentPickerComponent, "ComponentTree-builtin": ComponentTreeComponent, "CustomListItem-builtin": CustomListItemComponent, "DevToolsConnector-builtin": DevToolsConnectorComponent, "DialogContainer-builtin": DialogContainerComponent, "Drawer-builtin": DrawerComponent, "Dropdown-builtin": DropdownComponent, "ErrorPlaceholder-builtin": ErrorPlaceholderComponent, "FilePickerArea-builtin": FilePickerAreaComponent, "FlowContainer-builtin": FlowContainerComponent, "FundamentalRootComponent-builtin": FundamentalRootComponent, "GraphEditor-builtin": GraphEditorComponent, "Grid-builtin": GridComponent, "HeadingListItem-builtin": HeadingListItemComponent, "HighLevelComponent-builtin": HighLevelComponent, "Icon-builtin": IconComponent, "IconButton-builtin": IconButtonComponent, "Image-builtin": ImageComponent, "KeyEventListener-builtin": KeyEventListenerComponent, "LayoutDisplay-builtin": LayoutDisplayComponent, "Link-builtin": LinkComponent, "ListView-builtin": ListViewComponent, "Markdown-builtin": MarkdownComponent, "MediaPlayer-builtin": MediaPlayerComponent, "MouseEventListener-builtin": MouseEventListenerComponent, "MultiLineTextInput-builtin": MultiLineTextInputComponent, "NodeInput-builtin": NodeInputComponent, "NodeOutput-builtin": NodeOutputComponent, "Overlay-builtin": OverlayComponent, "Plot-builtin": PlotComponent, "PointerEventListener-builtin": PointerEventListenerComponent, "Popup-builtin": PopupComponent, "ProgressBar-builtin": ProgressBarComponent, "ProgressCircle-builtin": ProgressCircleComponent, "Rectangle-builtin": RectangleComponent, "Revealer-builtin": RevealerComponent, "Row-builtin": RowComponent, "ScrollContainer-builtin": ScrollContainerComponent, "ScrollTarget-builtin": ScrollTargetComponent, "Separator-builtin": SeparatorComponent, "SeparatorListItem-builtin": SeparatorListItemComponent, "Slider-builtin": SliderComponent, "Slideshow-builtin": SlideshowComponent, "Stack-builtin": StackComponent, "Switch-builtin": SwitchComponent, "Switcher-builtin": SwitcherComponent, "SwitcherBar-builtin": SwitcherBarComponent, "Table-builtin": TableComponent, "Text-builtin": TextComponent, "TextInput-builtin": TextInputComponent, "ThemeContextSwitcher-builtin": ThemeContextSwitcherComponent, "Tooltip-builtin": TooltipComponent, "Webview-builtin": WebviewComponent, }; globalThis.COMPONENT_CLASSES = COMPONENT_CLASSES; export const componentsById: { [id: ComponentId]: ComponentBase | undefined } = {}; export const componentsByElement = new Map(); let fundamentalRootComponent: FundamentalRootComponent | null = null; export function getRootComponent(): FundamentalRootComponent { if (fundamentalRootComponent === null) { throw new Error("There is no root component yet"); } return fundamentalRootComponent; } export function getComponentByElement(element: Element): ComponentBase { let instance = tryGetComponentByElement(element); if (instance === null) { // Just displaying the element itself isn't quite enough information for // debugging. We'll go up the tree until we find an element that belongs // to a component, and include that in the error message. let elem: Element | null = element.parentElement; while (elem) { instance = tryGetComponentByElement(elem); if (instance !== null) { throw `Element ${reprElement( element )} does not correspond to a component. It is a child element of ${instance.toString()}`; } elem = elem.parentElement; } throw `Element ${reprElement( element )} does not correspond to a component (and none of its parent elements correspond to a component, either)`; } return instance; } globalThis.componentsById = componentsById; // For debugging globalThis.getInstanceByElement = getComponentByElement; // For debugging export function tryGetComponentByElement( element: Element ): ComponentBase | null { let component = componentsByElement.get(element as HTMLElement); if (component !== undefined) { return component; } // Components may create additional HTML elements for layouting purposes // (alignment, scrolling, ...), so check if this is such an element if (element instanceof HTMLElement) { let ownerId = element.dataset.ownerId; if (ownerId !== undefined) { component = componentsById[ownerId]; if (component !== undefined) { return component; } } } return null; } export function isComponentElement(element: Element): boolean { return componentsByElement.has(element as HTMLElement); } export function getParentComponentElement( element: HTMLElement ): HTMLElement | null { let curElement = element.parentElement; while (curElement !== null) { if (isComponentElement(curElement)) { return curElement; } curElement = curElement.parentElement; } return null; } /// Given a state, return the ids of all its children export function getChildIds(state: ComponentState): ComponentId[] { let result: ComponentId[] = []; let propertyNamesWithChildren = globalThis.CHILD_ATTRIBUTE_NAMES[state["_type_"]!] || []; for (let propertyName of propertyNamesWithChildren) { let propertyValue = state[propertyName]; if (Array.isArray(propertyValue)) { result.push(...propertyValue); } else if (propertyValue !== null && propertyValue !== undefined) { result.push(propertyValue); } } return result; } export function updateComponentStates( deltaStates: { [id: string]: ComponentState }, rootComponentId: ComponentId | null ): void { // Modifying the DOM makes the keyboard focus get lost. Remember which // element had focus so we can restore it later. let focusedElement = document.activeElement; // Find the component that this HTMLElement belongs to while (focusedElement !== null && !isComponentElement(focusedElement)) { focusedElement = focusedElement.parentElement; } let focusedComponent = focusedElement === null ? null : getComponentByElement(focusedElement as HTMLElement); // Create a set to hold all latent components, so they aren't garbage // collected while updating the DOM. let latentComponents = new Set(); // Keep track of all components whose `_grow_` changed, because their // parents have to be notified so they can update their CSS let growChangedComponents: ComponentBase[] = []; // Make sure all components mentioned in the message have a corresponding // HTML element for (let componentIdAsString in deltaStates) { let deltaState = deltaStates[componentIdAsString]; let component = componentsById[componentIdAsString]; // This is a reused component, no need to instantiate a new one if (component) { // Check if its `_grow_` changed if (deltaState._grow_ !== undefined) { if ( deltaState._grow_[0] !== component.state._grow_[0] || deltaState._grow_[1] !== component.state._grow_[1] ) { growChangedComponents.push(component); } } continue; } // Get the class for this component const componentClass = COMPONENT_CLASSES[deltaState._type_!]; // Make sure the component type is valid (Just helpful for debugging) if (!componentClass) { throw `Encountered unknown component type: ${deltaState._type_}`; } // Create an instance for this component let newComponent: ComponentBase = new componentClass( parseInt(componentIdAsString), deltaState ); // Register the component for quick and easy lookup componentsById[componentIdAsString] = newComponent; componentsByElement.set(newComponent.element, newComponent); // Store the component's class name in the element. Used for debugging. newComponent.element.setAttribute( "dbg-py-class", deltaState._python_type_! ); newComponent.element.setAttribute("dbg-id", componentIdAsString); // Set the component's key, if it has one. Used for debugging. let key = deltaState["key"]; if (key !== undefined) { newComponent.element.setAttribute("dbg-key", `${key}`); } } // Some components, like Overlays, need access to the root component. If it // changed, assign it to our global variable. if (rootComponentId !== null) { fundamentalRootComponent = componentsById[ rootComponentId ] as FundamentalRootComponent; } // Update all components mentioned in the message for (let id in deltaStates) { let deltaState = deltaStates[id]; let component: ComponentBase = componentsById[id]!; // Perform updates specific to this component type component.updateElement(deltaState, latentComponents); // Update the component's state component.state = { ...component.state, ...deltaState, }; } // Notify the parents of all elements whose `_grow_` changed to update their // CSS let parents = new Set(); for (let child of growChangedComponents) { parents.add(child.parent!); } for (let parent of parents) { parent.onChildGrowChanged(); } // Restore the keyboard focus if (focusedComponent !== null) { restoreKeyboardFocus(focusedComponent, latentComponents); } // Remove the latent components for (let component of latentComponents) { // Dialog containers aren't part of the component tree, so they falsely // appear as latent. Don't destroy them. if (component instanceof DialogContainerComponent) { continue; } recursivelyDeleteComponent(component); } // If this is the first time, check if there's an #url-fragment and scroll // to it if (rootComponentId !== null) { scrollToUrlFragment("instant"); } // Notify the dev tools, if any if (devToolsConnector !== null) { devToolsConnector.afterComponentStateChange(deltaStates); } } export function recursivelyDeleteComponent(component: ComponentBase): void { let to_do = [component]; for (let comp of to_do) { // Make sure the children will be cleaned up as well to_do.push(...comp.children); // Inform the component of its impending doom comp.onDestruction(); // Remove it from the global lookup tables delete componentsById[comp.id]; componentsByElement.delete(comp.element); } // And finally, remove it from the DOM component.element.remove(); } function canHaveKeyboardFocus(instance: ComponentBase): boolean { // @ts-expect-error return typeof instance.grabKeyboardFocus === "function"; } function restoreKeyboardFocus( focusedComponent: ComponentBase, latentComponents: Set ): void { // The elements that are about to die still know the id of the parent from // which they were just removed. We'll go up the tree until we find a parent // that can accept the keyboard focus. // // Keep in mind that we have to traverse the component tree all the way up // to the root. Because even if a component still has a parent, the parent // itself might be about to die. let rootComponent = getRootComponent(); let current = focusedComponent; let winner: ComponentBase | null = null; while (current !== rootComponent) { // If this component is dead, no child of it can get the keyboard focus if (latentComponents.has(current)) { winner = null; } // If we don't currently know of a focusable (and live) component, check // if this one fits the bill else if (winner === null && canHaveKeyboardFocus(current)) { winner = current; } current = current.parent!; } // We made it to the root. Do we have a winner? if (winner !== null) { // @ts-expect-error winner.grabKeyboardFocus(); } }