All files / splittable splittable.tsx

32.14% Statements 18/56
36.36% Branches 8/22
26.67% Functions 4/15
32.61% Lines 15/46
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107  1x 1x   1x                         1x 11x 11x 4x 11x   4x 3x 3x 3x 3x     4x     1x                                                                                                                                                  
import { Func } from '@fuselab/ui-shared/lib/typeHelpers';
import * as React from 'react';
import * as _ from 'underscore';
/* tslint:enable:no-use-before-declare */
import classNames from './splittable.classNames';
 
export interface SplittableAttributes {
  orientation: 'horizontal' | 'vertical';
  parts: Func<JSX.Element>[];
}
 
export interface SplittableActions {
  onLayout(partitions: number[]);
}
 
export type SplittableProps = SplittableAttributes & SplittableActions;
 
export function layoutStack(max: number, parts: number[], index: number, delta: number): number[] {
  const total = parts.reduce((s, c) => s + c, 0);
  const unassignedCount = parts.reduce((s, c) => c ? s : s + 1, 0);
  const unassignedAmount = Math.round(max - total) / unassignedCount;
  const cur = parts.map(x => x || unassignedAmount);
 
  if (delta) {
    const prev = cur[index];
    const next = cur[index + 1];
    const effectiveDelta = delta > 0 ? Math.min(next, delta) : Math.max(-prev, delta);
    cur.splice(index, 2, prev + effectiveDelta, next - effectiveDelta);
  }
 
  return cur;
}
 
export const Splittable = (props: SplittableProps) => {
  const [partitions, setState] = React.useState<number[]>([]);
  const classes = classNames(props.orientation);
  const getSize = (index: number) => partitions[index] ? `${partitions[index]}px` : 'auto';
  const getStyle = (index: number) => ({ [props.orientation === 'vertical' ? 'width' : 'height']: getSize(index) });
  const rootRef = React.createRef<HTMLDivElement>();
  const dividerRefs = _.range(0, props.parts.length).map(x => React.createRef<HTMLDivElement>());
  const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
  };
 
  // render a part with part container
  const renderPartOnly = (part: Func<JSX.Element>, index: number) => ([(
    <div
      className={classes.part}
      key={`split_part_${index}`}
      style={getStyle(index)}
      onDragOver={onDragOver}
    >
      {part()}
    </div>)]);
 
  React.useEffect(() => {
    const root = rootRef.current;
    if (!partitions.length) {
      const parts = _.range(0, props.parts.length).map(x => 0);
      setState(layoutStack(root.clientWidth, parts, 0, 0));
    }
  });
 
  // render a part with optional  splitter
  const renderPart = (part: Func<JSX.Element>, index: number) => {
    let prevPosition = 0;
    if (index === 0) {
      return renderPartOnly(part, index);
    }
 
    const onStart = (e: React.DragEvent<HTMLDivElement>) => {
      e.dataTransfer.setData('text/plain', `${partitions[index - 1]}`);
      prevPosition = props.orientation === 'vertical' ? e.screenX : e.screenY;
    };
 
    const onDrag = (e: React.DragEvent<HTMLDivElement>) => {
      const cur = props.orientation === 'vertical' ? e.screenX : e.screenY;
      const delta = cur - prevPosition;
      const root = rootRef.current;
      if (delta !== 0) {
        prevPosition = cur;
        const next = layoutStack(root.clientWidth, partitions, index - 1, delta);
        setState(next);
      }
    };
 
    return [(
      <div
        ref={dividerRefs[index]}
        className={classes.divider}
        key={`split_divider_${index}`}
        draggable={true}
        onDragStart={onStart}
        onDrag={null}
        onDragEnd={onDrag}
      />),
    ...renderPartOnly(part, index)
    ];
  };
 
  return (
    <div className={classes.root} ref={rootRef}>
      {props.parts.map(renderPart)}
    </div>
  );
};