fix selection for nested ListViews

This commit is contained in:
Aran-Fey
2025-04-25 23:43:15 +02:00
parent 3ebb1aab1d
commit 7c86470760
75 changed files with 892 additions and 715 deletions
+41 -20
View File
@@ -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<ComponentBase>();
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<ComponentBase>
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<ComponentBase>();
public addEventListener(
type: "all states updated",
callback: EventListenerOrEventListenerObject | null,
options?: AddEventListenerOptions | boolean
): void {
super.addEventListener(type, callback, options);
}
}
+8 -9
View File
@@ -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<AbstractButtonState
updateElement(
deltaState: DeltaState<AbstractButtonState>,
latentComponents: Set<ComponentBase>
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";
+4 -3
View File
@@ -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<CalendarState> {
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<CalendarState> {
updateElement(
deltaState: DeltaState<CalendarState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.is_sensitive !== undefined) {
if (deltaState.is_sensitive) {
+5 -4
View File
@@ -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<CardState> {
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<CardState> {
updateElement(
deltaState: DeltaState<CardState>,
latentComponents: Set<ComponentBase>
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) {
+4 -3
View File
@@ -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<CheckboxState> {
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<CheckboxState> {
updateElement(
deltaState: DeltaState<CheckboxState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.is_on !== undefined) {
if (deltaState.is_on) {
+5 -4
View File
@@ -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<ClassContainerState> {
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
return document.createElement("div");
}
updateElement(
deltaState: DeltaState<ClassContainerState>,
latentComponents: Set<ComponentBase>
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
+4 -3
View File
@@ -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<CodeBlockState> {
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
const element = document.createElement("div");
return element;
}
updateElement(
deltaState: DeltaState<CodeBlockState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Re-create the code block
convertDivToCodeBlock(
+9 -5
View File
@@ -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<CodeExplorerState> {
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<CodeExplorerState> {
updateElement(
deltaState: DeltaState<CodeExplorerState>,
latentComponents: Set<ComponentBase>
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<CodeExplorerState> {
// Update the child
this.replaceOnlyChild(
latentComponents,
context,
deltaState.build_result,
this.buildResultElement
);
+4 -3
View File
@@ -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<ColorPickerState> {
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<ColorPickerState> {
updateElement(
deltaState: DeltaState<ColorPickerState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Color
//
+31 -21
View File
@@ -1,6 +1,7 @@
import {
componentsByElement,
componentsById,
ComponentStatesUpdateContext,
getComponentByElement,
} from "../componentManagement";
import { callRemoteMethodDiscardResponse } from "../rpc";
@@ -76,11 +77,15 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
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<S extends ComponentState = ComponentState> {
/// an argument because it's more efficient than calling `this.element`.
updateElement(
deltaState: DeltaState<S>,
latentComponents: Set<ComponentBase>
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<S extends ComponentState = ComponentState> {
}
}
private unparent(latentComponents: Set<ComponentBase>): 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<S extends ComponentState = ComponentState> {
);
this.parent!.children.delete(this);
latentComponents.add(this);
context.latentComponents.add(this);
}
registerChild(
latentComponents: Set<ComponentBase>,
context: ComponentStatesUpdateContext,
child: ComponentBase
): void {
// Remove the child from its previous parent
@@ -324,14 +329,14 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
// 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<ComponentBase>,
context: ComponentStatesUpdateContext,
childId: ComponentId | undefined,
parentElement: HTMLElement = this.element
): void {
@@ -344,14 +349,14 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
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<ComponentBase>,
context: ComponentStatesUpdateContext,
childId: null | undefined | ComponentId,
parentElement: HTMLElement = this.element
): void {
@@ -372,7 +377,7 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
let child = getComponentByElement(currentChildElement);
currentChildElement.remove();
child.unparent(latentComponents);
child.unparent(context);
}
console.assert(
@@ -394,14 +399,14 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
}
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<S extends ComponentState = ComponentState> {
/// If `wrapInDivs` is true, each child is wrapped in a `<div>` element.
/// This also requires any existing children to be wrapped in `<div>`s.
replaceChildren(
latentComponents: Set<ComponentBase>,
context: ComponentStatesUpdateContext,
childIds: undefined | ComponentId[],
parentElement: HTMLElement = this.element,
wrapInDivs: boolean = false
@@ -452,7 +457,7 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
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<S extends ComponentState = ComponentState> {
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<S extends ComponentState = ComponentState> {
wrap(expectedChild.outerElement),
curElement
);
this.registerChild(latentComponents, expectedChild);
this.registerChild(context, expectedChild);
curIndex++;
}
@@ -514,7 +519,7 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
/// This is **not recursive**. It only looks through the direct children of
/// an element and removes them.
removeHtmlOrComponentChildren(
latentComponents: Set<ComponentBase>,
context: ComponentStatesUpdateContext,
parentElement: HTMLElement
) {
while (true) {
@@ -534,7 +539,7 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
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<S extends ComponentState = ComponentState> {
/// 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<S extends ComponentState = ComponentState> {
_setStateDontNotifyBackend(deltaState: DeltaState<S>): void {
// Trigger an update
this.updateElement(deltaState, null as any as Set<ComponentBase>);
this.updateElement(
deltaState,
null as any as ComponentStatesUpdateContext
);
// Set the state
Object.assign(this.state, deltaState);
+3 -1
View File
@@ -7,7 +7,9 @@ export type ComponentPickerState = ComponentState & {
};
export class ComponentPickerComponent extends ComponentBase<ComponentPickerState> {
protected createElement(): HTMLElement {
protected createElement(
context: ComponentStatesUpdateContext
): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-component-picker");
+7 -4
View File
@@ -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<ComponentTreeState> {
private nodesByComponent: WeakMap<ComponentBase, HTMLElement> =
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<ComponentTreeState> {
updateElement(
deltaState: DeltaState<ComponentTreeState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.component_id !== undefined) {
// Highlight the tree item
@@ -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<CustomListItemState> {
// 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<CustomListItemState>,
latentComponents: Set<ComponentBase>
): 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",
});
}
}
+12 -10
View File
@@ -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<CustomTreeItemState>
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<CustomTreeItemState>
"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<CustomTreeItemState>
updateElement(
deltaState: DeltaState<CustomTreeItemState>,
latentComponents: Set<ComponentBase>
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<CustomTreeItemState>
//update children
if (deltaState.children !== undefined) {
this.replaceChildren(
latentComponents,
context,
deltaState.children,
this.childrenContainerElement
);
@@ -136,8 +138,8 @@ export class CustomTreeItemComponent extends ComponentBase<CustomTreeItemState>
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;
}
@@ -10,7 +10,7 @@ export class DevToolsConnectorComponent extends ComponentBase<DevToolsConnectorS
// If component tree components exists, they register here
public componentTreeComponent: ComponentTreeComponent | null = null;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
// Make the component globally known
setDevToolsConnector(this);
+7 -6
View File
@@ -1,5 +1,6 @@
import {
componentsById,
ComponentStatesUpdateContext,
recursivelyDeleteComponent,
} from "../componentManagement";
import { ComponentId } from "../dataModels";
@@ -27,7 +28,7 @@ export class DialogContainerComponent extends ComponentBase<DialogContainerState
// Used to restore the keyboard focus when the dialog is closed
private previouslyFocusedElement: Element | null;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
// Create the HTML elements
let element = document.createElement("div");
element.classList.add("rio-dialog-container");
@@ -48,7 +49,7 @@ export class DialogContainerComponent extends ComponentBase<DialogContainerState
// Open the popup manager once we're confident that all components have
// been created
requestAnimationFrame(() => {
context.addEventListener("all states updated", () => {
this.previouslyFocusedElement = document.activeElement;
this.popupManager.isOpen = true;
});
@@ -105,13 +106,13 @@ export class DialogContainerComponent extends ComponentBase<DialogContainerState
updateElement(
deltaState: DeltaState<DialogContainerState>,
latentComponents: Set<ComponentBase>
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<DialogContainerState
let owningComponent =
componentsById[deltaState.owning_component_id]!;
owningComponent.registerChild(latentComponents, this);
owningComponent.registerChild(context, this);
}
}
+6 -9
View File
@@ -4,6 +4,7 @@ import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ColorSet, ComponentId } from "../dataModels";
import { applySwitcheroo } from "../designApplication";
import { markEventAsHandled } from "../eventHandling";
import { ComponentStatesUpdateContext } from "../componentManagement";
export type DrawerState = ComponentState & {
_type_: "Drawer-builtin";
@@ -28,7 +29,7 @@ export class DrawerComponent extends ComponentBase<DrawerState> {
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<DrawerState> {
updateElement(
deltaState: DeltaState<DrawerState>,
latentComponents: Set<ComponentBase>
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
);
+4 -3
View File
@@ -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<DropdownState>
// 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<DropdownState>
updateElement(
deltaState: DeltaState<DropdownState>,
latentComponents: Set<ComponentBase>
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
+4 -3
View File
@@ -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<ErrorPlaceholderSta
private summaryElement: HTMLElement;
private detailsElement: HTMLElement;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
// Create the elements
let element = document.createElement("div");
element.classList.add("rio-error-placeholder");
@@ -54,9 +55,9 @@ export class ErrorPlaceholderComponent extends ComponentBase<ErrorPlaceholderSta
updateElement(
deltaState: DeltaState<ErrorPlaceholderState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.error_summary !== undefined) {
this.summaryElement.innerText = deltaState.error_summary;
+7 -6
View File
@@ -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<FilePickerAreaState>
private uploadProgresses: Map<number, [number, number, boolean]> =
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<FilePickerAreaState>
updateElement(
deltaState: DeltaState<FilePickerAreaState>,
latentComponents: Set<ComponentBase>
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<FilePickerAreaState>
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<FilePickerAreaState>
deltaState.child_component === null
) {
this.removeHtmlOrComponentChildren(
latentComponents,
context,
this.childContentContainer
);
this.childContentContainer.appendChild(
+8 -5
View File
@@ -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<FlowState> {
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<FlowState> {
updateElement(
deltaState: DeltaState<FlowState>,
latentComponents: Set<ComponentBase>
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<FlowState> {
if (deltaState.children !== undefined) {
this.replaceChildren(
latentComponents,
context,
deltaState.children,
this.innerElement,
true
@@ -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<FundamentalRootCompo
private connectionLostPopupContainer: HTMLElement;
public connectionLostPopupOverlaysContainer: HTMLElement;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-fundamental-root-component");
@@ -126,14 +129,14 @@ export class FundamentalRootComponent extends ComponentBase<FundamentalRootCompo
updateElement(
deltaState: DeltaState<FundamentalRootComponentState>,
latentComponents: Set<ComponentBase>
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<FundamentalRootCompo
// Connection lost popup
if (deltaState.connection_lost_component !== undefined) {
this.replaceOnlyChild(
latentComponents,
context,
deltaState.connection_lost_component,
this.connectionLostPopupContainer
);
@@ -151,7 +154,7 @@ export class FundamentalRootComponent extends ComponentBase<FundamentalRootCompo
// Dev tools sidebar
if (deltaState.dev_tools !== undefined) {
this.replaceOnlyChild(
latentComponents,
context,
deltaState.dev_tools,
this.devToolsContainer
);
@@ -19,6 +19,7 @@ import {
updateConnectionFromObject,
} from "./utils";
import { CuttingConnectionStrategy } from "./cuttingConnectionStrategy";
import { ComponentStatesUpdateContext } from "../../componentManagement";
export type GraphEditorState = ComponentState & {
_type_: "GraphEditor-builtin";
@@ -48,7 +49,7 @@ export class GraphEditorComponent extends ComponentBase<GraphEditorState> {
| 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<GraphEditorState> {
updateElement(
deltaState: DeltaState<GraphEditorState>,
latentComponents: Set<ComponentBase>
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<GraphEditorState> {
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<GraphEditorState> {
/// Creates a node element and adds it to the HTML child. Returns the node
/// state, augmented with the HTML element.
_makeNode(
latentComponents: Set<ComponentBase>,
context: ComponentStatesUpdateContext,
nodeState: NodeState
): AugmentedNodeState {
// Build the node HTML
@@ -336,7 +337,7 @@ export class GraphEditorComponent extends ComponentBase<GraphEditorState> {
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;
+8 -5
View File
@@ -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<GridState> {
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<GridState> {
updateElement(
deltaState: DeltaState<GridState>,
latentComponents: Set<ComponentBase>
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
@@ -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<HeadingListItemState> {
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<HeadingListItemState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
if (deltaState.text !== undefined) {
this.element.textContent = deltaState.text;
}
}
}
@@ -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<HighLevelComponentState> {
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<HighLevelComponentState> {
updateElement(
deltaState: DeltaState<HighLevelComponentState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
this.replaceOnlyChild(latentComponents, deltaState._child_);
this.replaceOnlyChild(context, deltaState._child_);
}
}
+4 -3
View File
@@ -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<IconState> {
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<IconState> {
updateElement(
deltaState: DeltaState<IconState>,
latentComponents: Set<ComponentBase>
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
+4 -3
View File
@@ -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<ImageState> {
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<ImageState> {
updateElement(
deltaState: DeltaState<ImageState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (
deltaState.imageUrl !== undefined &&
+5 -4
View File
@@ -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<KeyEve
private keyUpCombinations: Set<string> | true;
private keyPressCombinations: Set<string> | 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<KeyEve
updateElement(
deltaState: DeltaState<KeyEventListenerState>,
latentComponents: Set<ComponentBase>
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<KeyEve
this.element.onkeyup = null;
}
this.replaceOnlyChild(latentComponents, deltaState.content);
this.replaceOnlyChild(context, deltaState.content);
}
private handleKeyEvent(
+7 -4
View File
@@ -1,5 +1,8 @@
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { componentsById } from "../componentManagement";
import {
componentsById,
ComponentStatesUpdateContext,
} from "../componentManagement";
import { getDisplayableChildren } from "../devToolsTreeWalk";
import { Highlighter } from "../highlighter";
import { Debouncer } from "../debouncer";
@@ -32,7 +35,7 @@ export class LayoutDisplayComponent extends ComponentBase<LayoutDisplayState> {
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<LayoutDisplayState> {
updateElement(
deltaState: DeltaState<LayoutDisplayState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Has the target component changed?
if (deltaState.component_id !== undefined) {
+12 -9
View File
@@ -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<LinearContainerState
}
}
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-linear-container");
@@ -72,14 +75,14 @@ export abstract class LinearContainer extends ComponentBase<LinearContainerState
updateElement(
deltaState: DeltaState<LinearContainerState>,
latentComponents: Set<ComponentBase>
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;
}
+7 -6
View File
@@ -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<LinkState> {
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<LinkState> {
updateElement(
deltaState: DeltaState<LinkState>,
latentComponents: Set<ComponentBase>
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<LinkState> {
(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<LinkState> {
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");
+181
View File
@@ -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<S> {
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<HeadingListItemState> {
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<HeadingListItemState>,
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<SeparatorListItemState> {
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<CustomListItemState> {
// 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<CustomListItemState>,
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",
});
}
}
+102 -151
View File
@@ -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<ListViewState> {
private clickHandlers: Map<
Key,
[(event: MouseEvent) => void, ComponentId]
> = new Map();
private selectionKeysByOwner: Map<ComponentId, Set<Key>> = new Map();
private items = new Set<SelectableListItemComponent<ComponentState>>();
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<ListViewState>,
latentComponents: Set<ComponentBase>
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<ListViewState> {
// 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<ComponentState>): void {
this.items.add(item);
this.updateItemIsSelected(item);
this.updateItemIsSelectable(item);
}
unregisterItem(item: SelectableListItemComponent<ComponentState>): 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<ListViewState> {
return this._isGroupedListItemWorker(comp);
}
_updateChildStyles(): void {
updateChildStyles(): void {
// Precompute which children are grouped
let groupedChildren = new Set<HTMLElement>();
for (let child of this.element.children) {
let castChild = child as HTMLElement;
@@ -190,144 +211,74 @@ export class ListViewComponent extends ComponentBase<ListViewState> {
}
}
/// 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<Key>();
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<ComponentState>
): 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<Key>();
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<ComponentState>,
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<ComponentState>) {
item.isSelected = this.state.selected_items.includes(item.state.key);
}
}
+4 -3
View File
@@ -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<MarkdownState> {
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<MarkdownState> {
updateElement(
deltaState: DeltaState<MarkdownState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.text !== undefined) {
let defaultLanguage = firstDefined(
+4 -3
View File
@@ -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<MediaPlayer
this._lastPlaybackTime = currentTime;
}
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-media-player");
element.setAttribute("tabindex", "0");
@@ -569,9 +570,9 @@ export class MediaPlayerComponent extends KeyboardFocusableComponent<MediaPlayer
updateElement(
deltaState: DeltaState<MediaPlayerState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.mediaUrl !== undefined) {
let mediaUrl = new URL(deltaState.mediaUrl, document.location.href)
@@ -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<MouseEventListenerState> {
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<MouseEventListene
updateElement(
deltaState: DeltaState<MouseEventListenerState>,
latentComponents: Set<ComponentBase>
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) => {
@@ -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<Mult
private inputBox: InputBox;
private onChangeLimiter: Debouncer;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let textarea = document.createElement("textarea");
this.inputBox = new InputBox({ inputElement: textarea });
@@ -111,9 +112,9 @@ export class MultiLineTextInputComponent extends KeyboardFocusableComponent<Mult
updateElement(
deltaState: DeltaState<MultiLineTextInputState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.text !== undefined) {
this.inputBox.value = deltaState.text;
+4 -3
View File
@@ -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<NodeInputState> {
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<NodeInputState> {
updateElement(
deltaState: DeltaState<NodeInputState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Name
if (deltaState.name !== undefined) {
+4 -3
View File
@@ -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<NodeOutputState> {
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<NodeOutputState> {
updateElement(
deltaState: DeltaState<NodeOutputState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Name
if (deltaState.name !== undefined) {
+4 -3
View File
@@ -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<NumberInputState> {
private inputBox: InputBox;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
// Note: We don't use `<input type="number">` because of its ugly
// up/down buttons
this.inputBox = new InputBox();
@@ -101,9 +102,9 @@ export class NumberInputComponent extends KeyboardFocusableComponent<NumberInput
updateElement(
deltaState: DeltaState<NumberInputState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.label !== undefined) {
this.inputBox.label = deltaState.label;
+6 -5
View File
@@ -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<OverlayState> {
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<OverlayState> {
moveKeyboardFocusInside: false,
});
requestAnimationFrame(() => {
context.addEventListener("all states updated", () => {
this.popupManager.isOpen = true;
});
@@ -43,12 +44,12 @@ export class OverlayComponent extends ComponentBase<OverlayState> {
updateElement(
deltaState: DeltaState<OverlayState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
this.replaceOnlyChild(
latentComponents,
context,
deltaState.content,
this.overlayContentElement
);
+4 -3
View File
@@ -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<PdfViewerState> {
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<PdfViewerState> {
updateElement(
deltaState: DeltaState<PdfViewerState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (
deltaState.pdfUrl !== undefined &&
+4 -3
View File
@@ -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<PlotState> {
// 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<PlotState> {
updateElement(
deltaState: DeltaState<PlotState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.plot !== undefined) {
if (this.plotManager !== null) {
@@ -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<PointerEventLis
[button: number]: number | undefined;
} = {};
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-pointer-event-listener");
return element;
@@ -36,11 +37,11 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
updateElement(
deltaState: DeltaState<PointerEventListenerState>,
latentComponents: Set<ComponentBase>
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 ||
+9 -10
View File
@@ -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<PopupState> {
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<PopupState> {
updateElement(
deltaState: DeltaState<PopupState>,
latentComponents: Set<ComponentBase>
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<PopupState> {
if (deltaState.content !== undefined) {
this.replaceOnlyChild(
latentComponents,
context,
deltaState.content,
this.popupScrollerElement
);
+4 -3
View File
@@ -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<ProgressBarState> {
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<ProgressBarState> {
updateElement(
deltaState: DeltaState<ProgressBarState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.progress !== undefined) {
// Indeterminate progress
+4 -3
View File
@@ -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<ProgressCircleState> {
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.innerHTML = `
@@ -23,9 +24,9 @@ export class ProgressCircleComponent extends ComponentBase<ProgressCircleState>
updateElement(
deltaState: DeltaState<ProgressCircleState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Apply the progress
if (deltaState.progress !== undefined) {
+5 -4
View File
@@ -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<RectangleState> {
// `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<RectangleState> {
updateElement(
deltaState: DeltaState<RectangleState>,
latentComponents: Set<ComponentBase>
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`;
+5 -4
View File
@@ -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<RevealerState> {
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<RevealerState> {
updateElement(
deltaState: DeltaState<RevealerState>,
latentComponents: Set<ComponentBase>
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<RevealerState> {
// Update the child
this.replaceOnlyChild(
latentComponents,
context,
deltaState.content,
this.contentInnerElement
);
+5 -8
View File
@@ -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<ScrollContainerState
private childContainer: HTMLElement;
private scrollAnchor: HTMLElement;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-scroll-container");
@@ -58,15 +59,11 @@ export class ScrollContainerComponent extends ComponentBase<ScrollContainerState
updateElement(
deltaState: DeltaState<ScrollContainerState>,
latentComponents: Set<ComponentBase>
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;
+13 -10
View File
@@ -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<ScrollTargetState> {
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<ScrollTargetState> {
updateElement(
deltaState: DeltaState<ScrollTargetState>,
latentComponents: Set<ComponentBase>
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<ScrollTargetState> {
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<ScrollTargetState> {
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<ScrollTargetState> {
}
}
private _removeButtonChild(latentComponents: Set<ComponentBase>): void {
private _removeButtonChild(context: ComponentStatesUpdateContext): void {
let buttonChild = this.buttonContainerElement.firstElementChild;
if (buttonChild === null) return;
@@ -87,7 +90,7 @@ export class ScrollTargetComponent extends ComponentBase<ScrollTargetState> {
buttonChild.remove();
} else {
this.replaceOnlyChild(
latentComponents,
context,
childComponent.id,
this.buttonContainerElement
);
+4 -3
View File
@@ -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<SeparatorState> {
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<SeparatorState> {
updateElement(
deltaState: DeltaState<SeparatorState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Color
if (deltaState.color === undefined) {
@@ -1,13 +0,0 @@
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type SeparatorListItemState = ComponentState & {
_type_: "SeparatorListItem-builtin";
};
export class SeparatorListItemComponent extends ComponentBase<SeparatorListItemState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-separator-list-item");
return element;
}
}
+4 -4
View File
@@ -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<SliderState> {
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<SliderState> {
updateElement(
deltaState: DeltaState<SliderState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (
deltaState.minimum !== undefined ||
+5 -4
View File
@@ -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<SlideshowState> {
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<SlideshowState> {
updateElement(
deltaState: DeltaState<SlideshowState>,
latentComponents: Set<ComponentBase>
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
+5 -9
View File
@@ -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<StackState> {
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<StackState> {
updateElement(
deltaState: DeltaState<StackState>,
latentComponents: Set<ComponentBase>
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);
}
}
+4 -3
View File
@@ -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<SwitchState> {
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<SwitchState> {
updateElement(
deltaState: DeltaState<SwitchState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.is_on !== undefined) {
if (deltaState.is_on) {
+12 -9
View File
@@ -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<SwitcherState> {
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<SwitcherState> {
updateElement(
deltaState: DeltaState<SwitcherState>,
latentComponents: Set<ComponentBase>
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<SwitcherState> {
this.element.appendChild(this.activeChildContainer);
this.replaceOnlyChild(
latentComponents,
context,
deltaState.content,
this.activeChildContainer
);
@@ -57,7 +60,7 @@ export class SwitcherComponent extends ComponentBase<SwitcherState> {
} 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<SwitcherState> {
private async replaceContent(
content: ComponentId | null,
latentComponents: Set<ComponentBase>,
context: ComponentStatesUpdateContext,
transitionTime: number
): Promise<void> {
// Animating the size is trickier than you might expect. Firstly, CSS
@@ -104,7 +107,7 @@ export class SwitcherComponent extends ComponentBase<SwitcherState> {
) 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<SwitcherState> {
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
+4 -3
View File
@@ -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<SwitcherBarState> {
// 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<SwitcherBarState> {
updateElement(
deltaState: DeltaState<SwitcherBarState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
// Have the options changed?
if (deltaState.items !== undefined) {
+4 -3
View File
@@ -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<TableState> {
// 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<TableState> {
updateElement(
deltaState: DeltaState<TableState>,
latentComponents: Set<ComponentBase>
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;
+4 -3
View File
@@ -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<TextState> {
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<TextState> {
updateElement(
deltaState: DeltaState<TextState>,
latentComponents: Set<ComponentBase>
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) {
+4 -3
View File
@@ -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<TextInputStat
private inputBox: InputBox;
private onChangeLimiter: Debouncer;
createElement(): HTMLElement {
createElement(context: ComponentStatesUpdateContext): HTMLElement {
this.inputBox = new InputBox();
let element = this.inputBox.outerElement;
@@ -106,9 +107,9 @@ export class TextInputComponent extends KeyboardFocusableComponent<TextInputStat
updateElement(
deltaState: DeltaState<TextInputState>,
latentComponents: Set<ComponentBase>
context: ComponentStatesUpdateContext
): void {
super.updateElement(deltaState, latentComponents);
super.updateElement(deltaState, context);
if (deltaState.text !== undefined) {
this.inputBox.value = deltaState.text;
@@ -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<ThemeContextSwitcherState> {
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<ThemeContextSwi
updateElement(
deltaState: DeltaState<ThemeContextSwitcherState>,
latentComponents: Set<ComponentBase>
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) {
+6 -9
View File
@@ -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<TooltipState> {
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<TooltipState> {
updateElement(
deltaState: DeltaState<TooltipState>,
latentComponents: Set<ComponentBase>
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
);
+4 -3
View File
@@ -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<WebviewState> {
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<WebviewState> {
updateElement(
deltaState: DeltaState<WebviewState>,
latentComponents: Set<ComponentBase>
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
@@ -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 = `
<style>
:host {
display: block;
cursor: pointer;
}
:host(:focus) {
outline: 2px solid var(--focus-color, Highlight);
outline-offset: 2px;
}
</style>
<slot></slot>
`;
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);
-1
View File
@@ -18,7 +18,6 @@ import {
getPreferredPythonDateFormatString,
sleep,
getScrollBarSizeInPixels,
timeout,
} from "./utils";
import { AsyncQueue } from "./utils";
@@ -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;
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -299,7 +299,7 @@ class SimpleListItem(Component):
grow_x=True,
),
on_press=self.on_press,
key="",
key=self.key,
)
+6 -5
View File
@@ -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
+101 -117
View File
@@ -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="",
)
+28 -12
View File
@@ -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)
)
+1 -1
View File
@@ -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 = []