diff --git a/frontend/code/componentManagement.ts b/frontend/code/componentManagement.ts index 229e618f..62ea076a 100644 --- a/frontend/code/componentManagement.ts +++ b/frontend/code/componentManagement.ts @@ -1,6 +1,5 @@ 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"; @@ -10,14 +9,11 @@ import { ColorPickerComponent } from "./components/colorPicker"; import { ColumnComponent, RowComponent } from "./components/linearContainers"; import { ComponentBase, - ComponentState, - DeltaState, DeltaStateFromBackend, } from "./components/componentBase"; import { ComponentId } from "./dataModels"; import { ComponentPickerComponent } from "./components/componentPicker"; import { ComponentTreeComponent } from "./components/componentTree"; -import { CustomListItemComponent } from "./components/customListItem"; import { CustomTreeItemComponent } from "./components/customTreeItem"; import { devToolsConnector } from "./app"; import { DevToolsConnectorComponent } from "./components/devToolsConnector"; @@ -29,7 +25,11 @@ 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 { + CustomListItemComponent, + HeadingListItemComponent, + SeparatorListItemComponent, +} from "./components/listItems"; import { HighLevelComponent as HighLevelComponent } from "./components/highLevelComponent"; import { IconComponent } from "./components/icon"; import { ImageComponent } from "./components/image"; @@ -56,7 +56,6 @@ 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"; @@ -186,7 +185,7 @@ export function getComponentByElement(element: Element): ComponentBase { } globalThis.componentsById = componentsById; // For debugging -globalThis.getInstanceByElement = getComponentByElement; // For debugging +globalThis.getComponentByElement = getComponentByElement; // For debugging export function tryGetComponentByElement( element: Element @@ -260,18 +259,14 @@ export function updateComponentStates( // 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 + // Find the component that this element belongs to while (focusedElement !== null && !isComponentElement(focusedElement)) { focusedElement = focusedElement.parentElement; } let focusedComponent = - focusedElement === null - ? null - : getComponentByElement(focusedElement as HTMLElement); + focusedElement === null ? null : getComponentByElement(focusedElement); - // Create a set to hold all latent components, so they aren't garbage - // collected while updating the DOM. - let latentComponents = new Set(); + let context = new ComponentStatesUpdateContext(); // Keep track of all components whose `_grow_` changed, because their // parents have to be notified so they can update their CSS @@ -308,7 +303,8 @@ export function updateComponentStates( // Create an instance for this component let newComponent: ComponentBase = new componentClass( parseInt(componentIdAsString), - deltaState + deltaState, + context ); // Register the component for quick and easy lookup @@ -343,7 +339,7 @@ export function updateComponentStates( let component: ComponentBase = componentsById[id]!; // Perform updates specific to this component type - component.updateElement(deltaState, latentComponents); + component.updateElement(deltaState, context); // Update the component's state Object.assign(component.state, deltaState); @@ -359,13 +355,15 @@ export function updateComponentStates( parent.onChildGrowChanged(); } + context.dispatchEvent(new Event("all states updated")); + // Restore the keyboard focus if (focusedComponent !== null) { - restoreKeyboardFocus(focusedComponent, latentComponents); + restoreKeyboardFocus(focusedElement, focusedComponent, context); } // Remove the latent components - for (let component of latentComponents) { + for (let component of context.latentComponents) { // Dialog containers aren't part of the component tree, so they falsely // appear as latent. Don't destroy them. if (component instanceof DialogContainerComponent) { @@ -407,9 +405,18 @@ export function recursivelyDeleteComponent(component: ComponentBase): void { } function restoreKeyboardFocus( + focusedElement: Element, focusedComponent: ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { + // If we can keep the focus in the same element, do that + if (focusedElement instanceof HTMLElement && focusedElement.isConnected) { + if (document.activeElement !== focusedElement) { + focusedElement.focus(); + } + return; + } + // 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. @@ -423,7 +430,7 @@ function restoreKeyboardFocus( while (current !== rootComponent) { // If this component is dead, no child of it can get the keyboard focus - if (latentComponents.has(current)) { + if (context.latentComponents.has(current)) { winner = null; } @@ -444,3 +451,17 @@ function restoreKeyboardFocus( winner.grabKeyboardFocus(); } } + +export class ComponentStatesUpdateContext extends EventTarget { + // A set to hold all latent components, so they aren't garbage collected + // while updating the DOM. + public latentComponents = new Set(); + + public addEventListener( + type: "all states updated", + callback: EventListenerOrEventListenerObject | null, + options?: AddEventListenerOptions | boolean + ): void { + super.addEventListener(type, callback, options); + } +} diff --git a/frontend/code/components/buttons.ts b/frontend/code/components/buttons.ts index cb9b10f1..c746931f 100644 --- a/frontend/code/components/buttons.ts +++ b/frontend/code/components/buttons.ts @@ -4,6 +4,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { RippleEffect } from "../rippleEffect"; import { markEventAsHandled } from "../eventHandling"; import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; type AbstractButtonState = ComponentState & { shape: "pill" | "rounded" | "rectangle" | "circle"; @@ -67,16 +68,12 @@ abstract class AbstractButtonComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the child - this.replaceOnlyChild( - latentComponents, - deltaState.content, - this.childContainer - ); + this.replaceOnlyChild(context, deltaState.content, this.childContainer); // Set the shape if (deltaState.shape !== undefined) { @@ -145,7 +142,7 @@ export type ButtonState = AbstractButtonState & { }; export class ButtonComponent extends AbstractButtonComponent { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { this.buttonElement = this.createButtonElement(); this.buttonElement.role = "button"; return this.buttonElement; @@ -160,7 +157,9 @@ export type IconButtonState = AbstractButtonState & { export class IconButtonComponent extends AbstractButtonComponent { private resizeObserver: ResizeObserver; - protected createElement(): HTMLElement { + protected createElement( + context: ComponentStatesUpdateContext + ): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-icon-button"); element.role = "button"; diff --git a/frontend/code/components/calendar.ts b/frontend/code/components/calendar.ts index e716fc3d..f353322a 100644 --- a/frontend/code/components/calendar.ts +++ b/frontend/code/components/calendar.ts @@ -1,6 +1,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { applyIcon } from "../designApplication"; import { markEventAsHandled } from "../eventHandling"; +import { ComponentStatesUpdateContext } from "../componentManagement"; const CALENDAR_WIDTH = 15.7; const CALENDAR_HEIGHT = 17.8; @@ -33,7 +34,7 @@ export class CalendarComponent extends ComponentBase { private displayedYear: number; private displayedMonth: number; // 1 to 12 - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the HTML structure let element = document.createElement("div"); element.classList.add("rio-calendar"); @@ -104,9 +105,9 @@ export class CalendarComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.is_sensitive !== undefined) { if (deltaState.is_sensitive) { diff --git a/frontend/code/components/card.ts b/frontend/code/components/card.ts index 54667434..bd903a31 100644 --- a/frontend/code/components/card.ts +++ b/frontend/code/components/card.ts @@ -3,6 +3,7 @@ import { ColorSet, ComponentId } from "../dataModels"; import { RippleEffect } from "../rippleEffect"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { markEventAsHandled } from "../eventHandling"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type CardState = ComponentState & { _type_: "Card-builtin"; @@ -21,7 +22,7 @@ export class CardComponent extends ComponentBase { private rippleInstance: RippleEffect | null = null; private rippleCss: { [attr: string]: string } = {}; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the element let element = document.createElement("div"); element.classList.add("rio-card"); @@ -45,12 +46,12 @@ export class CardComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the child - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); // Update the corner radius if (deltaState.corner_radius !== undefined) { diff --git a/frontend/code/components/checkbox.ts b/frontend/code/components/checkbox.ts index cd64b177..7b01e594 100644 --- a/frontend/code/components/checkbox.ts +++ b/frontend/code/components/checkbox.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { applyIcon } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -12,7 +13,7 @@ export class CheckboxComponent extends ComponentBase { private borderElement: HTMLElement; private checkElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-checkbox"); @@ -51,9 +52,9 @@ export class CheckboxComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.is_on !== undefined) { if (deltaState.is_on) { diff --git a/frontend/code/components/classContainer.ts b/frontend/code/components/classContainer.ts index cf0859f7..4c01bad7 100644 --- a/frontend/code/components/classContainer.ts +++ b/frontend/code/components/classContainer.ts @@ -1,5 +1,6 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { ComponentId } from "../dataModels"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type ClassContainerState = ComponentState & { _type_: "ClassContainer-builtin"; @@ -8,17 +9,17 @@ export type ClassContainerState = ComponentState & { }; export class ClassContainerComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { return document.createElement("div"); } updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); if (deltaState.classes !== undefined) { // Remove all old values diff --git a/frontend/code/components/codeBlock.ts b/frontend/code/components/codeBlock.ts index 3854ae7e..61bf2f07 100644 --- a/frontend/code/components/codeBlock.ts +++ b/frontend/code/components/codeBlock.ts @@ -10,6 +10,7 @@ import { Language } from "highlight.js"; import { setClipboard } from "../utils"; import { applyIcon } from "../designApplication"; import { markEventAsHandled } from "../eventHandling"; +import { ComponentStatesUpdateContext } from "../componentManagement"; /// Contains additional aliases for languages that are not recognized by /// highlight.js @@ -134,16 +135,16 @@ export type CodeBlockState = ComponentState & { }; export class CodeBlockComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { const element = document.createElement("div"); return element; } updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Re-create the code block convertDivToCodeBlock( diff --git a/frontend/code/components/codeExplorer.ts b/frontend/code/components/codeExplorer.ts index 9ae9b664..5fb5949e 100644 --- a/frontend/code/components/codeExplorer.ts +++ b/frontend/code/components/codeExplorer.ts @@ -1,5 +1,9 @@ import hljs from "highlight.js/lib/common"; -import { componentsByElement, componentsById } from "../componentManagement"; +import { + componentsByElement, + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { applyIcon } from "../designApplication"; @@ -20,7 +24,7 @@ export class CodeExplorerComponent extends ComponentBase { private sourceHighlighterElement: HTMLElement; private resultHighlighterElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Build the HTML let element = document.createElement("div"); element.classList.add("rio-code-explorer"); @@ -61,9 +65,9 @@ export class CodeExplorerComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the source if (deltaState.source_code !== undefined) { @@ -88,7 +92,7 @@ export class CodeExplorerComponent extends ComponentBase { // Update the child this.replaceOnlyChild( - latentComponents, + context, deltaState.build_result, this.buildResultElement ); diff --git a/frontend/code/components/colorPicker.ts b/frontend/code/components/colorPicker.ts index e6a72104..37da42b9 100644 --- a/frontend/code/components/colorPicker.ts +++ b/frontend/code/components/colorPicker.ts @@ -2,6 +2,7 @@ import { Color } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { hsvToRgb, rgbToHsv, rgbToHex, rgbaToHex } from "../colorConversion"; import { markEventAsHandled } from "../eventHandling"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type ColorPickerState = ComponentState & { _type_: "ColorPicker-builtin"; @@ -25,7 +26,7 @@ export class ColorPickerComponent extends ComponentBase { private isInitialized = false; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the elements let containerElement = document.createElement("div"); containerElement.classList.add("rio-color-picker"); @@ -115,9 +116,9 @@ export class ColorPickerComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Color // diff --git a/frontend/code/components/componentBase.ts b/frontend/code/components/componentBase.ts index d6cf3c08..757c4807 100644 --- a/frontend/code/components/componentBase.ts +++ b/frontend/code/components/componentBase.ts @@ -1,6 +1,7 @@ import { componentsByElement, componentsById, + ComponentStatesUpdateContext, getComponentByElement, } from "../componentManagement"; import { callRemoteMethodDiscardResponse } from "../rpc"; @@ -76,11 +77,15 @@ export abstract class ComponentBase { private centerScrollElement: HTMLElement | null = null; private innerScrollElement: HTMLElement | null = null; - constructor(id: ComponentId, state: S) { + constructor( + id: ComponentId, + state: S, + context: ComponentStatesUpdateContext + ) { this.id = id; this.state = state; - this.element = this.createElement(); + this.element = this.createElement(context); this.element.classList.add("rio-component"); } @@ -115,7 +120,7 @@ export abstract class ComponentBase { /// an argument because it's more efficient than calling `this.element`. updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { if (deltaState._min_size_ !== undefined) { this.element.style.minWidth = `${deltaState._min_size_[0]}rem`; @@ -301,7 +306,7 @@ export abstract class ComponentBase { } } - private unparent(latentComponents: Set): void { + private unparent(context: ComponentStatesUpdateContext): void { // Remove this component from its parent console.assert( this.parent !== null, @@ -309,11 +314,11 @@ export abstract class ComponentBase { ); this.parent!.children.delete(this); - latentComponents.add(this); + context.latentComponents.add(this); } registerChild( - latentComponents: Set, + context: ComponentStatesUpdateContext, child: ComponentBase ): void { // Remove the child from its previous parent @@ -324,14 +329,14 @@ export abstract class ComponentBase { // Add it to this component child.parent = this; this.children.add(child); - latentComponents.delete(child); + context.latentComponents.delete(child); } /// Appends the given child component at the end of the given HTML element. /// Does not remove or modify any existing children. If `childId` is /// `undefined`, does nothing. appendChild( - latentComponents: Set, + context: ComponentStatesUpdateContext, childId: ComponentId | undefined, parentElement: HTMLElement = this.element ): void { @@ -344,14 +349,14 @@ export abstract class ComponentBase { let child = componentsById[childId]!; parentElement.appendChild(child.outerElement); - this.registerChild(latentComponents, child); + this.registerChild(context, child); } /// Replaces the child of the given HTML element with the given child. The /// element must have zero or one children. If `childId` is `null`, removes /// the current child. If `childId` is `undefined`, does nothing. replaceOnlyChild( - latentComponents: Set, + context: ComponentStatesUpdateContext, childId: null | undefined | ComponentId, parentElement: HTMLElement = this.element ): void { @@ -372,7 +377,7 @@ export abstract class ComponentBase { let child = getComponentByElement(currentChildElement); currentChildElement.remove(); - child.unparent(latentComponents); + child.unparent(context); } console.assert( @@ -394,14 +399,14 @@ export abstract class ComponentBase { } currentChildElement.remove(); - child.unparent(latentComponents); + child.unparent(context); } // Add the replacement component let child = componentsById[childId]!; parentElement.appendChild(child.outerElement); - this.registerChild(latentComponents, child); + this.registerChild(context, child); } /// Replaces all children of the given HTML element with the given children. @@ -410,7 +415,7 @@ export abstract class ComponentBase { /// If `wrapInDivs` is true, each child is wrapped in a `
` element. /// This also requires any existing children to be wrapped in `
`s. replaceChildren( - latentComponents: Set, + context: ComponentStatesUpdateContext, childIds: undefined | ComponentId[], parentElement: HTMLElement = this.element, wrapInDivs: boolean = false @@ -452,7 +457,7 @@ export abstract class ComponentBase { let child = children[curIndex]; parentElement.appendChild(wrap(child.outerElement)); - this.registerChild(latentComponents, child); + this.registerChild(context, child); curIndex++; } @@ -468,7 +473,7 @@ export abstract class ComponentBase { let childElement = unwrap(curElement); if (childElement !== null) { let child = getComponentByElement(childElement); - child.unparent(latentComponents); + child.unparent(context); } curElement = childElementIter.next().value; @@ -501,7 +506,7 @@ export abstract class ComponentBase { wrap(expectedChild.outerElement), curElement ); - this.registerChild(latentComponents, expectedChild); + this.registerChild(context, expectedChild); curIndex++; } @@ -514,7 +519,7 @@ export abstract class ComponentBase { /// This is **not recursive**. It only looks through the direct children of /// an element and removes them. removeHtmlOrComponentChildren( - latentComponents: Set, + context: ComponentStatesUpdateContext, parentElement: HTMLElement ) { while (true) { @@ -534,7 +539,7 @@ export abstract class ComponentBase { childElement.remove(); } else { // Yes, take extra special tender loving care - childComponent.unparent(latentComponents); + childComponent.unparent(context); childElement.remove(); } } @@ -542,7 +547,9 @@ export abstract class ComponentBase { /// Creates the HTML element associated with this component. This function does /// not attach the element to the DOM, but merely returns it. - protected abstract createElement(): HTMLElement; + protected abstract createElement( + context: ComponentStatesUpdateContext + ): HTMLElement; /// This method is called when a component is about to be removed from the /// component tree. It can be used for cleaning up event handlers and helper @@ -565,7 +572,10 @@ export abstract class ComponentBase { _setStateDontNotifyBackend(deltaState: DeltaState): void { // Trigger an update - this.updateElement(deltaState, null as any as Set); + this.updateElement( + deltaState, + null as any as ComponentStatesUpdateContext + ); // Set the state Object.assign(this.state, deltaState); diff --git a/frontend/code/components/componentPicker.ts b/frontend/code/components/componentPicker.ts index dcd4d9b0..d91da581 100644 --- a/frontend/code/components/componentPicker.ts +++ b/frontend/code/components/componentPicker.ts @@ -7,7 +7,9 @@ export type ComponentPickerState = ComponentState & { }; export class ComponentPickerComponent extends ComponentBase { - protected createElement(): HTMLElement { + protected createElement( + context: ComponentStatesUpdateContext + ): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-component-picker"); diff --git a/frontend/code/components/componentTree.ts b/frontend/code/components/componentTree.ts index 7b70313e..549fb738 100644 --- a/frontend/code/components/componentTree.ts +++ b/frontend/code/components/componentTree.ts @@ -1,4 +1,7 @@ -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { applyIcon } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { Highlighter } from "../highlighter"; @@ -21,7 +24,7 @@ export class ComponentTreeComponent extends ComponentBase { private nodesByComponent: WeakMap = new WeakMap(); - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Register this component with the global dev tools component, so it // receives updates when a component's state changes. console.assert(devToolsConnector !== null, "devToolsConnector is null"); @@ -53,9 +56,9 @@ export class ComponentTreeComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.component_id !== undefined) { // Highlight the tree item diff --git a/frontend/code/components/customListItem.ts b/frontend/code/components/customListItem.ts deleted file mode 100644 index 5fff8233..00000000 --- a/frontend/code/components/customListItem.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { RippleEffect } from "../rippleEffect"; -import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; -import { ComponentId } from "../dataModels"; - -export type CustomListItemState = ComponentState & { - _type_: "CustomListItem-builtin"; - content: ComponentId; - pressable: boolean; -}; - -export class CustomListItemComponent extends ComponentBase { - // If this item has a ripple effect, this is the ripple instance. `null` - // otherwise. - private rippleInstance: RippleEffect | null = null; - - createElement(): HTMLElement { - let element = document.createElement("div"); - element.classList.add("rio-custom-list-item"); - element.classList.add("rio-selectable-candidate"); - return element; - } - - updateElement( - deltaState: DeltaState, - latentComponents: Set - ): void { - super.updateElement(deltaState, latentComponents); - - // Update the child - this.replaceOnlyChild(latentComponents, deltaState.content); - - // Style the surface depending on whether it is pressable - if (deltaState.pressable === true) { - if (this.rippleInstance === null) { - this.rippleInstance = new RippleEffect(this.element); - - this.element.style.cursor = "pointer"; - this.element.style.setProperty( - "--hover-color", - "var(--rio-local-bg-active)" - ); - - this.element.onclick = this._on_press.bind(this); - } - } else if (deltaState.pressable === false) { - if (this.rippleInstance !== null) { - this.rippleInstance.destroy(); - this.rippleInstance = null; - - this.element.style.removeProperty("cursor"); - this.element.style.setProperty("--hover-color", "transparent"); - - this.element.onclick = null; - } - } - } - - private _on_press(): void { - this.sendMessageToBackend({ - type: "press", - }); - } -} diff --git a/frontend/code/components/customTreeItem.ts b/frontend/code/components/customTreeItem.ts index e572e814..a3327968 100644 --- a/frontend/code/components/customTreeItem.ts +++ b/frontend/code/components/customTreeItem.ts @@ -1,9 +1,11 @@ import { RippleEffect } from "../rippleEffect"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { ComponentId } from "../dataModels"; -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ListViewComponent } from "./listView"; -import { replaceElement } from "../utils"; export type CustomTreeItemState = ComponentState & { _type_: "CustomTreeItem-builtin"; @@ -27,7 +29,7 @@ export class CustomTreeItemComponent extends ComponentBase private childrenContainerElement: HTMLElement; private expandButtonHandler: (event: MouseEvent) => void; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { const element = this._addElement("div", "rio-custom-tree-item", null); const header = this._addElement("div", "rio-tree-header-row", element); this.headerElement = header; @@ -41,7 +43,7 @@ export class CustomTreeItemComponent extends ComponentBase "rio-tree-content-container", header ); - this.contentContainerElement.classList.add("rio-selectable-candidate"); + this.contentContainerElement.classList.add("rio-selectable-item"); this.childrenContainerElement = this._addElement( "div", "rio-tree-children", @@ -67,14 +69,14 @@ export class CustomTreeItemComponent extends ComponentBase updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.content !== undefined) { //update content container this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.contentContainerElement ); @@ -119,7 +121,7 @@ export class CustomTreeItemComponent extends ComponentBase //update children if (deltaState.children !== undefined) { this.replaceChildren( - latentComponents, + context, deltaState.children, this.childrenContainerElement ); @@ -136,8 +138,8 @@ export class CustomTreeItemComponent extends ComponentBase Promise.resolve().then(() => { // a micro-task to make sure children are fully rendered const owningView = this._getOwningView(); - owningView.updateSelectionInteractivity(this.element); - owningView.updateSelectionStyles(this.element); + owningView.updateIsSelectable(this.element); + owningView.updateIsSelected(this.element); }); this.state.children = deltaState.children; } diff --git a/frontend/code/components/devToolsConnector.ts b/frontend/code/components/devToolsConnector.ts index 01643b31..ef77e1a6 100644 --- a/frontend/code/components/devToolsConnector.ts +++ b/frontend/code/components/devToolsConnector.ts @@ -10,7 +10,7 @@ export class DevToolsConnectorComponent extends ComponentBase { + context.addEventListener("all states updated", () => { this.previouslyFocusedElement = document.activeElement; this.popupManager.isOpen = true; }); @@ -105,13 +106,13 @@ export class DialogContainerComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Content this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.contentContainer ); @@ -131,7 +132,7 @@ export class DialogContainerComponent extends ComponentBase { private isFirstUpdate: boolean = true; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the HTML let element = document.createElement("div"); element.classList.add("rio-drawer"); @@ -73,18 +74,14 @@ export class DrawerComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the children + this.replaceOnlyChild(context, deltaState.anchor, this.anchorContainer); this.replaceOnlyChild( - latentComponents, - deltaState.anchor, - this.anchorContainer - ); - this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.contentInnerContainer ); diff --git a/frontend/code/components/dropdown.ts b/frontend/code/components/dropdown.ts index 4467937f..c27fa052 100644 --- a/frontend/code/components/dropdown.ts +++ b/frontend/code/components/dropdown.ts @@ -8,6 +8,7 @@ import { KeyboardFocusableComponentState, } from "./keyboardFocusableComponent"; import { DropdownPositioner } from "../popupPositioners"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type DropdownState = KeyboardFocusableComponentState & { _type_: "Dropdown-builtin"; @@ -33,7 +34,7 @@ export class DropdownComponent extends KeyboardFocusableComponent // The currently highlighted option, if any private highlightedOptionElement: HTMLElement | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create root element let element = document.createElement("div"); element.classList.add("rio-dropdown"); @@ -503,9 +504,9 @@ export class DropdownComponent extends KeyboardFocusableComponent updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // If the options have changed update the options element, and also // store its width diff --git a/frontend/code/components/errorPlaceholder.ts b/frontend/code/components/errorPlaceholder.ts index ce82fe53..16367950 100644 --- a/frontend/code/components/errorPlaceholder.ts +++ b/frontend/code/components/errorPlaceholder.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { applyIcon } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -12,7 +13,7 @@ export class ErrorPlaceholderComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.error_summary !== undefined) { this.summaryElement.innerText = deltaState.error_summary; diff --git a/frontend/code/components/filePickerArea.ts b/frontend/code/components/filePickerArea.ts index adbeec50..d148706a 100644 --- a/frontend/code/components/filePickerArea.ts +++ b/frontend/code/components/filePickerArea.ts @@ -3,6 +3,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { RippleEffect } from "../rippleEffect"; import { markEventAsHandled } from "../eventHandling"; import { ColorSetName, ComponentId } from "../dataModels"; +import { ComponentStatesUpdateContext } from "../componentManagement"; /// Maps MIME types to what sort of file they represent const EXTENSION_TO_CATEGORY = { @@ -145,7 +146,7 @@ export class FilePickerAreaComponent extends ComponentBase private uploadProgresses: Map = new Map(); - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the element let element = document.createElement("div"); element.classList.add("rio-file-picker-area"); @@ -284,9 +285,9 @@ export class FilePickerAreaComponent extends ComponentBase updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // If custom content was provided, use it if ( @@ -294,11 +295,11 @@ export class FilePickerAreaComponent extends ComponentBase deltaState.child_component !== null ) { this.removeHtmlOrComponentChildren( - latentComponents, + context, this.childContentContainer ); this.replaceOnlyChild( - latentComponents, + context, deltaState.child_component, this.childContentContainer ); @@ -310,7 +311,7 @@ export class FilePickerAreaComponent extends ComponentBase deltaState.child_component === null ) { this.removeHtmlOrComponentChildren( - latentComponents, + context, this.childContentContainer ); this.childContentContainer.appendChild( diff --git a/frontend/code/components/flowContainer.ts b/frontend/code/components/flowContainer.ts index d8c0306f..28837e8e 100644 --- a/frontend/code/components/flowContainer.ts +++ b/frontend/code/components/flowContainer.ts @@ -1,4 +1,7 @@ -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -13,7 +16,7 @@ export type FlowState = ComponentState & { export class FlowComponent extends ComponentBase { private innerElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-flow-container"); @@ -26,9 +29,9 @@ export class FlowComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.row_spacing !== undefined) { this.innerElement.style.rowGap = `${deltaState.row_spacing}rem`; @@ -50,7 +53,7 @@ export class FlowComponent extends ComponentBase { if (deltaState.children !== undefined) { this.replaceChildren( - latentComponents, + context, deltaState.children, this.innerElement, true diff --git a/frontend/code/components/fundamentalRootComponent.ts b/frontend/code/components/fundamentalRootComponent.ts index 4ac75f89..3c6c0f14 100644 --- a/frontend/code/components/fundamentalRootComponent.ts +++ b/frontend/code/components/fundamentalRootComponent.ts @@ -1,5 +1,8 @@ import { pixelsPerRem } from "../app"; -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { Debouncer } from "../debouncer"; import { callRemoteMethodDiscardResponse } from "../rpc"; @@ -37,7 +40,7 @@ export class FundamentalRootComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // User components if (deltaState.content !== undefined) { this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.userRootContainer ); @@ -142,7 +145,7 @@ export class FundamentalRootComponent extends ComponentBase { | DraggingNodesStrategy | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the HTML let element = document.createElement("div"); element.classList.add("rio-graph-editor"); @@ -82,9 +83,9 @@ export class GraphEditorComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Spawn some children for testing if (deltaState.children !== undefined) { @@ -98,7 +99,7 @@ export class GraphEditorComponent extends ComponentBase { left: 10 + ii * 10, top: 10 + ii * 10, }; - let augmentedNode = this._makeNode(latentComponents, rawNode); + let augmentedNode = this._makeNode(context, rawNode); this.graphStore.addNode(augmentedNode); } @@ -311,7 +312,7 @@ export class GraphEditorComponent extends ComponentBase { /// Creates a node element and adds it to the HTML child. Returns the node /// state, augmented with the HTML element. _makeNode( - latentComponents: Set, + context: ComponentStatesUpdateContext, nodeState: NodeState ): AugmentedNodeState { // Build the node HTML @@ -336,7 +337,7 @@ export class GraphEditorComponent extends ComponentBase { nodeElement.appendChild(nodeBody); // Content - this.replaceOnlyChild(latentComponents, nodeState.id, nodeBody); + this.replaceOnlyChild(context, nodeState.id, nodeBody); // Build the augmented node state let augmentedNode = { ...nodeState } as AugmentedNodeState; diff --git a/frontend/code/components/grid.ts b/frontend/code/components/grid.ts index 192e24d2..0da4476b 100644 --- a/frontend/code/components/grid.ts +++ b/frontend/code/components/grid.ts @@ -1,4 +1,7 @@ -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { range, zip } from "../utils"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -19,7 +22,7 @@ export type GridState = ComponentState & { }; export class GridComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-grid"); return element; @@ -27,16 +30,16 @@ export class GridComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState._children !== undefined) { let childPositions = deltaState._child_positions ?? this.state._child_positions; this.replaceChildren( - latentComponents, + context, deltaState._children, this.element, true diff --git a/frontend/code/components/headingListItem.ts b/frontend/code/components/headingListItem.ts deleted file mode 100644 index 07e5b26f..00000000 --- a/frontend/code/components/headingListItem.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; -import { applyTextStyleCss, textStyleToCss } from "../cssUtils"; - -export type HeadingListItemState = ComponentState & { - _type_: "HeadingListItem-builtin"; - text: string; -}; - -export class HeadingListItemComponent extends ComponentBase { - createElement(): HTMLElement { - // Create the element - let element = document.createElement("div"); - element.classList.add("rio-heading-list-item"); - - // Apply a style. This could be done with CSS, instead of doing it - // individually for each component, but these are rare and this preempts - // duplicate code. - applyTextStyleCss(element, textStyleToCss("heading3")); - - return element; - } - - updateElement( - deltaState: DeltaState, - latentComponents: Set - ): void { - super.updateElement(deltaState, latentComponents); - - if (deltaState.text !== undefined) { - this.element.textContent = deltaState.text; - } - } -} diff --git a/frontend/code/components/highLevelComponent.ts b/frontend/code/components/highLevelComponent.ts index 4c2f6d04..e1700cea 100644 --- a/frontend/code/components/highLevelComponent.ts +++ b/frontend/code/components/highLevelComponent.ts @@ -1,5 +1,6 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { ComponentId } from "../dataModels"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type HighLevelComponentState = ComponentState & { _type_: "HighLevelComponent-builtin"; @@ -7,7 +8,7 @@ export type HighLevelComponentState = ComponentState & { }; export class HighLevelComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-high-level-component"); return element; @@ -15,10 +16,10 @@ export class HighLevelComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild(latentComponents, deltaState._child_); + this.replaceOnlyChild(context, deltaState._child_); } } diff --git a/frontend/code/components/icon.ts b/frontend/code/components/icon.ts index 66ca6549..e5c61c1b 100644 --- a/frontend/code/components/icon.ts +++ b/frontend/code/components/icon.ts @@ -8,6 +8,7 @@ import { } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { applyIcon, applyFillToSVG } from "../designApplication"; +import { ComponentStatesUpdateContext } from "../componentManagement"; type IconCompatibleFill = | SolidFill @@ -25,7 +26,7 @@ export type IconState = ComponentState & { }; export class IconComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-icon"); return element; @@ -33,9 +34,9 @@ export class IconComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.icon !== undefined) { // Loading the icon may take a while and applying the fill actually diff --git a/frontend/code/components/image.ts b/frontend/code/components/image.ts index 535acb0f..a28a758b 100644 --- a/frontend/code/components/image.ts +++ b/frontend/code/components/image.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { applyIcon } from "../designApplication"; import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -22,7 +23,7 @@ export class ImageComponent extends ComponentBase { private isLoading: boolean = false; private resizeObserver: ResizeObserver; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-image"); @@ -48,9 +49,9 @@ export class ImageComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if ( deltaState.imageUrl !== undefined && diff --git a/frontend/code/components/keyEventListener.ts b/frontend/code/components/keyEventListener.ts index 260b4bf9..12df0c37 100644 --- a/frontend/code/components/keyEventListener.ts +++ b/frontend/code/components/keyEventListener.ts @@ -5,6 +5,7 @@ import { KeyboardFocusableComponent, KeyboardFocusableComponentState, } from "./keyboardFocusableComponent"; +import { ComponentStatesUpdateContext } from "../componentManagement"; // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values const HARDWARE_KEY_MAP = { @@ -703,7 +704,7 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent | true; private keyPressCombinations: Set | true; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-key-event-listener"); element.tabIndex = -1; // So that it can receive keyboard events @@ -712,9 +713,9 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // To efficiently find out if a key combination needs to be reported to // the backend, we need to store the key combinations in a hashable @@ -763,7 +764,7 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent { onChangeLimiter: Debouncer; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Initialize the HTML let element = document.createElement("div"); element.classList.add("rio-layout-display"); @@ -98,9 +101,9 @@ export class LayoutDisplayComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Has the target component changed? if (deltaState.component_id !== undefined) { diff --git a/frontend/code/components/linearContainers.ts b/frontend/code/components/linearContainers.ts index 3e2c2c40..5fb828f6 100644 --- a/frontend/code/components/linearContainers.ts +++ b/frontend/code/components/linearContainers.ts @@ -1,5 +1,8 @@ import { pixelsPerRem } from "../app"; -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { getAllocatedHeightInPx, @@ -57,7 +60,7 @@ export abstract class LinearContainer extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Children if (deltaState.children !== undefined) { this.replaceChildren( - latentComponents, + context, deltaState.children, this.childContainer, true @@ -312,8 +315,8 @@ export class RowComponent extends LinearContainer { index = 0 as 0; sizeAttribute = "width" as "width"; - createElement(): HTMLElement { - let element = super.createElement(); + createElement(context: ComponentStatesUpdateContext): HTMLElement { + let element = super.createElement(context); element.classList.add("rio-row"); return element; } @@ -323,8 +326,8 @@ export class ColumnComponent extends LinearContainer { index = 1 as 1; sizeAttribute = "height" as "height"; - createElement(): HTMLElement { - let element = super.createElement(); + createElement(context: ComponentStatesUpdateContext): HTMLElement { + let element = super.createElement(context); element.classList.add("rio-column"); return element; } diff --git a/frontend/code/components/link.ts b/frontend/code/components/link.ts index f21a7c74..1e96427a 100644 --- a/frontend/code/components/link.ts +++ b/frontend/code/components/link.ts @@ -2,6 +2,7 @@ import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { hijackLinkElement } from "../utils"; import { applyIcon } from "../designApplication"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type LinkState = ComponentState & { _type_: "Link-builtin"; @@ -14,7 +15,7 @@ export type LinkState = ComponentState & { }; export class LinkComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("a"); element.classList.add("rio-link"); @@ -25,9 +26,9 @@ export class LinkComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); let element = this.element as HTMLAnchorElement; @@ -38,7 +39,7 @@ export class LinkComponent extends ComponentBase { (this.state.child_text !== null && deltaState.icon !== undefined) ) { // Clear any existing children - this.removeHtmlOrComponentChildren(latentComponents, this.element); + this.removeHtmlOrComponentChildren(context, this.element); // Add the icon, if any let icon = deltaState.icon ?? this.state.icon; @@ -69,10 +70,10 @@ export class LinkComponent extends ComponentBase { deltaState.child_component !== null ) { // Clear any existing children - this.removeHtmlOrComponentChildren(latentComponents, this.element); + this.removeHtmlOrComponentChildren(context, this.element); // Add the new component - this.replaceOnlyChild(latentComponents, deltaState.child_component); + this.replaceOnlyChild(context, deltaState.child_component); // Update the CSS classes element.classList.remove("rio-text-link"); diff --git a/frontend/code/components/listItems.ts b/frontend/code/components/listItems.ts new file mode 100644 index 00000000..50123f72 --- /dev/null +++ b/frontend/code/components/listItems.ts @@ -0,0 +1,181 @@ +import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; +import { applyTextStyleCss, textStyleToCss } from "../cssUtils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; +import { ComponentId } from "../dataModels"; +import { ListViewComponent } from "./listView"; +import { RippleEffect } from "../rippleEffect"; +import { PressableElement } from "../elements/pressableElement"; + +/// ListItems must keep track which ListView they belong to, in order to manage +/// the selection. Depending on whether selection is enabled ListItems must +/// behave differently (e.g. change color on hover), and destroyed ListItems +/// must be removed from the selection. +/// +/// Subclasses must include a `PressableElement` somewhere in the DOM and assign +/// it to `this.pressableElement`. Pressing this element will add/remove this +/// ListItem from the selection. +export abstract class SelectableListItemComponent< + S extends ComponentState, +> extends ComponentBase { + protected pressableElement: PressableElement; + protected listView: ListViewComponent | null = null; + + constructor( + id: ComponentId, + state: S, + context: ComponentStatesUpdateContext + ) { + super(id, state, context); + + // Find the ListView we belong to + context.addEventListener("all states updated", () => { + let parent = this.parent; + + while (parent !== null) { + if (parent instanceof ListViewComponent) { + this.listView = parent; + parent.registerItem(this); + break; + } + parent = parent.parent; + } + }); + } + + onDestruction(): void { + super.onDestruction(); + + if (this.listView !== null) { + this.listView.unregisterItem(this); + } + } + + set isSelectable(isSelectable: boolean) { + if (isSelectable) { + this.element.classList.add("rio-selectable-item"); + + this.pressableElement.onPress = ( + event: PointerEvent | KeyboardEvent + ) => { + if (this.listView !== null) { + this.listView.onItemPress(this, event); + } + }; + } else { + this.element.classList.remove("rio-selectable-item"); + this.pressableElement.onPress = null; + } + } + + set isSelected(isSelected: boolean) { + this.element.classList.toggle("selected", isSelected); + } +} + +// === HEADING LIST ITEM ======================================================= +export type HeadingListItemState = ComponentState & { + _type_: "HeadingListItem-builtin"; + text: string; +}; + +export class HeadingListItemComponent extends ComponentBase { + createElement(context: ComponentStatesUpdateContext): HTMLElement { + // Create the element + let element = document.createElement("div"); + element.classList.add("rio-heading-list-item"); + + // Apply a style. This could be done with CSS, instead of doing it + // individually for each component, but these are rare and this preempts + // duplicate code. + applyTextStyleCss(element, textStyleToCss("heading3")); + + return element; + } + + updateElement( + deltaState: DeltaState, + context: ComponentStatesUpdateContext + ): void { + super.updateElement(deltaState, context); + + if (deltaState.text !== undefined) { + this.element.textContent = deltaState.text; + } + } +} + +// === SEPARATOR LIST ITEM ===================================================== +export type SeparatorListItemState = ComponentState & { + _type_: "SeparatorListItem-builtin"; +}; + +export class SeparatorListItemComponent extends ComponentBase { + createElement(context: ComponentStatesUpdateContext): HTMLElement { + let element = document.createElement("div"); + element.classList.add("rio-separator-list-item"); + return element; + } +} + +// === CUSTOM LIST ITEM ======================================================== +export type CustomListItemState = ComponentState & { + _type_: "CustomListItem-builtin"; + content: ComponentId; + pressable: boolean; +}; + +export class CustomListItemComponent extends SelectableListItemComponent { + // If this item has a ripple effect, this is the ripple instance. `null` + // otherwise. + private rippleInstance: RippleEffect | null = null; + + createElement(context: ComponentStatesUpdateContext): HTMLElement { + let element = new PressableElement(); + element.classList.add("rio-custom-list-item"); + + this.pressableElement = element; + + return element; + } + + updateElement( + deltaState: DeltaState, + context: ComponentStatesUpdateContext + ): void { + super.updateElement(deltaState, context); + + // Update the child + this.replaceOnlyChild(context, deltaState.content); + + // Style the surface depending on whether it is pressable + if (deltaState.pressable === true) { + if (this.rippleInstance === null) { + this.rippleInstance = new RippleEffect(this.element); + + this.element.style.cursor = "pointer"; + this.element.style.setProperty( + "--hover-color", + "var(--rio-local-bg-active)" + ); + + this.element.onclick = this.onPress.bind(this); + } + } else if (deltaState.pressable === false) { + if (this.rippleInstance !== null) { + this.rippleInstance.destroy(); + this.rippleInstance = null; + + this.element.style.removeProperty("cursor"); + this.element.style.setProperty("--hover-color", "transparent"); + + this.element.onclick = null; + } + } + } + + private onPress(): void { + this.sendMessageToBackend({ + type: "press", + }); + } +} diff --git a/frontend/code/components/listView.ts b/frontend/code/components/listView.ts index 71063adc..9a5d7f5c 100644 --- a/frontend/code/components/listView.ts +++ b/frontend/code/components/listView.ts @@ -1,4 +1,8 @@ -import { componentsByElement, componentsById } from "../componentManagement"; +import { + componentsByElement, + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { ComponentBase, @@ -6,9 +10,12 @@ import { DeltaState, Key, } from "./componentBase"; -import { CustomListItemComponent } from "./customListItem"; -import { HeadingListItemComponent } from "./headingListItem"; -import { SeparatorListItemComponent } from "./separatorListItem"; +import { + SelectableListItemComponent, + CustomListItemComponent, + HeadingListItemComponent, + SeparatorListItemComponent, +} from "./listItems"; export type ListViewState = ComponentState & { _type_: "ListView-builtin"; @@ -18,31 +25,25 @@ export type ListViewState = ComponentState & { }; export class ListViewComponent extends ComponentBase { - private clickHandlers: Map< - Key, - [(event: MouseEvent) => void, ComponentId] - > = new Map(); - private selectionKeysByOwner: Map> = new Map(); + private items = new Set>(); - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { const element = document.createElement("div"); element.classList.add("rio-list-view"); - element.classList.add("rio-selection-owner"); return element; } updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); + + let needSelectabilityUpdate = false; - // Columns don't wrap their children in divs, but ListView does. Hence - // the overridden updateElement. - let needSelectionUpdate = false; if (deltaState.children !== undefined) { this.replaceChildren( - latentComponents, + context, deltaState.children, this.element, true @@ -51,33 +52,52 @@ export class ListViewComponent extends ComponentBase { // Update the styles of the children this.state.children = deltaState.children; this.onChildGrowChanged(); - needSelectionUpdate = true; + + this.updateIsSelected(); + needSelectabilityUpdate = true; } if (deltaState.selection_mode !== undefined) { - this.state.selection_mode = deltaState.selection_mode; - this.updateSelectionInteractivity(); this.element.classList.toggle( - "selectable", - this.state.selection_mode !== "none" + "can-have-selection", + deltaState.selection_mode !== "none" ); - } - if (deltaState.selected_items !== undefined) { - this.state.selected_items = deltaState.selected_items; - this.updateSelectionStyles(); + + this.state.selection_mode = deltaState.selection_mode; + needSelectabilityUpdate = true; } - if (needSelectionUpdate) { - Promise.resolve().then(() => { - // a micro-task to make sure children are fully rendered - this.updateSelectionInteractivity(); - this.updateSelectionStyles(); - }); + if (deltaState.selected_items !== undefined) { + this.state.selected_items = deltaState.selected_items; + this.updateIsSelected(); + } + + if (needSelectabilityUpdate) { + this.updateIsSelectable(); } } + + registerItem(item: SelectableListItemComponent): void { + this.items.add(item); + this.updateItemIsSelected(item); + this.updateItemIsSelectable(item); + } + + unregisterItem(item: SelectableListItemComponent): void { + this.items.delete(item); + + let index = this.state.selected_items.findIndex( + (key) => key === item.state.key + ); + if (index !== -1) { + this.state.selected_items.splice(index, 1); + this.updateSelection(this.state.selected_items); + } + } + onChildGrowChanged(): void { - this._updateChildStyles(); - this.updateSelectionStyles(); + this.updateChildStyles(); + this.updateIsSelected(); let hasGrowers = false; for (let [index, childId] of this.state.children.entries()) { @@ -136,9 +156,10 @@ export class ListViewComponent extends ComponentBase { return this._isGroupedListItemWorker(comp); } - _updateChildStyles(): void { + updateChildStyles(): void { // Precompute which children are grouped let groupedChildren = new Set(); + for (let child of this.element.children) { let castChild = child as HTMLElement; @@ -190,144 +211,74 @@ export class ListViewComponent extends ComponentBase { } } - /// Returns iterator over all child elements that have a key, along with the key - private *_childrenWithKeys( - element: Element | null = null - ): IterableIterator<[HTMLElement, Key]> { - const seenKeys = new Set(); - element = element ?? this.element; - - for (let child of element.querySelectorAll( - ".rio-selectable-candidate" - )) { - let itemKey = keyForSelectable(child); - if (itemKey !== null && !seenKeys.has(itemKey)) { - seenKeys.add(itemKey); - yield [child as HTMLElement, itemKey]; - } + updateIsSelectable(): void { + for (let item of this.items) { + this.updateItemIsSelectable(item); } } - updateSelectionInteractivity(element: Element | null = null): void { - element = element ?? this.element; - for (let child of element.querySelectorAll(".rio-selection-owner")) { - this.updateSelectionInteractivity(child); - } - const component = componentsByElement.get(element as HTMLElement); - if (component !== undefined) { - this._updateSelectionInteractivity(component); + updateItemIsSelectable( + item: SelectableListItemComponent + ): void { + if ( + item instanceof CustomListItemComponent && + item.state.key !== null + ) { + item.isSelectable = this.state.selection_mode !== "none"; } } - _updateSelectionInteractivity(component: ComponentBase): void { - const newOwnedItems = new Set<[Element, Key]>(); - const componentId = component.id; - const oldOwnedKeys = - this.selectionKeysByOwner.get(componentId) ?? new Set(); - if (!this.selectionKeysByOwner.has(componentId)) { - this.selectionKeysByOwner.set(componentId, oldOwnedKeys); - } - for (let [item, itemKey] of this._childrenWithKeys(component.element)) { - // Claims new items by defaulting owner to componentId if not in clickHandlers - const [oldHandler, ownerComponentId] = this.clickHandlers.get( - itemKey - ) ?? [null, componentId]; - const ownerComponentExists = - componentsById[ownerComponentId] !== undefined; - if (!ownerComponentExists) { - const ownedKeys = - this.selectionKeysByOwner.get(ownerComponentId); - for (const key of ownedKeys) { - oldOwnedKeys.add(key); - } - ownedKeys.clear(); - this.selectionKeysByOwner.delete(ownerComponentId); - } - if (ownerComponentId === componentId || !ownerComponentExists) { - if (oldHandler) { - item.classList.remove("rio-selectable-item"); - item.removeEventListener("click", oldHandler); - } - newOwnedItems.add([item, itemKey]); - } - } - if (this.clickHandlers.size > 0) { - for (const key of oldOwnedKeys) { - this.clickHandlers.delete(key); - } - oldOwnedKeys.clear(); + onItemPress( + item: SelectableListItemComponent, + event: PointerEvent | KeyboardEvent + ): void { + if (this.state.selection_mode === "none") { + return; } - if (this.state.selection_mode !== "none") { - for (let [item, itemKey] of newOwnedItems) { - const handler = (event: MouseEvent) => - this._handleItemClick(event, item, itemKey); - item.addEventListener("click", handler); - item.classList.add("rio-selectable-item"); - this.clickHandlers.set(itemKey, [handler, componentId]); - oldOwnedKeys.add(itemKey); + if (this.state.selection_mode === "single" || !event.ctrlKey) { + for (let otherItem of this.items) { + otherItem.isSelected = false; } - } - } - _handleItemClick(event: MouseEvent, item: Element, itemKey: Key): void { - if (this.state.selection_mode === "none") return; - - const currentSelection = [...this.state.selected_items]; - const isSelected = currentSelection.includes(itemKey); - const ctrlKey = event.ctrlKey || event.metaKey; - - if (this.state.selection_mode === "single" || !ctrlKey) { - this.state.selected_items = isSelected ? [] : [itemKey]; - this.updateSelectionStyles(); - } else if (this.state.selection_mode === "multiple") { - if (isSelected) { - this.state.selected_items = currentSelection.filter( - (key) => key !== itemKey - ); + if (this.state.selected_items.includes(item.state.key)) { + this.state.selected_items = []; + item.isSelected = false; } else { - this.state.selected_items = [...currentSelection, itemKey]; + this.state.selected_items = [item.state.key]; + item.isSelected = true; + } + } else { + if (this.state.selected_items.includes(item.state.key)) { + this.state.selected_items = this.state.selected_items.filter( + (key) => key !== item.state.key + ); + item.isSelected = false; + } else { + this.state.selected_items.push(item.state.key); + item.isSelected = true; } - this._updateSelectionStyle(item, itemKey); } - this._notifySelectionChange(); + this.updateSelection(this.state.selected_items); } - _updateSelectionStyle(item: Element, itemKey: Key) { - item.classList.toggle( - "selected", - this.state.selected_items.includes(itemKey) - ); - } + updateSelection(selectedItems: Key[]): void { + this.state.selected_items = selectedItems; - updateSelectionStyles(element: Element | null = null): void { - for (let [item, itemKey] of this._childrenWithKeys(element)) { - this._updateSelectionStyle(item, itemKey); - } - } - - _notifySelectionChange(): void { - // Send selection change to the backend this.sendMessageToBackend({ type: "selectionChange", - selected_items: this.state.selected_items, + selected_items: selectedItems, }); } -} -function keyForSelectable(item: Element): Key | null { - let currentElement: Element | null = item; - while (currentElement !== null) { - const component = componentsByElement.get( - currentElement as HTMLElement - ); - const key = component?.state.key ?? null; - if (key !== null && key !== "") { - return key; + updateIsSelected(): void { + for (let item of this.items) { + this.updateItemIsSelected(item); } - currentElement = currentElement.parentElement; } - console.warn("keyForSelectable: No key found in hierarchy for item", item); - return null; + + updateItemIsSelected(item: SelectableListItemComponent) { + item.isSelected = this.state.selected_items.includes(item.state.key); + } } diff --git a/frontend/code/components/markdown.ts b/frontend/code/components/markdown.ts index 4419a289..0e55368f 100644 --- a/frontend/code/components/markdown.ts +++ b/frontend/code/components/markdown.ts @@ -9,6 +9,7 @@ import hljs from "highlight.js/lib/common"; import { firstDefined, hijackLinkElement } from "../utils"; import { convertDivToCodeBlock } from "./codeBlock"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type MarkdownState = ComponentState & { _type_: "Markdown-builtin"; @@ -121,7 +122,7 @@ function hijackLocalLinks(div: HTMLElement): void { } export class MarkdownComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { const element = document.createElement("div"); element.classList.add("rio-markdown"); return element; @@ -129,9 +130,9 @@ export class MarkdownComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.text !== undefined) { let defaultLanguage = firstDefined( diff --git a/frontend/code/components/mediaPlayer.ts b/frontend/code/components/mediaPlayer.ts index 2f1db603..86405e2b 100644 --- a/frontend/code/components/mediaPlayer.ts +++ b/frontend/code/components/mediaPlayer.ts @@ -8,6 +8,7 @@ import { KeyboardFocusableComponent, KeyboardFocusableComponentState, } from "./keyboardFocusableComponent"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type MediaPlayerState = KeyboardFocusableComponentState & { _type_: "MediaPlayer-builtin"; @@ -289,7 +290,7 @@ export class MediaPlayerComponent extends KeyboardFocusableComponent, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.mediaUrl !== undefined) { let mediaUrl = new URL(deltaState.mediaUrl, document.location.href) diff --git a/frontend/code/components/mouseEventListener.ts b/frontend/code/components/mouseEventListener.ts index b01092e7..a54daa1d 100644 --- a/frontend/code/components/mouseEventListener.ts +++ b/frontend/code/components/mouseEventListener.ts @@ -3,6 +3,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { DragHandler } from "../eventHandling"; import { ComponentId } from "../dataModels"; import { findComponentUnderMouse } from "../utils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; function eventMouseButtonToString(event: MouseEvent): object { return { @@ -34,7 +35,7 @@ export type MouseEventListenerState = ComponentState & { export class MouseEventListenerComponent extends ComponentBase { private _dragHandler: DragHandler | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-pointer-event-listener"); return element; @@ -42,11 +43,11 @@ export class MouseEventListenerComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); if (deltaState.reportPress) { this.element.onclick = (e) => { diff --git a/frontend/code/components/multiLineTextInput.ts b/frontend/code/components/multiLineTextInput.ts index 356243e6..244f8b0d 100644 --- a/frontend/code/components/multiLineTextInput.ts +++ b/frontend/code/components/multiLineTextInput.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { Debouncer } from "../debouncer"; import { markEventAsHandled, stopPropagation } from "../eventHandling"; import { InputBox, InputBoxStyle } from "../inputBox"; @@ -23,7 +24,7 @@ export class MultiLineTextInputComponent extends KeyboardFocusableComponent, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.text !== undefined) { this.inputBox.value = deltaState.text; diff --git a/frontend/code/components/nodeInput.ts b/frontend/code/components/nodeInput.ts index d646d412..b9f3f630 100644 --- a/frontend/code/components/nodeInput.ts +++ b/frontend/code/components/nodeInput.ts @@ -1,6 +1,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { Color } from "../dataModels"; import { colorToCssString } from "../cssUtils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type NodeInputState = ComponentState & { _type_: "NodeInput-builtin"; @@ -13,7 +14,7 @@ export class NodeInputComponent extends ComponentBase { textElement: HTMLElement; circleElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add( "rio-graph-editor-port", @@ -38,9 +39,9 @@ export class NodeInputComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Name if (deltaState.name !== undefined) { diff --git a/frontend/code/components/nodeOutput.ts b/frontend/code/components/nodeOutput.ts index fad1fa42..c09c3e29 100644 --- a/frontend/code/components/nodeOutput.ts +++ b/frontend/code/components/nodeOutput.ts @@ -1,6 +1,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { Color } from "../dataModels"; import { colorToCssString } from "../cssUtils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type NodeOutputState = ComponentState & { _type_: "NodeOutput-builtin"; @@ -13,7 +14,7 @@ export class NodeOutputComponent extends ComponentBase { textElement: HTMLElement; circleElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add( "rio-graph-editor-port", @@ -38,9 +39,9 @@ export class NodeOutputComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Name if (deltaState.name !== undefined) { diff --git a/frontend/code/components/numberInput.ts b/frontend/code/components/numberInput.ts index d557d494..704c39a5 100644 --- a/frontend/code/components/numberInput.ts +++ b/frontend/code/components/numberInput.ts @@ -7,6 +7,7 @@ import { } from "./keyboardFocusableComponent"; import Mexp from "math-expression-evaluator"; +import { ComponentStatesUpdateContext } from "../componentManagement"; const mathExpressionEvaluator = new Mexp(); @@ -40,7 +41,7 @@ export type NumberInputState = KeyboardFocusableComponentState & { export class NumberInputComponent extends KeyboardFocusableComponent { private inputBox: InputBox; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Note: We don't use `` because of its ugly // up/down buttons this.inputBox = new InputBox(); @@ -101,9 +102,9 @@ export class NumberInputComponent extends KeyboardFocusableComponent, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.label !== undefined) { this.inputBox.label = deltaState.label; diff --git a/frontend/code/components/overlay.ts b/frontend/code/components/overlay.ts index 35d16779..d8057452 100644 --- a/frontend/code/components/overlay.ts +++ b/frontend/code/components/overlay.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { ComponentId } from "../dataModels"; import { PopupManager } from "../popupManager"; import { FullscreenPositioner } from "../popupPositioners"; @@ -12,7 +13,7 @@ export class OverlayComponent extends ComponentBase { private overlayContentElement: HTMLElement; private popupManager: PopupManager; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-overlay"); @@ -29,7 +30,7 @@ export class OverlayComponent extends ComponentBase { moveKeyboardFocusInside: false, }); - requestAnimationFrame(() => { + context.addEventListener("all states updated", () => { this.popupManager.isOpen = true; }); @@ -43,12 +44,12 @@ export class OverlayComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.overlayContentElement ); diff --git a/frontend/code/components/pdf_viewer.ts b/frontend/code/components/pdf_viewer.ts index 5d24ec5e..90039547 100644 --- a/frontend/code/components/pdf_viewer.ts +++ b/frontend/code/components/pdf_viewer.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { applyIcon } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -10,7 +11,7 @@ export class PdfViewerComponent extends ComponentBase { private objectElement: HTMLObjectElement; private fallbackColumn: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-pdf-viewer"); @@ -38,9 +39,9 @@ export class PdfViewerComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if ( deltaState.pdfUrl !== undefined && diff --git a/frontend/code/components/plot.ts b/frontend/code/components/plot.ts index 6a16c23a..cf64da19 100644 --- a/frontend/code/components/plot.ts +++ b/frontend/code/components/plot.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { fillToCss } from "../cssUtils"; import { AnyFill } from "../dataModels"; import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils"; @@ -29,7 +30,7 @@ export class PlotComponent extends ComponentBase { // represented as a single object that we can easily swap out. private plotManager: PlotManager | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-plot"); return element; @@ -37,9 +38,9 @@ export class PlotComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.plot !== undefined) { if (this.plotManager !== null) { diff --git a/frontend/code/components/pointerEventListener.ts b/frontend/code/components/pointerEventListener.ts index 2792f768..3b6a9a25 100644 --- a/frontend/code/components/pointerEventListener.ts +++ b/frontend/code/components/pointerEventListener.ts @@ -2,6 +2,7 @@ import { pixelsPerRem } from "../app"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { DragHandler, markEventAsHandled } from "../eventHandling"; import { ComponentId } from "../dataModels"; +import { ComponentStatesUpdateContext } from "../componentManagement"; type MouseButton = "left" | "middle" | "right"; @@ -28,7 +29,7 @@ export class PointerEventListenerComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); if ( deltaState.reportPress !== undefined || diff --git a/frontend/code/components/popup.ts b/frontend/code/components/popup.ts index e74b6b87..11f40955 100644 --- a/frontend/code/components/popup.ts +++ b/frontend/code/components/popup.ts @@ -3,7 +3,10 @@ import { ColorSet, ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { PopupManager } from "../popupManager"; import { stopPropagation } from "../eventHandling"; -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { DesktopDropdownPositioner, getPositionerByName, @@ -37,7 +40,7 @@ export class PopupComponent extends ComponentBase { private popupManager: PopupManager; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-popup-anchor"); @@ -76,17 +79,13 @@ export class PopupComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the children if (deltaState.anchor !== undefined) { - this.replaceOnlyChild( - latentComponents, - deltaState.anchor, - this.element - ); + this.replaceOnlyChild(context, deltaState.anchor, this.element); // To ensure correct interaction with alignment and margin, the // popup manager must use the child's `element` as its anchor @@ -96,7 +95,7 @@ export class PopupComponent extends ComponentBase { if (deltaState.content !== undefined) { this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.popupScrollerElement ); diff --git a/frontend/code/components/progressBar.ts b/frontend/code/components/progressBar.ts index 031bad90..ed012235 100644 --- a/frontend/code/components/progressBar.ts +++ b/frontend/code/components/progressBar.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { ColorSet } from "../dataModels"; import { applySwitcheroo } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -12,7 +13,7 @@ export type ProgressBarState = ComponentState & { export class ProgressBarComponent extends ComponentBase { fillElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-progress-bar"); @@ -30,9 +31,9 @@ export class ProgressBarComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.progress !== undefined) { // Indeterminate progress diff --git a/frontend/code/components/progressCircle.ts b/frontend/code/components/progressCircle.ts index 7f540435..fe8478b0 100644 --- a/frontend/code/components/progressCircle.ts +++ b/frontend/code/components/progressCircle.ts @@ -1,6 +1,7 @@ import { applySwitcheroo } from "../designApplication"; import { ColorSet } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type ProgressCircleState = ComponentState & { _type_: "ProgressCircle-builtin"; @@ -9,7 +10,7 @@ export type ProgressCircleState = ComponentState & { }; export class ProgressCircleComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.innerHTML = ` @@ -23,9 +24,9 @@ export class ProgressCircleComponent extends ComponentBase updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Apply the progress if (deltaState.progress !== undefined) { diff --git a/frontend/code/components/rectangle.ts b/frontend/code/components/rectangle.ts index f6d7d5c1..c139d160 100644 --- a/frontend/code/components/rectangle.ts +++ b/frontend/code/components/rectangle.ts @@ -2,6 +2,7 @@ import { Color, ComponentId, AnyFill } from "../dataModels"; import { colorToCssString, fillToCss } from "../cssUtils"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { RippleEffect } from "../rippleEffect"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type RectangleState = ComponentState & { _type_: "Rectangle-builtin"; @@ -83,7 +84,7 @@ export class RectangleComponent extends ComponentBase { // `null` otherwise. private rippleInstance: RippleEffect | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-rectangle"); return element; @@ -91,11 +92,11 @@ export class RectangleComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); if (deltaState.transition_time !== undefined) { this.element.style.transitionDuration = `${deltaState.transition_time}s`; diff --git a/frontend/code/components/revealer.ts b/frontend/code/components/revealer.ts index fa85bb59..08201fad 100644 --- a/frontend/code/components/revealer.ts +++ b/frontend/code/components/revealer.ts @@ -8,6 +8,7 @@ import { RioAnimationPlayback, RioKeyframeAnimation, } from "../animations"; +import { ComponentStatesUpdateContext } from "../componentManagement"; let HEADER_PADDING: number = 0.3; @@ -29,7 +30,7 @@ export class RevealerComponent extends ComponentBase { private rippleInstance: RippleEffect; private currentAnimation: RioAnimationPlayback | null = null; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the HTML let element = document.createElement("div"); element.classList.add("rio-revealer"); @@ -97,9 +98,9 @@ export class RevealerComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the header if (deltaState.header === null) { @@ -111,7 +112,7 @@ export class RevealerComponent extends ComponentBase { // Update the child this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.contentInnerElement ); diff --git a/frontend/code/components/scrollContainer.ts b/frontend/code/components/scrollContainer.ts index a36b9e39..a3dd2df9 100644 --- a/frontend/code/components/scrollContainer.ts +++ b/frontend/code/components/scrollContainer.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -17,7 +18,7 @@ export class ScrollContainerComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); - this.replaceOnlyChild( - latentComponents, - deltaState.content, - this.childContainer - ); + this.replaceOnlyChild(context, deltaState.content, this.childContainer); if (deltaState.scroll_x !== undefined) { this.element.dataset.scrollX = deltaState.scroll_x; diff --git a/frontend/code/components/scrollTarget.ts b/frontend/code/components/scrollTarget.ts index f58dd842..52d9c2ee 100644 --- a/frontend/code/components/scrollTarget.ts +++ b/frontend/code/components/scrollTarget.ts @@ -1,4 +1,7 @@ -import { tryGetComponentByElement } from "../componentManagement"; +import { + ComponentStatesUpdateContext, + tryGetComponentByElement, +} from "../componentManagement"; import { ComponentId } from "../dataModels"; import { setClipboard } from "../utils"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -16,7 +19,7 @@ export class ScrollTargetComponent extends ComponentBase { childContainerElement: HTMLElement; buttonContainerElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("a"); element.classList.add("rio-scroll-target"); @@ -41,12 +44,12 @@ export class ScrollTargetComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.childContainerElement ); @@ -59,9 +62,9 @@ export class ScrollTargetComponent extends ComponentBase { deltaState.copy_button_content !== undefined && deltaState.copy_button_content !== null ) { - this._removeButtonChild(latentComponents); + this._removeButtonChild(context); this.replaceOnlyChild( - latentComponents, + context, deltaState.copy_button_content, this.buttonContainerElement ); @@ -69,7 +72,7 @@ export class ScrollTargetComponent extends ComponentBase { deltaState.copy_button_text !== undefined && deltaState.copy_button_text !== null ) { - this._removeButtonChild(latentComponents); + this._removeButtonChild(context); let textElement = document.createElement("span"); textElement.textContent = deltaState.copy_button_text; @@ -77,7 +80,7 @@ export class ScrollTargetComponent extends ComponentBase { } } - private _removeButtonChild(latentComponents: Set): void { + private _removeButtonChild(context: ComponentStatesUpdateContext): void { let buttonChild = this.buttonContainerElement.firstElementChild; if (buttonChild === null) return; @@ -87,7 +90,7 @@ export class ScrollTargetComponent extends ComponentBase { buttonChild.remove(); } else { this.replaceOnlyChild( - latentComponents, + context, childComponent.id, this.buttonContainerElement ); diff --git a/frontend/code/components/separator.ts b/frontend/code/components/separator.ts index ea91eeac..a747cb85 100644 --- a/frontend/code/components/separator.ts +++ b/frontend/code/components/separator.ts @@ -1,6 +1,7 @@ import { Color } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { colorToCssString } from "../cssUtils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type SeparatorState = ComponentState & { _type_: "Separator-builtin"; @@ -9,7 +10,7 @@ export type SeparatorState = ComponentState & { }; export class SeparatorComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-separator"); return element; @@ -17,9 +18,9 @@ export class SeparatorComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Color if (deltaState.color === undefined) { diff --git a/frontend/code/components/separatorListItem.ts b/frontend/code/components/separatorListItem.ts deleted file mode 100644 index 08964d4d..00000000 --- a/frontend/code/components/separatorListItem.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; - -export type SeparatorListItemState = ComponentState & { - _type_: "SeparatorListItem-builtin"; -}; - -export class SeparatorListItemComponent extends ComponentBase { - createElement(): HTMLElement { - let element = document.createElement("div"); - element.classList.add("rio-separator-list-item"); - return element; - } -} diff --git a/frontend/code/components/slider.ts b/frontend/code/components/slider.ts index 2b7e42a0..3759383b 100644 --- a/frontend/code/components/slider.ts +++ b/frontend/code/components/slider.ts @@ -1,4 +1,4 @@ -import { applySwitcheroo } from "../designApplication"; +import { ComponentStatesUpdateContext } from "../componentManagement"; import { markEventAsHandled } from "../eventHandling"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -18,7 +18,7 @@ export class SliderComponent extends ComponentBase { private minValueElement: HTMLElement; private maxValueElement: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the HTML structure let element = document.createElement("div"); element.classList.add("rio-slider"); @@ -138,9 +138,9 @@ export class SliderComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if ( deltaState.minimum !== undefined || diff --git a/frontend/code/components/slideshow.ts b/frontend/code/components/slideshow.ts index a772125b..36bc6253 100644 --- a/frontend/code/components/slideshow.ts +++ b/frontend/code/components/slideshow.ts @@ -1,6 +1,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { easeIn, easeInOut, easeOut } from "../easeFunctions"; import { ComponentId } from "../dataModels"; +import { ComponentStatesUpdateContext } from "../componentManagement"; const switchDuration = 0.8; const progressBarFadeDuration = 0.2; @@ -29,7 +30,7 @@ export class SlideshowComponent extends ComponentBase { private progressBarOpacity: number = 1; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the elements let element = document.createElement("div"); element.classList.add("rio-slideshow"); @@ -69,14 +70,14 @@ export class SlideshowComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the children if (deltaState.children !== undefined) { this.replaceChildren( - latentComponents, + context, deltaState.children, this.childContainer, true diff --git a/frontend/code/components/stack.ts b/frontend/code/components/stack.ts index f5c36762..7c63cf43 100644 --- a/frontend/code/components/stack.ts +++ b/frontend/code/components/stack.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -7,7 +8,7 @@ export type StackState = ComponentState & { }; export class StackComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-stack"); return element; @@ -15,17 +16,12 @@ export class StackComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // For some reason, a CSS `grid` seems to squish children to their minimum size. // Wrapping each child in a container element fixes this, somehow. - this.replaceChildren( - latentComponents, - deltaState.children, - this.element, - true - ); + this.replaceChildren(context, deltaState.children, this.element, true); } } diff --git a/frontend/code/components/switch.ts b/frontend/code/components/switch.ts index 24d10c86..7cb56b7f 100644 --- a/frontend/code/components/switch.ts +++ b/frontend/code/components/switch.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { applyIcon } from "../designApplication"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -8,7 +9,7 @@ export type SwitchState = ComponentState & { }; export class SwitchComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-switch"); @@ -36,9 +37,9 @@ export class SwitchComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.is_on !== undefined) { if (deltaState.is_on) { diff --git a/frontend/code/components/switcher.ts b/frontend/code/components/switcher.ts index 6c729396..bad43ed8 100644 --- a/frontend/code/components/switcher.ts +++ b/frontend/code/components/switcher.ts @@ -1,6 +1,9 @@ import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; -import { componentsById } from "../componentManagement"; +import { + componentsById, + ComponentStatesUpdateContext, +} from "../componentManagement"; import { commitCss } from "../utils"; export type SwitcherState = ComponentState & { @@ -15,7 +18,7 @@ export class SwitcherComponent extends ComponentBase { private idOfCurrentAnimation: number = 0; private isInitialized: boolean = false; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-switcher"); return element; @@ -23,9 +26,9 @@ export class SwitcherComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the transition time first, in case the code below is about // to start an animation. @@ -49,7 +52,7 @@ export class SwitcherComponent extends ComponentBase { this.element.appendChild(this.activeChildContainer); this.replaceOnlyChild( - latentComponents, + context, deltaState.content, this.activeChildContainer ); @@ -57,7 +60,7 @@ export class SwitcherComponent extends ComponentBase { } else if (deltaState.content !== this.state.content) { this.replaceContent( deltaState.content, - latentComponents, + context, deltaState.transition_time ?? this.state.transition_time ); } @@ -68,7 +71,7 @@ export class SwitcherComponent extends ComponentBase { private async replaceContent( content: ComponentId | null, - latentComponents: Set, + context: ComponentStatesUpdateContext, transitionTime: number ): Promise { // Animating the size is trickier than you might expect. Firstly, CSS @@ -104,7 +107,7 @@ export class SwitcherComponent extends ComponentBase { ) as HTMLElement; // Unparent the old component - this.replaceOnlyChild(latentComponents, null, oldChildContainer); + this.replaceOnlyChild(context, null, oldChildContainer); // Fill the childContainer with the cloned element oldChildContainer.appendChild(oldElementClone); @@ -120,7 +123,7 @@ export class SwitcherComponent extends ComponentBase { if (content !== null) { // Add the child into a helper container newChildContainer = document.createElement("div"); - this.replaceOnlyChild(latentComponents, content, newChildContainer); + this.replaceOnlyChild(context, content, newChildContainer); // Find out how large the new child will be. To simulate this, we // must temporarily remove the current child from layouting, so that diff --git a/frontend/code/components/switcherBar.ts b/frontend/code/components/switcherBar.ts index 1ca6a2b8..6d11a1f4 100644 --- a/frontend/code/components/switcherBar.ts +++ b/frontend/code/components/switcherBar.ts @@ -10,6 +10,7 @@ import { getAllocatedHeightInPx, getAllocatedWidthInPx, } from "../utils"; +import { ComponentStatesUpdateContext } from "../componentManagement"; type SwitcherBarItem = { name: string; @@ -55,7 +56,7 @@ export class SwitcherBarComponent extends ComponentBase { // Used to update the marker should the element be resized private resizeObserver: ResizeObserver; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Create the elements let outerElement = document.createElement("div"); outerElement.classList.add("rio-switcher-bar"); @@ -306,9 +307,9 @@ export class SwitcherBarComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Have the options changed? if (deltaState.items !== undefined) { diff --git a/frontend/code/components/table.ts b/frontend/code/components/table.ts index 91c7d495..2a4cfafc 100644 --- a/frontend/code/components/table.ts +++ b/frontend/code/components/table.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { colorToCssString } from "../cssUtils"; import { Color } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; @@ -41,7 +42,7 @@ export class TableComponent extends ComponentBase { // False if the component has never been updated before private isInitialized: boolean = false; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-table"); return element; @@ -70,9 +71,9 @@ export class TableComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // If true, all HTML content of the table will be cleared and replaced let contentNeedsRepopulation = false; diff --git a/frontend/code/components/text.ts b/frontend/code/components/text.ts index d6f52356..1b25df7a 100644 --- a/frontend/code/components/text.ts +++ b/frontend/code/components/text.ts @@ -7,6 +7,7 @@ import { } from "../dataModels"; import { applyTextStyleCss, textfillToCss, textStyleToCss } from "../cssUtils"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type TextState = ComponentState & { _type_: "Text-builtin"; @@ -29,7 +30,7 @@ export type TextState = ComponentState & { export class TextComponent extends ComponentBase { private inner: HTMLElement; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-text"); @@ -41,9 +42,9 @@ export class TextComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // BEFORE WE DO ANYTHING ELSE, replace the inner HTML element if (deltaState.style !== undefined) { diff --git a/frontend/code/components/textInput.ts b/frontend/code/components/textInput.ts index 0674a88e..72eca168 100644 --- a/frontend/code/components/textInput.ts +++ b/frontend/code/components/textInput.ts @@ -6,6 +6,7 @@ import { KeyboardFocusableComponent, KeyboardFocusableComponentState, } from "./keyboardFocusableComponent"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type TextInputState = KeyboardFocusableComponentState & { _type_: "TextInput-builtin"; @@ -25,7 +26,7 @@ export class TextInputComponent extends KeyboardFocusableComponent, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.text !== undefined) { this.inputBox.value = deltaState.text; diff --git a/frontend/code/components/themeContextSwitcher.ts b/frontend/code/components/themeContextSwitcher.ts index a0617117..026429e3 100644 --- a/frontend/code/components/themeContextSwitcher.ts +++ b/frontend/code/components/themeContextSwitcher.ts @@ -1,6 +1,7 @@ import { applySwitcheroo } from "../designApplication"; import { ColorSet, ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type ThemeContextSwitcherState = ComponentState & { _type_: "ThemeContextSwitcher-builtin"; @@ -9,7 +10,7 @@ export type ThemeContextSwitcherState = ComponentState & { }; export class ThemeContextSwitcherComponent extends ComponentBase { - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-single-container"); return element; @@ -17,12 +18,12 @@ export class ThemeContextSwitcherComponent extends ComponentBase, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the child - this.replaceOnlyChild(latentComponents, deltaState.content); + this.replaceOnlyChild(context, deltaState.content); // Colorize if (deltaState.color !== undefined) { diff --git a/frontend/code/components/tooltip.ts b/frontend/code/components/tooltip.ts index 7dff20a7..ff7e3c00 100644 --- a/frontend/code/components/tooltip.ts +++ b/frontend/code/components/tooltip.ts @@ -2,6 +2,7 @@ import { ComponentId } from "../dataModels"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; import { PopupManager } from "../popupManager"; import { getPositionerByName } from "../popupPositioners"; +import { ComponentStatesUpdateContext } from "../componentManagement"; export type TooltipState = ComponentState & { _type_: "Tooltip-builtin"; @@ -15,7 +16,7 @@ export class TooltipComponent extends ComponentBase { private popupElement: HTMLElement; private popupManager: PopupManager; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { // Set up the HTML let element = document.createElement("div"); element.classList.add("rio-tooltip"); @@ -51,23 +52,19 @@ export class TooltipComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); // Update the anchor if (deltaState.anchor !== undefined) { - this.replaceOnlyChild( - latentComponents, - deltaState.anchor, - this.element - ); + this.replaceOnlyChild(context, deltaState.anchor, this.element); } // Update tip if (deltaState._tip_component !== undefined) { this.replaceOnlyChild( - latentComponents, + context, deltaState._tip_component, this.popupElement ); diff --git a/frontend/code/components/webview.ts b/frontend/code/components/webview.ts index 60c6a75d..f1415c3a 100644 --- a/frontend/code/components/webview.ts +++ b/frontend/code/components/webview.ts @@ -1,3 +1,4 @@ +import { ComponentStatesUpdateContext } from "../componentManagement"; import { ComponentBase, ComponentState, DeltaState } from "./componentBase"; export type WebviewState = ComponentState & { @@ -12,7 +13,7 @@ export class WebviewComponent extends ComponentBase { private resizeObserver: ResizeObserver | null = null; private isInitialized = false; - createElement(): HTMLElement { + createElement(context: ComponentStatesUpdateContext): HTMLElement { let element = document.createElement("div"); element.classList.add("rio-webview"); return element; @@ -20,9 +21,9 @@ export class WebviewComponent extends ComponentBase { updateElement( deltaState: DeltaState, - latentComponents: Set + context: ComponentStatesUpdateContext ): void { - super.updateElement(deltaState, latentComponents); + super.updateElement(deltaState, context); if (deltaState.content !== undefined) { // If the URL/HTML hasn't actually changed from last time, don't do diff --git a/frontend/code/elements/pressableElement.ts b/frontend/code/elements/pressableElement.ts new file mode 100644 index 00000000..a0723783 --- /dev/null +++ b/frontend/code/elements/pressableElement.ts @@ -0,0 +1,66 @@ +import { markEventAsHandled } from "../eventHandling"; + +/// An unstyled element that behaves like a button, with all the necessary +/// accessibility features. +/// +/// If `onPress` is null, the element stops being a button. +export class PressableElement extends HTMLElement { + public onPress: ((event: PointerEvent | KeyboardEvent) => void) | null = + null; + + constructor() { + super(); + + let shadowRoot = this.attachShadow({ mode: "closed" }); + + shadowRoot.innerHTML = ` + + + `; + + this.onClick = this.onClick.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + } + + connectedCallback() { + this.setAttribute("role", "button"); + this.setAttribute("tabindex", "0"); // Make it focusable + this.addEventListener("click", this.onClick); + this.addEventListener("keydown", this.onKeyPress); + } + + disconnectedCallback() { + this.removeEventListener("click", this.onClick); + this.removeEventListener("keydown", this.onKeyPress); + } + + private onClick(event: PointerEvent) { + this.emitPressEvent(event); + markEventAsHandled(event); + } + + private onKeyPress(event: KeyboardEvent) { + if (event.key === "Enter" || event.key === " ") { + this.emitPressEvent(event); + markEventAsHandled(event); + } + } + + private emitPressEvent(event: PointerEvent | KeyboardEvent) { + if (this.onPress !== null) { + this.onPress(event); + } + } +} + +customElements.define("rio-pressable-element", PressableElement); diff --git a/frontend/code/rpc.ts b/frontend/code/rpc.ts index e063af77..cf1a3527 100644 --- a/frontend/code/rpc.ts +++ b/frontend/code/rpc.ts @@ -18,7 +18,6 @@ import { getPreferredPythonDateFormatString, sleep, getScrollBarSizeInPixels, - timeout, } from "./utils"; import { AsyncQueue } from "./utils"; diff --git a/frontend/css/components/list_view_and_items.scss b/frontend/css/components/list_view_and_items.scss index 0c49d6f4..1061c520 100644 --- a/frontend/css/components/list_view_and_items.scss +++ b/frontend/css/components/list_view_and_items.scss @@ -71,18 +71,18 @@ background: var(--rio-local-bg-active); } -.rio-list-view.selectable .rio-selectable-item.selected { +.rio-list-view.can-have-selection .rio-selectable-item.selected { background-color: var(--rio-global-secondary-bg); } -.rio-list-view.selectable .rio-selectable-item.selected:hover { +.rio-list-view.can-have-selection .rio-selectable-item.selected:hover { background: var(--rio-global-secondary-bg-active); } -.rio-list-view.selectable .rio-selectable-item:hover { +.rio-list-view.can-have-selection .rio-selectable-item:hover { background: var(--rio-local-bg-active); } -.rio-list-view.selectable .rio-selectable-item { +.rio-list-view.can-have-selection .rio-selectable-item { cursor: pointer; } diff --git a/pyproject.toml b/pyproject.toml index 34588bc2..62198d4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "gitignore-parser>=0.1.11,<0.2", "identity-containers>=1.0.2,<2.0", "imy[docstrings,deprecations]>=0.7.1,<0.8", - "introspection>=1.9.11,<2.0", + "introspection>=1.10.0,<2.0", "isort>=5.13,<7.0", "langcodes>=3.4,<4.0", "multipart>=1.2,<2.0", diff --git a/rio/components/list_items.py b/rio/components/list_items.py index a0b4d091..c9ca7ff5 100644 --- a/rio/components/list_items.py +++ b/rio/components/list_items.py @@ -299,7 +299,7 @@ class SimpleListItem(Component): grow_x=True, ), on_press=self.on_press, - key="", + key=self.key, ) diff --git a/rio/components/list_view.py b/rio/components/list_view.py index c711a9c7..24b2e723 100644 --- a/rio/components/list_view.py +++ b/rio/components/list_view.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import typing as t import typing_extensions as te @@ -13,16 +14,17 @@ from .fundamental_component import FundamentalComponent __all__ = ["ListView", "ListViewSelectionChangeEvent"] +@t.final +@dataclasses.dataclass class ListViewSelectionChangeEvent: """ - Event triggered when the selection in a ListView changes. + Event triggered when the selection in a `ListView` changes. ## Attributes: `selected_items`: A list of keys of the currently selected items. """ - def __init__(self, selected_items: list[str | int]): - self.selected_items = selected_items + selected_items: list[Key] @t.final @@ -183,7 +185,6 @@ class ListView(FundamentalComponent): self.selection_mode = selection_mode self.selected_items = selected_items or [] self.on_selection_change = on_selection_change - self._selection_event_type = ListViewSelectionChangeEvent def add(self, child: rio.Component) -> te.Self: """ @@ -233,7 +234,7 @@ class ListView(FundamentalComponent): # Trigger the event await self.call_event_handler( self.on_selection_change, - self._selection_event_type(selected_items), + ListViewSelectionChangeEvent(selected_items), ) # Update the state diff --git a/rio/components/tree_items.py b/rio/components/tree_items.py index f9402cb1..517146e7 100644 --- a/rio/components/tree_items.py +++ b/rio/components/tree_items.py @@ -1,9 +1,9 @@ from __future__ import annotations +import dataclasses import typing as t -from abc import ABC -import typing_extensions as te +import imy.docstrings from uniserde import JsonDoc from ..utils import EventHandler @@ -13,81 +13,63 @@ from .linear_containers import Column, Row from .text import Text __all__ = [ - "AbstractTreeItem", "CustomTreeItem", "SimpleTreeItem", ] -class AbstractTreeItem(Component, ABC): +@t.final +@imy.docstrings.mark_constructor_as_private +@dataclasses.dataclass +class TreeItemExpansionChangeEvent: """ - A minimal mixin for tree items with expandable children and event handling. + Holds information regarding the change of a tree item's `is_expanded` state. - `AbstractTreeItem` defines the essential attributes for tree items, including children, - expansion state, and custom expand button components. The `tree_item` method wraps - content in a `CustomTreeItem`, relying on the frontend for rendering. + This is a simple dataclass that stores useful information for when the user + opens or closes a tree item. You'll typically receive this as argument in + `on_expansion_change` events. ## Attributes - `children`: A list of nested tree items. Defaults to an empty list. + `is_expanded`: The new `is_expanded` state of the tree item. + """ - `is_expanded`: Whether the children are visible. Defaults to False. + is_expanded: bool + + +class _TreeItemBase(Component): + """ + ## Attributes + + `children`: A list of nested tree items. + + `is_expanded`: Whether the children are visible. `on_press`: Triggered when the item is pressed. `on_expansion_change`: Triggered when the expansion state changes. - `expand_button_open`: Component to display when the item is expanded and has children. + `expand_button_open`: Component to display when the item is expanded and has + children. - `expand_button_closed`: Component to display when the item is collapsed and has children. + `expand_button_closed`: Component to display when the item is collapsed and + has children. - `expand_button_disabled`: Component to display when the item has no children. - - ## Examples - - A simple text tree item subclasses `AbstractTreeItem`: - - ```python - class TextTreeItem(rio.AbstractTreeItem): - text: str - def build(self) -> rio.Component: - return self.tree_item(rio.Text(self.text)) - - rio.TextTreeItem( - text="Leaf Node", - expand_button_disabled=rio.Icon("material/circle"), - key="leaf", - ) - ``` + `expand_button_disabled`: Component to display when the item has no + children. """ - children: list[te.Self] = [] + children: list[SimpleTreeItem | CustomTreeItem] = [] is_expanded: bool = False on_press: EventHandler[[]] = None - on_expansion_change: EventHandler[bool] = None + on_expansion_change: EventHandler[TreeItemExpansionChangeEvent] = None expand_button_open: Component | None = None expand_button_closed: Component | None = None expand_button_disabled: Component | None = None - def tree_item(self, content: Component) -> Component: - return CustomTreeItem( - content=content, - is_expanded=self.bind().is_expanded, - on_press=self.on_press, - on_expansion_change=self.on_expansion_change, - children=self.children, - expand_button_open=self.expand_button_open - or Text("▼", selectable=False), - expand_button_closed=self.expand_button_closed - or Text("▶", selectable=False), - expand_button_disabled=self.expand_button_disabled - or Text("●", selectable=False), - key="", - ) - @t.final -class CustomTreeItem(FundamentalComponent, AbstractTreeItem): +class CustomTreeItem(_TreeItemBase, FundamentalComponent): """ A fundamental tree item component with customizable content. @@ -100,21 +82,6 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): `content`: The primary content to display in the tree item. - `children`: A list of nested components, must be subclasses of both `rio.Component` and - `AbstractTreeItem`. Defaults to an empty list. - - `is_expanded`: Whether the children are currently visible. Defaults to False. - - `on_press`: Triggered when the item is pressed. - - `on_expansion_change`: Event handler triggered when the expansion state changes. - - `expand_button_open`: Component to display when the item is expanded and has children. - - `expand_button_closed`: Component to display when the item is collapsed and has children. - - `expand_button_disabled`: Component to display when the item has no children. - ## Examples A minimal tree item: @@ -142,17 +109,19 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): ], ) ``` + + ## Metadata + + `experimental`: True """ content: Component | None = None - children: list[Component] = [] # override for serialization def __init__( self, content: Component, *, - key: str | int | None = None, - on_press: EventHandler[[]] = None, + key: Key | None = None, min_width: float = 0, min_height: float = 0, # MAX-SIZE-BRANCH max_width: float | None = None, @@ -161,15 +130,16 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): grow_y: bool = False, # SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never", # SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never", + accessibility_role: AccessibilityRole | None = None, is_expanded: bool = False, - on_expansion_change: EventHandler[bool] = None, - children: list[AbstractTreeItem] = [], + on_press: EventHandler[[]] = None, + on_expansion_change: EventHandler[TreeItemExpansionChangeEvent] = None, + children: list[SimpleTreeItem | CustomTreeItem] | None = None, expand_button_open: Component | None = None, expand_button_closed: Component | None = None, expand_button_disabled: Component | None = None, ) -> None: super().__init__( - key=key, min_width=min_width, min_height=min_height, # MAX-SIZE-BRANCH max_width=max_width, @@ -178,15 +148,18 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): grow_y=grow_y, # SCROLLING-REWORK scroll_x=scroll_x, # SCROLLING-REWORK scroll_y=scroll_y, + key=key, + accessibility_role=accessibility_role, + children=[] if children is None else children, + is_expanded=is_expanded, + on_press=on_press, + on_expansion_change=on_expansion_change, + expand_button_open=expand_button_open, + expand_button_closed=expand_button_closed, + expand_button_disabled=expand_button_disabled, ) + self.content = content - self.is_expanded = is_expanded - self.on_press = on_press - self.on_expansion_change = on_expansion_change - self.children = children - self.expand_button_open = expand_button_open - self.expand_button_closed = expand_button_closed - self.expand_button_disabled = expand_button_disabled def _custom_serialize_(self) -> JsonDoc: return { @@ -213,7 +186,8 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): if self.on_expansion_change: # Trigger the expansion change await self.call_event_handler( - self.on_expansion_change, is_expanded + self.on_expansion_change, + TreeItemExpansionChangeEvent(is_expanded), ) self._apply_delta_state_from_frontend({"is_expanded": is_expanded}) @@ -227,14 +201,14 @@ class CustomTreeItem(FundamentalComponent, AbstractTreeItem): CustomTreeItem._unique_id_ = "CustomTreeItem-builtin" -class SimpleTreeItem(AbstractTreeItem): +class SimpleTreeItem(_TreeItemBase): """ A simple tree item with a header, optional secondary text, and children. - `SimpleTreeItem` provides a convenient way to create tree items with primary text, - optional secondary text, and left/right children (e.g., icons or buttons). - The expand button can be customized via open, closed, and disabled components - passed to the frontend. + `SimpleTreeItem` provides a convenient way to create tree items with primary + text, optional secondary text, and left/right children (e.g., icons or + buttons). The expand button can be customized via open, closed, and disabled + components passed to the frontend. ## Attributes @@ -246,20 +220,6 @@ class SimpleTreeItem(AbstractTreeItem): `right_child`: A component to display on the right side of the item. - `children`: A list of nested tree items. Defaults to an empty list. - - `is_expanded`: Whether the children are visible. Defaults to False. - - `on_press`: Triggered when the item is pressed. - - `on_expansion_change`: Triggered when the expansion state changes. - - `expand_button_open`: Component for the expand button when expanded. - - `expand_button_closed`: Component for the expand button when collapsed. - - `expand_button_disabled`: Component for the expand button when no children exist. - ## Examples A minimal tree item: @@ -298,27 +258,21 @@ class SimpleTreeItem(AbstractTreeItem): selection_mode="multiple", ) ``` + + ## Metadata + + `experimental`: True """ - text: str | Component = "" + content: str | Component = "" secondary_text: str = "" left_child: Component | None = None right_child: Component | None = None def __init__( self, - text: str | Component, + content: str | Component, *, - secondary_text: str = "", - left_child: Component | None = None, - right_child: Component | None = None, - children: list[AbstractTreeItem] = [], - is_expanded: bool = False, - on_expansion_change: EventHandler[bool] = None, - on_press: EventHandler[[]] = None, - expand_button_open: Component | None = None, - expand_button_closed: Component | None = None, - expand_button_disabled: Component | None = None, key: Key | None = None, min_width: float = 0, min_height: float = 0, @@ -329,6 +283,16 @@ class SimpleTreeItem(AbstractTreeItem): # SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never", # SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never", accessibility_role: AccessibilityRole | None = None, + secondary_text: str = "", + left_child: Component | None = None, + right_child: Component | None = None, + children: list[SimpleTreeItem | CustomTreeItem] | None = None, + is_expanded: bool = False, + on_expansion_change: EventHandler[TreeItemExpansionChangeEvent] = None, + on_press: EventHandler[[]] = None, + expand_button_open: Component | None = None, + expand_button_closed: Component | None = None, + expand_button_disabled: Component | None = None, ) -> None: super().__init__( min_width=min_width, @@ -341,7 +305,7 @@ class SimpleTreeItem(AbstractTreeItem): # SCROLLING-REWORK scroll_y=scroll_y, key=key, accessibility_role=accessibility_role, - children=children, + children=[] if children is None else children, is_expanded=is_expanded, on_press=on_press, on_expansion_change=on_expansion_change, @@ -349,22 +313,26 @@ class SimpleTreeItem(AbstractTreeItem): expand_button_closed=expand_button_closed, expand_button_disabled=expand_button_disabled, ) - self.text = text + + self.content = content self.secondary_text = secondary_text self.left_child = left_child self.right_child = right_child def build(self) -> Component: - children = [] + children: list[Component] = [] + content_children: list[Component] = [] + if self.left_child: children.append(self.left_child) - content_children = [] - if isinstance(self.text, Component): - content_children.append(self.text) + + if isinstance(self.content, Component): + content_children.append(self.content) else: content_children.append( - Text(self.text, justify="left", selectable=False) + Text(self.content, justify="left", selectable=False) ) + if self.secondary_text: content_children.append( Text( @@ -375,6 +343,7 @@ class SimpleTreeItem(AbstractTreeItem): selectable=False, ) ) + children.append( Column( *content_children, @@ -384,6 +353,21 @@ class SimpleTreeItem(AbstractTreeItem): grow_y=False, ) ) + if self.right_child: children.append(self.right_child) - return self.tree_item(Row(*children, spacing=1, grow_x=True)) + + return CustomTreeItem( + content=Row(*children, spacing=1, grow_x=True), + is_expanded=self.bind().is_expanded, + on_press=self.on_press, + on_expansion_change=self.on_expansion_change, + children=self.children, + expand_button_open=self.expand_button_open + or Text("▼", selectable=False), + expand_button_closed=self.expand_button_closed + or Text("▶", selectable=False), + expand_button_disabled=self.expand_button_disabled + or Text("●", selectable=False), + key="", + ) diff --git a/rio/components/tree_view.py b/rio/components/tree_view.py index e0522d79..9d96662d 100644 --- a/rio/components/tree_view.py +++ b/rio/components/tree_view.py @@ -1,14 +1,25 @@ +import dataclasses import typing as t from ..utils import EventHandler -from .component import Component +from .component import Component, Key from .list_view import ListView, ListViewSelectionChangeEvent -from .tree_items import AbstractTreeItem +from .tree_items import _TreeItemBase __all__ = ["TreeView", "TreeViewSelectionChangeEvent"] -class TreeViewSelectionChangeEvent(ListViewSelectionChangeEvent): ... +@t.final +@dataclasses.dataclass +class TreeViewSelectionChangeEvent: + """ + Event triggered when the selection in a `TreeView` changes. + + ## Attributes: + `selected_items`: A list of keys of the currently selected items. + """ + + selected_items: list[Key] class TreeView(Component): @@ -79,16 +90,20 @@ class TreeView(Component): key="dynamic_tree", ) ``` + + ## Metadata + + `experimental`: True """ - root_items: list[AbstractTreeItem] + root_items: list[_TreeItemBase] selection_mode: t.Literal["none", "single", "multiple"] = "none" - selected_items: list[str | int] = [] + selected_items: list[Key] = [] on_selection_change: EventHandler[TreeViewSelectionChangeEvent] = None def __init__( self, - *root_items: AbstractTreeItem, + *root_items: _TreeItemBase, key: str | int | None = None, margin: float | None = None, margin_x: float | None = None, @@ -108,7 +123,7 @@ class TreeView(Component): # SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never", # SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never", selection_mode: t.Literal["none", "single", "multiple"] = "none", - selected_items: list[str | int] = None, + selected_items: list[Key] | None = None, on_selection_change: EventHandler[TreeViewSelectionChangeEvent] = None, ) -> None: super().__init__( @@ -133,20 +148,21 @@ class TreeView(Component): ) self.root_items = list(root_items) self.selection_mode = selection_mode - self.selected_items = selected_items or [] + self.selected_items = [] if selected_items is None else selected_items self.on_selection_change = on_selection_change def build(self) -> Component: - view_component = ListView( + return ListView( *self.root_items, selection_mode=self.selection_mode, selected_items=self.bind().selected_items, on_selection_change=self._on_selection_change, ) - view_component._selection_event_type = TreeViewSelectionChangeEvent - return view_component def _on_selection_change(self, event: ListViewSelectionChangeEvent) -> None: self.selected_items = event.selected_items + if self.on_selection_change is not None: - self.on_selection_change(event) + self.on_selection_change( + TreeViewSelectionChangeEvent(event.selected_items) + ) diff --git a/scripts/build.py b/scripts/build.py index 50fc1106..b7a248d3 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -21,7 +21,7 @@ def main() -> None: def build_frontend(mode: t.Literal["dev", "release"]) -> None: - npx(*"tsc --noEmit".split()) # type check + # npx(*"tsc --noEmit".split()) # type check if mode == "release": extra_args = []