floaty
Version:
Floatable dockable panels in React
143 lines (136 loc) • 5.41 kB
text/typescript
import * as ReactManagedDragable from 'react-managed-draggable';
import * as DropModel from './drop-model';
import * as StateModel from './state-model';
type TraverseOrder = 'pre-order' | 'post-order';
export class StaticUtilities {
public static traverseLayoutTree<T>(layout: StateModel.Layout<T> | null, callback: (node: StateModel.Layout<T>) => void, order: TraverseOrder) {
if (layout === null) {
return;
}
if (order === 'pre-order') {
callback(layout);
}
if (layout.type !== 'stack') {
for (const item of layout.items) {
StaticUtilities.traverseLayoutTree(item.child, callback, order);
}
}
if (order === 'post-order') {
callback(layout);
}
}
public static minimizeLayout<T>(layout: StateModel.Layout<T> | null): StateModel.Layout<T> | null {
if (layout === null) {
return null;
}
switch (layout.type) {
case 'column':
case 'row': {
const type = layout.type;
const items: StateModel.ColumnOrRowItem<T>[] = [];
let changed = false;
for (const item of layout.items) {
const result = this.minimizeLayout(item.child);
if (result !== item.child) {
changed = true;
}
if (result) {
if (result.type === type) {
const childSumFractions = result.items.map((item) => item.fraction).reduce((sum, fraction) => sum + fraction);
const normalizedChildFractions = result.items.map((item) => item.fraction / childSumFractions);
for (let i = 0; i < result.items.length; i++) {
items.push({ ...result.items[i], fraction: normalizedChildFractions[i] * item.fraction });
}
changed = true;
} else {
items.push({ ...item, child: result });
}
}
}
if (items.length >= 2) {
if (changed) {
// Normalize fractions.
const fractionSum = items.reduce((sum, item) => sum + item.fraction, 0);
const normalizedItems = items.map((item) => {
return {
...item,
fraction: item.fraction / fractionSum
};
});
return { ...layout, items: normalizedItems };
} else {
return layout;
}
} else if (items.length === 1) {
return items[0].child;
} else {
return null;
}
}
case 'stack': {
if (layout.items.length >= 1) {
return layout;
} else {
return null;
}
}
}
}
public static findStackItem<T>(predicate: (item: T) => boolean, layout: StateModel.Layout<T> | null): StateModel.StackItem<T> | null {
if (layout === null) {
return null;
}
if (layout.type === 'stack') {
for (const item of layout.items) {
if (predicate(item.item)) {
return item;
}
}
} else {
for (const item of layout.items) {
const found = StaticUtilities.findStackItem(predicate, item.child);
if (found) {
return found;
}
}
}
return null;
}
public static findStack<T>(stackItem: StateModel.StackItem<T>, from: StateModel.Layout<T> | null): StateModel.Stack<T> | null {
if (from === null) {
return null;
}
switch (from.type) {
case 'column':
case 'row': {
for (const item of from.items) {
const found = StaticUtilities.findStack(stackItem, item.child);
if (found) {
return found;
}
}
break;
}
case 'stack': {
for (const item of from.items) {
if (stackItem === item) {
return from;
}
}
break;
}
}
return null;
}
public static getContentResolutionSide(resolution: DropModel.DropResolutionContainer<unknown>, mousePosition: ReactManagedDragable.XY): 'left' | 'right' | 'top' | 'bottom' {
if (mousePosition.x <= resolution.dropArea.left + resolution.dropArea.width * 0.2) {
return 'left';
} else if (mousePosition.x >= resolution.dropArea.left + resolution.dropArea.width * 0.8) {
return 'right';
} else if (mousePosition.y <= resolution.dropArea.top + resolution.dropArea.height * 0.5) {
return 'top';
} else {
return 'bottom';
}
}
}