mirror of
https://github.com/rio-labs/rio.git
synced 2026-04-26 06:08:30 -05:00
fix selection for nested ListViews
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -299,7 +299,7 @@ class SimpleListItem(Component):
|
||||
grow_x=True,
|
||||
),
|
||||
on_press=self.on_press,
|
||||
key="",
|
||||
key=self.key,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user