diff --git a/frontend/code/components/align.ts b/frontend/code/components/align.ts index dc09eacb..9bae5592 100644 --- a/frontend/code/components/align.ts +++ b/frontend/code/components/align.ts @@ -22,7 +22,7 @@ export class AlignComponent extends ComponentBase { deltaState: AlignState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); this.makeLayoutDirty(); } diff --git a/frontend/code/components/button.ts b/frontend/code/components/button.ts index 96f96155..f2b95e26 100644 --- a/frontend/code/components/button.ts +++ b/frontend/code/components/button.ts @@ -64,7 +64,7 @@ export class ButtonComponent extends SingleContainer { latentComponents: Set ): void { // Update the child - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.innerElement diff --git a/frontend/code/components/card.ts b/frontend/code/components/card.ts index 7ccfc000..273c7c7f 100644 --- a/frontend/code/components/card.ts +++ b/frontend/code/components/card.ts @@ -43,7 +43,7 @@ export class CardComponent extends SingleContainer { latentComponents: Set ): void { // Update the child - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); // Update the corner radius if (deltaState.corner_radius !== undefined) { diff --git a/frontend/code/components/classContainer.ts b/frontend/code/components/classContainer.ts index 84dff500..13da0936 100644 --- a/frontend/code/components/classContainer.ts +++ b/frontend/code/components/classContainer.ts @@ -19,7 +19,7 @@ export class ClassContainerComponent extends SingleContainer { deltaState: ClassContainerState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); if (deltaState.classes !== undefined) { // Remove all old values diff --git a/frontend/code/components/codeExplorer.ts b/frontend/code/components/codeExplorer.ts index e92f275d..b603dda3 100644 --- a/frontend/code/components/codeExplorer.ts +++ b/frontend/code/components/codeExplorer.ts @@ -119,7 +119,7 @@ export class CodeExplorerComponent extends ComponentBase { this.resultHighlighterElement.remove(); // Update the child - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.build_result, this.buildResultElement diff --git a/frontend/code/components/componentBase.ts b/frontend/code/components/componentBase.ts index 13c02eb3..dec2ea33 100644 --- a/frontend/code/components/componentBase.ts +++ b/frontend/code/components/componentBase.ts @@ -151,10 +151,10 @@ export abstract class ComponentBase { this.makeLayoutDirty(); } - /// Replaces the first child of the given HTML element with the given child. - /// If `childId` is `null`, removes the current child. If `childId` is - /// `undefined`, does nothing. - replaceFirstChild( + /// 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, childId: null | undefined | ComponentId, parentElement: HTMLElement = this.element diff --git a/frontend/code/components/customListItem.ts b/frontend/code/components/customListItem.ts index a0b5bc53..2602ed6a 100644 --- a/frontend/code/components/customListItem.ts +++ b/frontend/code/components/customListItem.ts @@ -29,7 +29,7 @@ export class CustomListItemComponent extends ComponentBase { latentComponents: Set ): void { // Update the child - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); // Style the surface depending on whether it is pressable if (deltaState.pressable === true) { diff --git a/frontend/code/components/drawer.ts b/frontend/code/components/drawer.ts index d90444c4..e22f84bc 100644 --- a/frontend/code/components/drawer.ts +++ b/frontend/code/components/drawer.ts @@ -77,12 +77,12 @@ export class DrawerComponent extends ComponentBase { latentComponents: Set ): void { // Update the children - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.anchor, this.anchorContainer ); - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.contentInnerContainer diff --git a/frontend/code/components/keyEventListener.ts b/frontend/code/components/keyEventListener.ts index e2790243..2c9f6968 100644 --- a/frontend/code/components/keyEventListener.ts +++ b/frontend/code/components/keyEventListener.ts @@ -754,7 +754,7 @@ export class KeyEventListenerComponent extends SingleContainer { element.onkeyup = null; } - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); } grabKeyboardFocus(): void { diff --git a/frontend/code/components/link.ts b/frontend/code/components/link.ts index 2826d687..b83f6e31 100644 --- a/frontend/code/components/link.ts +++ b/frontend/code/components/link.ts @@ -16,19 +16,15 @@ export type LinkState = ComponentState & { export class LinkComponent extends ComponentBase { state: Required; - childAttributeName(): string { - return 'child_component'; - } - createElement(): HTMLElement { - let containerElement = document.createElement('a'); - containerElement.classList.add('rio-link'); + let element = document.createElement('a'); + element.classList.add('rio-link'); // Listen for clicks // // This only needs to handle pages, since regular links will be handled // by the browser. - containerElement.addEventListener('click', (event: MouseEvent) => { + element.addEventListener('click', (event: MouseEvent) => { if (this.state.isPage) { this.sendMessageToBackend({ page: this.state.targetUrl, @@ -45,7 +41,28 @@ export class LinkComponent extends ComponentBase { event.preventDefault(); }); - return containerElement; + return element; + } + + removeHtmlChild(latentComponents: Set) { + /// If `element` has a child, remove it. There mustn't be more than one. + + // Components need special consideration, since they're tracked + if (this.state.child_component !== null) { + this.replaceOnlyChild(latentComponents, null); + return; + } + + // Plain HTML elements can be removed directly + if (this.state.child_text !== null) { + let element = this.element as HTMLAnchorElement; + while (element.firstChild) { + element.removeChild(element.firstChild); + } + } + + // There should be no children left + console.assert(this.element.childElementCount === 0); } updateElement( @@ -60,7 +77,7 @@ export class LinkComponent extends ComponentBase { deltaState.child_text !== null ) { // Clear any existing children - this.replaceFirstChild(latentComponents, null); + this.removeHtmlChild(latentComponents); // Add the new text let textElement = document.createElement('div'); @@ -76,10 +93,13 @@ export class LinkComponent extends ComponentBase { deltaState.child_component !== undefined && deltaState.child_component !== null ) { - this.replaceFirstChild( - latentComponents, - deltaState.child_component - ); + // Clear any existing children + this.removeHtmlChild(latentComponents); + + // Add the new component + this.replaceOnlyChild(latentComponents, deltaState.child_component); + + // Update the CSS classes element.classList.remove('rio-text-link'); } diff --git a/frontend/code/components/margin.ts b/frontend/code/components/margin.ts index d14aecae..b7230e20 100644 --- a/frontend/code/components/margin.ts +++ b/frontend/code/components/margin.ts @@ -24,7 +24,7 @@ export class MarginComponent extends ComponentBase { deltaState: MarginState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); } updateNaturalWidth(ctx: LayoutContext): void { diff --git a/frontend/code/components/mouseEventListener.ts b/frontend/code/components/mouseEventListener.ts index e913412c..fb68b3c2 100644 --- a/frontend/code/components/mouseEventListener.ts +++ b/frontend/code/components/mouseEventListener.ts @@ -70,7 +70,7 @@ export class MouseEventListenerComponent extends SingleContainer { deltaState: MouseEventListenerState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); if (deltaState.reportPress) { this.element.onclick = (e) => { diff --git a/frontend/code/components/overlay.ts b/frontend/code/components/overlay.ts index af79df9f..5c577b5b 100644 --- a/frontend/code/components/overlay.ts +++ b/frontend/code/components/overlay.ts @@ -21,7 +21,7 @@ export class OverlayComponent extends ComponentBase { deltaState: OverlayState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); } updateAllocatedWidth(ctx: LayoutContext): void { diff --git a/frontend/code/components/placeholder.ts b/frontend/code/components/placeholder.ts index 8d5e77f2..2050efcc 100644 --- a/frontend/code/components/placeholder.ts +++ b/frontend/code/components/placeholder.ts @@ -18,6 +18,6 @@ export class PlaceholderComponent extends SingleContainer { deltaState: PlaceholderState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState._child_); + this.replaceOnlyChild(latentComponents, deltaState._child_); } } diff --git a/frontend/code/components/popup.ts b/frontend/code/components/popup.ts index 204eb2fe..ad5920e9 100644 --- a/frontend/code/components/popup.ts +++ b/frontend/code/components/popup.ts @@ -5,7 +5,6 @@ import { LayoutContext } from '../layouting'; import { ColorSet, ComponentId } from '../models'; import { ComponentBase, ComponentState } from './componentBase'; - export type PopupState = ComponentState & { _type_: 'Popup-builtin'; anchor?: ComponentId; @@ -43,12 +42,12 @@ export class PopupComponent extends ComponentBase { latentComponents: Set ): void { // Update the children - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.anchor, this.anchorContainer ); - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.contentContainer diff --git a/frontend/code/components/rectangle.ts b/frontend/code/components/rectangle.ts index 0067c9ac..b2c030d3 100644 --- a/frontend/code/components/rectangle.ts +++ b/frontend/code/components/rectangle.ts @@ -66,7 +66,7 @@ export class RectangleComponent extends SingleContainer { ): void { let element = this.element; - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); if (deltaState.transition_time !== undefined) { element.style.transitionDuration = `${deltaState.transition_time}s`; diff --git a/frontend/code/components/revealer.ts b/frontend/code/components/revealer.ts index 7b2f806a..aedad3be 100644 --- a/frontend/code/components/revealer.ts +++ b/frontend/code/components/revealer.ts @@ -122,7 +122,7 @@ export class RevealerComponent extends ComponentBase { } // Update the child - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.contentInnerElement diff --git a/frontend/code/components/scrollContainer.ts b/frontend/code/components/scrollContainer.ts index 0151457e..64025a7d 100644 --- a/frontend/code/components/scrollContainer.ts +++ b/frontend/code/components/scrollContainer.ts @@ -57,7 +57,7 @@ export class ScrollContainerComponent extends ComponentBase { deltaState: ScrollContainerState, latentComponents: Set ): void { - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.childContainer diff --git a/frontend/code/components/scrollTarget.ts b/frontend/code/components/scrollTarget.ts index 03b2381f..978c2263 100644 --- a/frontend/code/components/scrollTarget.ts +++ b/frontend/code/components/scrollTarget.ts @@ -19,7 +19,7 @@ export class ScrollTargetComponent extends SingleContainer { deltaState: ScrollTargetState, latentComponents: Set ): void { - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); if (deltaState.id !== undefined) { this.element.id = deltaState.id; diff --git a/frontend/code/components/switcher.ts b/frontend/code/components/switcher.ts index dd9d888c..77bf91eb 100644 --- a/frontend/code/components/switcher.ts +++ b/frontend/code/components/switcher.ts @@ -56,7 +56,7 @@ export class SwitcherComponent extends ComponentBase { // Out with the old if (this.activeChildContainer !== null) { - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, null, this.activeChildContainer @@ -78,7 +78,7 @@ export class SwitcherComponent extends ComponentBase { this.activeChildContainer.style.top = '0'; this.element.appendChild(this.activeChildContainer); - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.content, this.activeChildContainer diff --git a/frontend/code/components/themeContextSwitcher.ts b/frontend/code/components/themeContextSwitcher.ts index fd7b5ac7..c3f1d3e6 100644 --- a/frontend/code/components/themeContextSwitcher.ts +++ b/frontend/code/components/themeContextSwitcher.ts @@ -22,7 +22,7 @@ export class ThemeContextSwitcherComponent extends SingleContainer { latentComponents: Set ): void { // Update the child - this.replaceFirstChild(latentComponents, deltaState.content); + this.replaceOnlyChild(latentComponents, deltaState.content); // Colorize if (deltaState.color !== undefined) { diff --git a/frontend/code/components/tooltip.ts b/frontend/code/components/tooltip.ts index 1dbedfa6..a7a2603a 100644 --- a/frontend/code/components/tooltip.ts +++ b/frontend/code/components/tooltip.ts @@ -53,7 +53,7 @@ export class TooltipComponent extends ComponentBase { ): void { // Update the anchor if (deltaState.anchor !== undefined) { - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.anchor, this.anchorContainer @@ -62,7 +62,7 @@ export class TooltipComponent extends ComponentBase { // Update tip if (deltaState.tip_component !== undefined) { - this.replaceFirstChild( + this.replaceOnlyChild( latentComponents, deltaState.tip_component, this.labelElement diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 189a3a87..b4986f5b 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -1655,13 +1655,16 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, .rio-link { pointer-events: auto; display: block; - - text-align: center; } .rio-text-link { color: var(--rio-local-accent-bg); cursor: pointer; + + // Center the text + display: flex; + align-items: center; + justify-content: center; } // ScrollContainer