mirror of
https://github.com/rio-labs/rio.git
synced 2026-05-04 09:59:16 -05:00
notify components when a child's _grow_ changes
This commit is contained in:
committed by
Jakob Pinterits
parent
a47d2222ba
commit
e0c19ba166
@@ -245,18 +245,31 @@ export function updateComponentStates(
|
||||
? null
|
||||
: getComponentByElement(focusedElement as HTMLElement);
|
||||
|
||||
// Create a HTML element to hold all latent components, so they aren't
|
||||
// garbage collected while updating the DOM.
|
||||
// Create a set to hold all latent components, so they aren't garbage
|
||||
// collected while updating the DOM.
|
||||
let latentComponents = new Set<ComponentBase>();
|
||||
|
||||
// Make sure all components mentioned in the message have a corresponding HTML
|
||||
// element
|
||||
// Keep track of all components whose `_grow_` changed, because their
|
||||
// parents have to be notified so they can update their CSS
|
||||
let growChangedComponents: ComponentBase[] = [];
|
||||
|
||||
// Make sure all components mentioned in the message have a corresponding
|
||||
// HTML element
|
||||
for (let componentIdAsString in deltaStates) {
|
||||
let deltaState = deltaStates[componentIdAsString];
|
||||
let component = componentsById[componentIdAsString];
|
||||
|
||||
// This is a reused component, no need to instantiate a new one
|
||||
if (component) {
|
||||
// Check if its `_grow_` changed
|
||||
if (deltaState._grow_ !== undefined) {
|
||||
if (
|
||||
deltaState._grow_[0] !== component.state._grow_[0] ||
|
||||
deltaState._grow_[1] !== component.state._grow_[1]
|
||||
) {
|
||||
growChangedComponents.push(component);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -313,6 +326,16 @@ export function updateComponentStates(
|
||||
document.body.appendChild(rootElement);
|
||||
}
|
||||
|
||||
// Notify the parents of all elements whose `_grow_` changed to update their
|
||||
// CSS
|
||||
let parents = new Set<ComponentBase>();
|
||||
for (let child of growChangedComponents) {
|
||||
parents.add(child.parent!);
|
||||
}
|
||||
for (let parent of parents) {
|
||||
parent.onChildGrowChanged();
|
||||
}
|
||||
|
||||
// Restore the keyboard focus
|
||||
if (focusedComponent !== null) {
|
||||
restoreKeyboardFocus(focusedComponent, latentComponents);
|
||||
|
||||
@@ -36,6 +36,9 @@ export class GridComponent extends ComponentBase {
|
||||
let element = this.element;
|
||||
|
||||
if (deltaState._children !== undefined) {
|
||||
let childPositions =
|
||||
deltaState._child_positions ?? this.state._child_positions;
|
||||
|
||||
this.replaceChildren(
|
||||
latentComponents,
|
||||
deltaState._children,
|
||||
@@ -45,7 +48,7 @@ export class GridComponent extends ComponentBase {
|
||||
|
||||
for (let [childWrapper, childPos] of zip(
|
||||
this.element.children,
|
||||
deltaState._child_positions!
|
||||
childPositions
|
||||
)) {
|
||||
// Note: `rio.Grid` starts counting at row/column 0, but CSS
|
||||
// starts counting at 1
|
||||
@@ -58,10 +61,7 @@ export class GridComponent extends ComponentBase {
|
||||
}`;
|
||||
}
|
||||
|
||||
this.updateTrackSizes(
|
||||
deltaState._children,
|
||||
deltaState._child_positions!
|
||||
);
|
||||
this.updateTrackSizes(deltaState._children, childPositions);
|
||||
}
|
||||
|
||||
if (deltaState.row_spacing !== undefined) {
|
||||
@@ -84,38 +84,77 @@ export class GridComponent extends ComponentBase {
|
||||
childIds: ComponentId[],
|
||||
childPositions: GridChildPosition[]
|
||||
): void {
|
||||
let maxRow = 0;
|
||||
let maxCol = 0;
|
||||
let growingColumns = new Map<number, boolean>();
|
||||
let growingRows = new Map<number, boolean>();
|
||||
let hasGrowingColumns = false;
|
||||
let hasGrowingRows = false;
|
||||
let childrenWithPositions: [ComponentBase, GridChildPosition][] =
|
||||
childIds.map((childId: ComponentId, index: number) => [
|
||||
componentsById[childId]!,
|
||||
childPositions[index],
|
||||
]);
|
||||
|
||||
for (let [i, childPos] of childPositions.entries()) {
|
||||
let child = componentsById[childIds[i]]!;
|
||||
// Sort the children by the number of rows they take up
|
||||
let childrenByNumberOfRows = Array.from(childrenWithPositions);
|
||||
childrenByNumberOfRows.sort((a, b) => a[1].height - b[1].height);
|
||||
|
||||
maxCol = Math.max(maxCol, childPos.column + childPos.width);
|
||||
maxRow = Math.max(maxRow, childPos.row + childPos.height);
|
||||
let nRows = 0;
|
||||
let growingRows = new Set();
|
||||
|
||||
if (child.state._grow_[0]) {
|
||||
for (
|
||||
let column = childPos.column + childPos.width - 1;
|
||||
column >= childPos.column;
|
||||
column--
|
||||
) {
|
||||
hasGrowingColumns = true;
|
||||
growingColumns.set(column, true);
|
||||
}
|
||||
for (let [childComponent, childPosition] of childrenByNumberOfRows) {
|
||||
// Keep track of how how many rows this grid has
|
||||
nRows = Math.max(nRows, childPosition.row + childPosition.height);
|
||||
|
||||
let allRows = range(
|
||||
childPosition.row,
|
||||
childPosition.row + childPosition.height
|
||||
);
|
||||
|
||||
// Determine which rows need to grow
|
||||
if (!childComponent.state._grow_[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.state._grow_[1]) {
|
||||
for (
|
||||
let row = childPos.row + childPos.height - 1;
|
||||
row >= childPos.row;
|
||||
row--
|
||||
) {
|
||||
hasGrowingRows = true;
|
||||
growingRows.set(row, true);
|
||||
// Does any of the rows already grow?
|
||||
let alreadyGrowing = allRows.some((row) => growingRows.has(row));
|
||||
|
||||
// If not, mark them all as growing
|
||||
if (!alreadyGrowing) {
|
||||
for (let row of allRows) {
|
||||
growingRows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the children by the number of columns they take up
|
||||
let childrenByNumberOfColumns = Array.from(childrenWithPositions);
|
||||
childrenByNumberOfColumns.sort((a, b) => a[1].width - b[1].width);
|
||||
|
||||
let nColumns = 0;
|
||||
let growingColumns = new Set();
|
||||
|
||||
for (let [childComponent, childPosition] of childrenByNumberOfColumns) {
|
||||
// Keep track of how how many columns this grid has
|
||||
nColumns = Math.max(
|
||||
nColumns,
|
||||
childPosition.column + childPosition.width
|
||||
);
|
||||
|
||||
let allColumns = range(
|
||||
childPosition.column,
|
||||
childPosition.column + childPosition.width
|
||||
);
|
||||
|
||||
// Determine which columns need to grow
|
||||
if (!childComponent.state._grow_[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Does any of the rows already grow?
|
||||
let alreadyGrowing = allColumns.some((column) =>
|
||||
growingColumns.has(column)
|
||||
);
|
||||
|
||||
// If not, mark them all as growing
|
||||
if (!alreadyGrowing) {
|
||||
for (let column of allColumns) {
|
||||
growingColumns.add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,26 +163,26 @@ export class GridComponent extends ComponentBase {
|
||||
const NO_GROW = 'min-content';
|
||||
|
||||
let columnWidths: string[] = [];
|
||||
if (hasGrowingColumns) {
|
||||
for (let i = 0; i < maxCol; i++) {
|
||||
columnWidths.push(growingColumns.get(i) ? GROW : NO_GROW);
|
||||
if (growingColumns.size === 0) {
|
||||
// If nobody wants to grow, all of them do
|
||||
for (let i = 0; i < nColumns; i++) {
|
||||
columnWidths.push(GROW);
|
||||
}
|
||||
} else {
|
||||
// If nobody wants to grow, all of them do
|
||||
for (let i = 0; i < maxCol; i++) {
|
||||
columnWidths.push(GROW);
|
||||
for (let i = 0; i < nColumns; i++) {
|
||||
columnWidths.push(growingColumns.has(i) ? GROW : NO_GROW);
|
||||
}
|
||||
}
|
||||
|
||||
let rowHeights: string[] = [];
|
||||
if (hasGrowingRows) {
|
||||
for (let i = 0; i < maxRow; i++) {
|
||||
rowHeights.push(growingRows.get(i) ? GROW : NO_GROW);
|
||||
if (growingRows.size === 0) {
|
||||
// If nobody wants to grow, all of them do
|
||||
for (let i = 0; i < nRows; i++) {
|
||||
rowHeights.push(GROW);
|
||||
}
|
||||
} else {
|
||||
// If nobody wants to grow, all of them do
|
||||
for (let i = 0; i < maxRow; i++) {
|
||||
rowHeights.push(GROW);
|
||||
for (let i = 0; i < nRows; i++) {
|
||||
rowHeights.push(growingRows.has(i) ? GROW : NO_GROW);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { textStyleToCss } from '../cssUtils';
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { ComponentId, TextStyle } from '../dataModels';
|
||||
import { firstDefined } from '../utils';
|
||||
import { commitCss, firstDefined } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { RippleEffect } from '../rippleEffect';
|
||||
|
||||
@@ -163,26 +163,22 @@ export class RevealerComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
// Update the CSS to trigger the expand animation
|
||||
this.contentOuterElement.style.maxHeight = '0';
|
||||
this.element.classList.add('rio-revealer-open');
|
||||
|
||||
// The components may currently be in flux due to a pending re-layout.
|
||||
// If that is the case, reading the `scrollHeight` would lead to an
|
||||
// incorrect value. Wait for the resize to finish before fetching it.
|
||||
requestAnimationFrame(() => {
|
||||
let contentHeight = this.contentInnerElement.scrollHeight;
|
||||
let selfHeight = this.element.scrollHeight;
|
||||
let headerHeight = this.headerElement.scrollHeight;
|
||||
let targetHeight = Math.max(
|
||||
contentHeight,
|
||||
selfHeight - headerHeight
|
||||
);
|
||||
|
||||
this.contentOuterElement.style.maxHeight = `${targetHeight}px`;
|
||||
// Animating max-height only works with fixed values (and not
|
||||
// `unset`, etc), so we have to assign the child's exact height in
|
||||
// pixels
|
||||
this.setMaxHeightToChildHeight();
|
||||
|
||||
// Once the animation is finished, remove the max-height so that the
|
||||
// child component can freely resize itself
|
||||
setTimeout(() => {
|
||||
this.contentOuterElement.style.removeProperty('maxHeight');
|
||||
this.contentOuterElement.style.maxHeight = 'unset';
|
||||
}, 1000 * 0.25);
|
||||
});
|
||||
}
|
||||
@@ -193,7 +189,21 @@ export class RevealerComponent extends ComponentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
// Again, animating from `max-height: unset` doesn't work, so we have to
|
||||
// set it to the child's size in pixels
|
||||
this.setMaxHeightToChildHeight();
|
||||
commitCss(this.contentOuterElement);
|
||||
|
||||
this.element.classList.remove('rio-revealer-open');
|
||||
this.contentOuterElement.style.maxHeight = '0';
|
||||
}
|
||||
|
||||
private setMaxHeightToChildHeight(): void {
|
||||
let contentHeight = this.contentInnerElement.scrollHeight;
|
||||
let selfHeight = this.element.scrollHeight;
|
||||
let headerHeight = this.headerElement.scrollHeight;
|
||||
let targetHeight = Math.max(contentHeight, selfHeight - headerHeight);
|
||||
|
||||
this.contentOuterElement.style.maxHeight = `${targetHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,109 +41,101 @@ export class SwitcherComponent extends ComponentBase {
|
||||
deltaState.content !== undefined &&
|
||||
deltaState.content !== this.state.content
|
||||
) {
|
||||
this.replaceContent(latentComponents, deltaState.content);
|
||||
this.removeCurrentChild(latentComponents);
|
||||
this.addNewChild(deltaState.content, latentComponents);
|
||||
}
|
||||
}
|
||||
|
||||
private replaceContent(
|
||||
latentComponents: Set<ComponentBase>,
|
||||
content: ComponentId | null
|
||||
private removeCurrentChild(latentComponents: Set<ComponentBase>): void {
|
||||
if (this.activeChildContainer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The old component may be used somewhere else in the UI, so
|
||||
// the switcher can't rely on it still being available. To get
|
||||
// around this, create a copy of the element's HTML tree and use
|
||||
// that for the animation.
|
||||
//
|
||||
// Moreover, teh component may have already been removed from
|
||||
// the switcher. This can happen when it was moved into another
|
||||
// component. Thus, fetch the component by its id, rather than
|
||||
// using the contained HTML node.
|
||||
let oldComponent = componentsById[this.state.content!]!;
|
||||
let oldElementClone = oldComponent.element.cloneNode(
|
||||
true
|
||||
) as HTMLElement;
|
||||
|
||||
// Discard the old component
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
null,
|
||||
this.activeChildContainer
|
||||
);
|
||||
|
||||
// Animate out the old component
|
||||
this.activeChildContainer.appendChild(oldElementClone);
|
||||
this.activeChildContainer.classList.remove('rio-switcher-active-child');
|
||||
this.activeChildContainer.style.maxWidth = '0';
|
||||
this.activeChildContainer.style.maxHeight = '0';
|
||||
|
||||
// Make sure to remove the child after the animation finishes
|
||||
let oldChildContainer = this.activeChildContainer;
|
||||
|
||||
setTimeout(() => {
|
||||
oldChildContainer.remove();
|
||||
}, this.state.transition_time * 1000);
|
||||
|
||||
// No more children :(
|
||||
this.activeChildContainer = null;
|
||||
}
|
||||
|
||||
private addNewChild(
|
||||
content: ComponentId | null,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
// Out with the old
|
||||
if (this.activeChildContainer !== null) {
|
||||
// The old component may be used somewhere else in the UI, so
|
||||
// the switcher can't rely on it still being available. To get
|
||||
// around this, create a copy of the element's HTML tree and use
|
||||
// that for the animation.
|
||||
//
|
||||
// Moreover, teh component may have already been removed from
|
||||
// the switcher. This can happen when it was moved into another
|
||||
// component. Thus, fetch the component by its id, rather than
|
||||
// using the contained HTML node.
|
||||
let oldComponent = componentsById[this.state.content!]!;
|
||||
let oldElementClone = oldComponent.element.cloneNode(
|
||||
true
|
||||
) as HTMLElement;
|
||||
|
||||
// Discard the old component
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
null,
|
||||
this.activeChildContainer
|
||||
);
|
||||
|
||||
// Animate out the old component
|
||||
this.activeChildContainer.appendChild(oldElementClone);
|
||||
this.activeChildContainer.classList.remove(
|
||||
'rio-switcher-active-child'
|
||||
);
|
||||
this.activeChildContainer.style.maxWidth = '0';
|
||||
this.activeChildContainer.style.maxHeight = '0';
|
||||
|
||||
// Make sure to remove the child after the animation finishes
|
||||
let oldChildContainer = this.activeChildContainer;
|
||||
|
||||
setTimeout(() => {
|
||||
oldChildContainer.remove();
|
||||
}, this.state.transition_time * 1000);
|
||||
|
||||
// No more children :(
|
||||
this.activeChildContainer = null;
|
||||
}
|
||||
|
||||
// In with the new
|
||||
if (content === null) {
|
||||
this.activeChildContainer = null;
|
||||
} else {
|
||||
// Add the child into a helper container
|
||||
this.activeChildContainer = document.createElement('div');
|
||||
this.activeChildContainer.style.maxWidth = '0';
|
||||
this.activeChildContainer.style.maxHeight = '0';
|
||||
this.element.appendChild(this.activeChildContainer);
|
||||
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
content,
|
||||
this.activeChildContainer
|
||||
);
|
||||
|
||||
// Animate the child in
|
||||
commitCss(this.activeChildContainer);
|
||||
this.activeChildContainer.classList.add(
|
||||
'rio-switcher-active-child'
|
||||
);
|
||||
|
||||
// The components may currently be in flux due to a pending re-layout.
|
||||
// If that is the case, reading the `scrollHeight` would lead to an
|
||||
// incorrect value. Wait for the resize to finish before fetching it.
|
||||
requestAnimationFrame(() => {
|
||||
if (this.activeChildContainer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let contentWidth = this.activeChildContainer.scrollWidth;
|
||||
let selfWidth = this.element.scrollWidth;
|
||||
let targetWidth = Math.max(contentWidth, selfWidth);
|
||||
|
||||
let contentHeight = this.activeChildContainer.scrollHeight;
|
||||
let selfHeight = this.element.scrollHeight;
|
||||
let targetHeight = Math.max(contentHeight, selfHeight);
|
||||
|
||||
this.activeChildContainer.style.maxWidth = `${targetWidth}px`;
|
||||
this.activeChildContainer.style.maxHeight = `${targetHeight}px`;
|
||||
|
||||
// Once the animation is finished, remove the
|
||||
// max-width/max-height so that the child component can freely
|
||||
// resize itself
|
||||
setTimeout(() => {
|
||||
if (this.activeChildContainer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeChildContainer.style.removeProperty('maxWidth');
|
||||
this.activeChildContainer.style.removeProperty('maxHeight');
|
||||
}, 1000 * this.state.transition_time);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the child into a helper container
|
||||
this.activeChildContainer = document.createElement('div');
|
||||
this.activeChildContainer.style.maxWidth = '0';
|
||||
this.activeChildContainer.style.maxHeight = '0';
|
||||
this.element.appendChild(this.activeChildContainer);
|
||||
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
content,
|
||||
this.activeChildContainer
|
||||
);
|
||||
|
||||
// Animate the child in
|
||||
commitCss(this.activeChildContainer);
|
||||
this.activeChildContainer.classList.add('rio-switcher-active-child');
|
||||
|
||||
// The components may currently be in flux due to a pending re-layout.
|
||||
// If that is the case, reading the `scrollHeight` would lead to an
|
||||
// incorrect value. Wait for the resize to finish before fetching it.
|
||||
let activeChildContainer = this.activeChildContainer;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
let contentWidth = activeChildContainer.scrollWidth;
|
||||
let selfWidth = this.element.scrollWidth;
|
||||
let targetWidth = Math.max(contentWidth, selfWidth);
|
||||
|
||||
let contentHeight = activeChildContainer.scrollHeight;
|
||||
let selfHeight = this.element.scrollHeight;
|
||||
let targetHeight = Math.max(contentHeight, selfHeight);
|
||||
|
||||
activeChildContainer.style.maxWidth = `${targetWidth}px`;
|
||||
activeChildContainer.style.maxHeight = `${targetHeight}px`;
|
||||
|
||||
// Once the animation is finished, remove the max-width/max-height
|
||||
// so that the child component can freely resize itself
|
||||
setTimeout(() => {
|
||||
activeChildContainer.style.removeProperty('max-width');
|
||||
activeChildContainer.style.removeProperty('max-height');
|
||||
}, this.state.transition_time * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,11 +1001,9 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
}
|
||||
|
||||
.rio-revealer-content-outer {
|
||||
overflow: hidden;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
transition: max-height 0.25s ease-in-out;
|
||||
}
|
||||
@@ -2575,6 +2573,8 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
}
|
||||
|
||||
.rio-switcher > * {
|
||||
@include single-container();
|
||||
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user