@@ -43,6 +42,7 @@ import { debounce } from 'lodash-es';
[ngClass]="[this.direction | lowercase]"
[ngStyle]="this.splitterSizeStyle"
(mousedown)="this.onGutterMouseDown($event, index)"
+ (mouseup)="this.onGutterMouseUp($event)"
>
@@ -57,7 +57,7 @@ export class SplitterComponent implements OnChanges, AfterContentInit {
public readonly direction?: SplitterDirection = SplitterDirection.Horizontal;
@Input()
- public readonly debounceTime: number = 20;
+ public readonly debounceTime: number = 12;
@Input()
public readonly splitterSize: number = 16;
@@ -69,16 +69,24 @@ export class SplitterComponent implements OnChanges, AfterContentInit {
private readonly contents!: QueryList
;
protected contents$!: Observable;
+ protected classes: string[] = [];
protected splitterSizeStyle?: Partial;
+
private mouseMoveListener?: () => void;
private mouseUpListener?: () => void;
- private size?: number;
- private resizeColumnSize?: number;
- private currentSplitterElement?: HTMLElement;
- private startPos?: number;
- private previous?: ContentWithMetadata;
- private next?: ContentWithMetadata;
+ private normalizationParameters: NormalizationParameters = {
+ itemCount: 0,
+ pxPerItem: 0
+ };
+
+ private resizeStartParameters: ResizeStartParameters = {
+ startPositionPx: 0,
+ prevContentStartSizePx: 0,
+ nextContentStartSizePx: 0
+ };
+
+ private readonly debounceResize = debounce(this.resize, this.debounceTime);
public constructor(
@Inject(DOCUMENT) private readonly document: Document,
@@ -87,213 +95,165 @@ export class SplitterComponent implements OnChanges, AfterContentInit {
private readonly renderer: Renderer2
) {}
- public ngAfterContentInit(): void {
- this.contents$ = queryListAndChanges$(this.contents ?? EMPTY).pipe(map(contents => contents.toArray()));
- }
-
public ngOnChanges(changes: TypedSimpleChanges): void {
if (changes.splitterSize || changes.direction) {
this.setSplitterSizeStyle();
+ this.classes = this.buildClasses();
}
}
- protected onGutterMouseDown(event: MouseEvent, index: number) {
- this.resizeStart(event, index);
- this.bindMouseListeners();
+ public ngAfterContentInit(): void {
+ this.subscribeToQueryListChanges();
}
- protected readonly getFlex = (dimension: SplitterCellDimension): string => {
- if (dimension.unit === 'PX') {
- return `1 1 ${dimension.value}${dimension.unit.toLowerCase()}`;
- } else {
- return `${dimension.value} ${dimension.value} 0`;
- }
- };
-
- protected readonly getMaxHeight = (dimension: SplitterCellDimension): string | undefined => {
- if (dimension.unit === 'PX') {
- return `${dimension.value}${dimension.unit.toLowerCase()}`;
- }
- };
+ private buildNormalizationParams(contents: SplitterContentDirective[]): NormalizationParameters {
+ const totalAvailablePx = this.getElementSizePx(this.element.nativeElement);
+ const splitterPx = this.splitterSize * (contents.length - 1);
+ const pxPerItem = (totalAvailablePx - splitterPx) / contents.length;
- protected horizontal() {
- return this.direction === SplitterDirection.Horizontal;
+ return {
+ itemCount: contents.length,
+ pxPerItem: pxPerItem
+ };
}
- private setSplitterSizeStyle(): void {
- if (this.direction === SplitterDirection.Vertical) {
- this.splitterSizeStyle = {
- height: `${this.splitterSize}px`
- };
- }
+ private normalizeContentsDimensions(contents: SplitterContentDirective[]): SplitterContentDirective[] {
+ this.normalizationParameters = this.buildNormalizationParams(contents);
- if (this.direction === SplitterDirection.Horizontal) {
- this.splitterSizeStyle = {
- width: `${this.splitterSize}px`
- };
- }
+ return contents.map(content => this.normalizeContentDimension(content));
}
- private bindMouseListeners() {
- if (!this.mouseMoveListener) {
- this.mouseMoveListener = this.renderer.listen(this.document, 'mousemove', event => {
- this.debouncedOnResize(event);
- });
+ private normalizeContentDimension(content: SplitterContentDirective): SplitterContentDirective {
+ if (content.dimension.unit === 'FR') {
+ content.dimension = {
+ unit: 'PX',
+ value: content.dimension.value * this.normalizationParameters.pxPerItem
+ };
}
- if (!this.mouseUpListener) {
- this.mouseUpListener = this.renderer.listen(this.document, 'mouseup', () => {
- this.resizeEnd();
- this.unbindMouseListeners();
- });
- }
+ return content;
}
- private unbindMouseListeners() {
- this.mouseMoveListener?.();
- this.mouseMoveListener = undefined;
-
- this.mouseUpListener?.();
- this.mouseUpListener = undefined;
+ protected onGutterMouseDown(event: MouseEvent, index: number) {
+ this.resizeStart(event, index);
+ this.bindMouseListeners();
}
- private resizeStart(event: MouseEvent, index: number) {
- this.currentSplitterElement = event.currentTarget as HTMLElement;
- this.size = this.getElementSize(this.element.nativeElement);
- this.resizeColumnSize = this.horizontal() ? this.size / 12 : 1;
-
- this.startPos = this.horizontal() ? event.pageX : event.pageY;
-
- this.previous = this.buildPreviousContentWithMetaData(index, this.currentSplitterElement);
- this.next = this.buildNextContentWithMetaData(index, this.currentSplitterElement);
+ protected onGutterMouseUp(event: MouseEvent) {
+ this.resize(event);
+ this.unbindMouseListeners();
}
- private readonly debouncedOnResize: (event: MouseEvent) => void = debounce(this.onResize, 0);
-
- private onResize(event: MouseEvent): void {
- let newPos;
-
- if (
- this.size !== undefined &&
- this.startPos !== undefined &&
- this.resizeColumnSize !== undefined &&
- this.previous !== undefined
- ) {
- let newPrevPanelSize = 0;
- let newNextPanelSize = 0;
-
- if (this.horizontal()) {
- newPos = event.pageX - this.startPos;
- newPrevPanelSize = this.previous.size + newPos;
- if (this.next) {
- newNextPanelSize = this.next.size - newPos;
- }
- } else {
- newPos = event.pageY - this.startPos;
- newPrevPanelSize = this.previous.size + newPos;
- if (this.next) {
- newNextPanelSize = this.next.size; // Let the top container grow.
- }
- }
-
- if (this.validateResize(newPrevPanelSize, newNextPanelSize) && this.previous !== undefined) {
- this.previous.content.dimension = { value: newPrevPanelSize, unit: 'PX' };
-
- if (this.next) {
- this.next.content.dimension = { value: newNextPanelSize, unit: 'PX' };
- }
-
- this.setStyle();
- }
- }
+ private resizeStart(startEvent: MouseEvent, index: number): void {
+ this.resizeStartParameters = this.buildResizeStartParameters(index, startEvent);
}
- private resizeEnd() {
- if (this.previous !== undefined && this.resizeColumnSize !== undefined && this.size !== undefined) {
- if (this.horizontal()) {
- this.previous.size = this.getElementSize(this.previous.element);
-
- let previousPanelColumnWidth = Math.round(this.previous.size / this.resizeColumnSize);
- this.previous.content.dimension = { value: previousPanelColumnWidth, unit: 'FR' };
+ private resize(endEvent: MouseEvent): void {
+ const prev = this.resizeStartParameters.prevContent;
+ const next = this.resizeStartParameters.nextContent;
- if (this.next) {
- this.next.size = this.getElementSize(this.next.element);
+ const positionDiffPx = this.getClientPx(endEvent) - this.resizeStartParameters.startPositionPx;
- let nextPanelColumnWidth = Math.round(
- (this.next.size - (previousPanelColumnWidth * this.resizeColumnSize - this.previous.size)) /
- this.resizeColumnSize
- );
- this.next.content.dimension = { value: nextPanelColumnWidth, unit: 'FR' };
- }
+ if (prev) {
+ prev.dimension.value = this.resizeStartParameters.prevContentStartSizePx + positionDiffPx;
+ }
- this.setStyle();
- }
+ if (next) {
+ next.dimension.value = this.resizeStartParameters.nextContentStartSizePx - positionDiffPx;
}
this.layoutChange.emit(this.contents.map(c => c.dimension));
this.layoutChangeService.publishLayoutChange();
- this.clear();
}
- private buildPreviousContentWithMetaData(index: number, splitterElement: HTMLElement): ContentWithMetadata {
- const element = splitterElement.previousElementSibling as HTMLElement;
+ private buildResizeStartParameters(index: number, startEvent: MouseEvent): ResizeStartParameters {
+ const prevContent = this.contents.get(index);
+ const nextContent = this.contents.get(index + 1);
return {
- content: this.contents.get(index)!,
- index: index,
- element: element,
- size: this.getElementSize(element)
+ startPositionPx: this.getClientPx(startEvent),
+ prevContentStartSizePx: prevContent?.dimension.value ?? 0,
+ nextContentStartSizePx: nextContent?.dimension.value ?? 0,
+ prevContent: prevContent,
+ nextContent: nextContent
};
}
- private buildNextContentWithMetaData(index: number, splitterElement: HTMLElement): ContentWithMetadata | undefined {
- const element = splitterElement.nextElementSibling as HTMLElement | undefined;
+ private subscribeToQueryListChanges(): void {
+ this.contents$ = queryListAndChanges$(this.contents ?? EMPTY).pipe(
+ map(contents => contents.toArray()),
+ map(contents => this.normalizeContentsDimensions(contents))
+ );
+ }
- if (!element) {
- return undefined;
+ private bindMouseListeners() {
+ if (!this.mouseMoveListener) {
+ this.mouseMoveListener = this.renderer.listen(this.document, 'mousemove', event => this.debounceResize(event));
}
- return {
- content: this.contents.get(index + 1)!,
- index: index,
- element: element,
- size: this.getElementSize(element)
- };
+ if (!this.mouseUpListener) {
+ this.mouseUpListener = this.renderer.listen(this.document, 'mouseup', event => {
+ this.resize(event);
+ this.unbindMouseListeners();
+ });
+ }
+ }
+
+ private unbindMouseListeners() {
+ this.mouseMoveListener?.();
+ this.mouseMoveListener = undefined;
+
+ this.mouseUpListener?.();
+ this.mouseUpListener = undefined;
}
- private getElementSize(element: HTMLElement): number {
- return this.horizontal() ? element.getBoundingClientRect().width : element.getBoundingClientRect().height;
+ private getClientPx(event: MouseEvent): number {
+ return this.isHorizontal() ? event.clientX : event.clientY;
}
- private validateResize(_newPrevPanelSize: number, _newNextPanelSize?: number): boolean {
- /**
- * Stub method to validate resize. For now, returning true
- */
- return true;
+ private getElementSizePx(element: HTMLElement): number {
+ return this.isHorizontal() ? element.getBoundingClientRect().width : element.getBoundingClientRect().height;
}
- private setStyle(): void {
- if (this.previous !== undefined) {
- this.renderer.setStyle(this.previous.element, 'flex', this.getFlex(this.previous.content.dimension));
+ private isHorizontal() {
+ return this.direction === SplitterDirection.Horizontal;
+ }
- if (this.next) {
- this.renderer.setStyle(this.next.element, 'flex', this.getFlex(this.next.content.dimension));
- }
+ private setSplitterSizeStyle(): void {
+ switch (this.direction) {
+ case SplitterDirection.Horizontal:
+ this.splitterSizeStyle = {
+ width: `${this.splitterSize}px`
+ };
+ break;
+ case SplitterDirection.Vertical:
+ this.splitterSizeStyle = {
+ height: `${this.splitterSize}px`
+ };
+ break;
+ case undefined:
+ break;
+ default:
+ assertUnreachable(this.direction);
}
}
- private clear() {
- this.size = undefined;
- this.startPos = undefined;
- this.previous = undefined;
- this.next = undefined;
- this.currentSplitterElement = undefined;
+ private buildClasses(): string[] {
+ return [this.direction?.toLowerCase() ?? ''].filter(c => !isEmpty(c));
}
+
+ protected readonly buildFlex = (pixels: number): string => `1 1 ${pixels}px`;
+}
+
+interface NormalizationParameters {
+ itemCount: number;
+ pxPerItem: number;
}
-interface ContentWithMetadata {
- content: SplitterContentDirective;
- index: number;
- element: HTMLElement;
- size: number;
+interface ResizeStartParameters {
+ startPositionPx: number;
+ prevContentStartSizePx: number;
+ nextContentStartSizePx: number;
+ prevContent?: SplitterContentDirective;
+ nextContent?: SplitterContentDirective;
}