diff --git a/projects/angular-split/src/lib/angular-split-config.token.ts b/projects/angular-split/src/lib/angular-split-config.token.ts index fb4a831..905cba4 100644 --- a/projects/angular-split/src/lib/angular-split-config.token.ts +++ b/projects/angular-split/src/lib/angular-split-config.token.ts @@ -1,5 +1,6 @@ -import { InjectionToken, Provider, inject } from '@angular/core' +import { InjectionToken, type Provider, type Type, inject } from '@angular/core' import type { SplitDir, SplitDirection, SplitUnit } from './models' +import type { SplitGutterComponent } from './gutter/split-gutter-component' export interface AngularSplitDefaultOptions { dir: SplitDir @@ -12,6 +13,7 @@ export interface AngularSplitDefaultOptions { restrictMove: boolean unit: SplitUnit useTransition: boolean + gutterComponent: Type | undefined } const defaultOptions: AngularSplitDefaultOptions = { @@ -25,6 +27,7 @@ const defaultOptions: AngularSplitDefaultOptions = { restrictMove: false, unit: 'percent', useTransition: false, + gutterComponent: undefined, } export const ANGULAR_SPLIT_DEFAULT_OPTIONS = new InjectionToken( diff --git a/projects/angular-split/src/lib/gutter/split-gutter-component.ts b/projects/angular-split/src/lib/gutter/split-gutter-component.ts new file mode 100644 index 0000000..ea9fcaa --- /dev/null +++ b/projects/angular-split/src/lib/gutter/split-gutter-component.ts @@ -0,0 +1,6 @@ +import type { InputSignal } from '@angular/core' +import type { SplitGutterContext } from './split-gutter-context' + +export interface SplitGutterComponent { + context: InputSignal +} diff --git a/projects/angular-split/src/lib/gutter/split-gutter-context.ts b/projects/angular-split/src/lib/gutter/split-gutter-context.ts new file mode 100644 index 0000000..cd35b78 --- /dev/null +++ b/projects/angular-split/src/lib/gutter/split-gutter-context.ts @@ -0,0 +1,33 @@ +import type { SplitAreaComponent } from '../split-area/split-area.component' + +export interface SplitGutterContext { + /** + * The area before the gutter. + * In RTL the right area and in LTR the left area + */ + areaBefore: SplitAreaComponent + /** + * The area after the gutter. + * In RTL the left area and in LTR the right area + */ + areaAfter: SplitAreaComponent + /** + * The absolute number of the gutter based on direction (RTL and LTR). + * First gutter is 1, second is 2, etc... + */ + gutterNum: number + /** + * Whether this is the first gutter. + * In RTL the most right area and in LTR the most left area + */ + first: boolean + /** + * Whether this is the last gutter. + * In RTL the most left area and in LTR the most right area + */ + last: boolean + /** + * Whether the gutter is being dragged now + */ + isDragged: boolean +} diff --git a/projects/angular-split/src/lib/gutter/split-gutter-drag-handle.directive.ts b/projects/angular-split/src/lib/gutter/split-gutter-drag-handle.directive.ts index 98c37df..c75c54d 100644 --- a/projects/angular-split/src/lib/gutter/split-gutter-drag-handle.directive.ts +++ b/projects/angular-split/src/lib/gutter/split-gutter-drag-handle.directive.ts @@ -1,20 +1,45 @@ -import { Directive, OnDestroy, ElementRef, inject } from '@angular/core' -import { SplitGutterDirective } from './split-gutter.directive' +import { Directive, OnDestroy, ElementRef, inject, computed, input, booleanAttribute } from '@angular/core' import { GUTTER_NUM_TOKEN } from './gutter-num-token' +import { SplitGuttersManagerService } from './split-gutters-manager.service' +import { SplitComponent } from '../split/split.component' +import { assertUnreachable } from '../utils' @Directive({ selector: '[asSplitGutterDragHandle]', + host: { + '[style.cursor]': `cursor()`, + }, }) export class SplitGutterDragHandleDirective implements OnDestroy { private readonly gutterNum = inject(GUTTER_NUM_TOKEN) private readonly elementRef = inject>(ElementRef) - private readonly gutterDir = inject(SplitGutterDirective) + private readonly guttersManager = inject(SplitGuttersManagerService) + private readonly splitComponent = inject(SplitComponent) + + readonly suppressDefaultCursor = input(false, { transform: booleanAttribute }) + + protected readonly cursor = computed(() => { + if (this.suppressDefaultCursor()) { + return undefined + } + + const direction = this.splitComponent.direction() + + switch (direction) { + case 'horizontal': + return 'col-resize' + case 'vertical': + return 'row-resize' + default: + return assertUnreachable(direction, 'SplitDirection') + } + }) constructor() { - this.gutterDir._addToMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addDragHandle(this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.gutterDir._removedFromMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef) + this.guttersManager.removeDragHandle(this.gutterNum, this.elementRef) } } diff --git a/projects/angular-split/src/lib/gutter/split-gutter-exclude-from-drag.directive.ts b/projects/angular-split/src/lib/gutter/split-gutter-exclude-from-drag.directive.ts index cbc8f0d..5191392 100644 --- a/projects/angular-split/src/lib/gutter/split-gutter-exclude-from-drag.directive.ts +++ b/projects/angular-split/src/lib/gutter/split-gutter-exclude-from-drag.directive.ts @@ -1,20 +1,27 @@ -import { Directive, OnDestroy, ElementRef, inject } from '@angular/core' -import { SplitGutterDirective } from './split-gutter.directive' +import { Directive, OnDestroy, ElementRef, inject, input, booleanAttribute, computed } from '@angular/core' import { GUTTER_NUM_TOKEN } from './gutter-num-token' +import { SplitGuttersManagerService } from './split-gutters-manager.service' @Directive({ selector: '[asSplitGutterExcludeFromDrag]', + host: { + '[style.cursor]': `cursor()`, + }, }) export class SplitGutterExcludeFromDragDirective implements OnDestroy { private readonly gutterNum = inject(GUTTER_NUM_TOKEN) private readonly elementRef = inject>(ElementRef) - private readonly gutterDir = inject(SplitGutterDirective) + private readonly guttersManager = inject(SplitGuttersManagerService) + + readonly suppressDefaultCursor = input(false, { transform: booleanAttribute }) + + cursor = computed(() => (this.suppressDefaultCursor() ? undefined : 'default')) constructor() { - this.gutterDir._addToMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addExcludeDrag(this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.gutterDir._removedFromMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) + this.guttersManager.removeExcludeDrag(this.gutterNum, this.elementRef) } } diff --git a/projects/angular-split/src/lib/gutter/split-gutter.directive.ts b/projects/angular-split/src/lib/gutter/split-gutter.directive.ts index 534bb7c..d2cf26e 100644 --- a/projects/angular-split/src/lib/gutter/split-gutter.directive.ts +++ b/projects/angular-split/src/lib/gutter/split-gutter.directive.ts @@ -1,37 +1,7 @@ -import { Directive, ElementRef, inject, TemplateRef } from '@angular/core' -import type { SplitAreaComponent } from '../split-area/split-area.component' +import { Directive, inject, TemplateRef } from '@angular/core' +import type { SplitGutterContext } from './split-gutter-context' -export interface SplitGutterTemplateContext { - /** - * The area before the gutter. - * In RTL the right area and in LTR the left area - */ - areaBefore: SplitAreaComponent - /** - * The area after the gutter. - * In RTL the left area and in LTR the right area - */ - areaAfter: SplitAreaComponent - /** - * The absolute number of the gutter based on direction (RTL and LTR). - * First gutter is 1, second is 2, etc... - */ - gutterNum: number - /** - * Whether this is the first gutter. - * In RTL the most right area and in LTR the most left area - */ - first: boolean - /** - * Whether this is the last gutter. - * In RTL the most left area and in LTR the most right area - */ - last: boolean - /** - * Whether the gutter is being dragged now - */ - isDragged: boolean -} +export type SplitGutterTemplateContext = SplitGutterContext @Directive({ selector: '[asSplitGutter]', @@ -39,67 +9,6 @@ export interface SplitGutterTemplateContext { export class SplitGutterDirective { readonly template = inject>(TemplateRef) - /** - * The map holds reference to the drag handle elements inside instances - * of the provided template. - * - * @internal - */ - readonly _gutterToHandleElementMap = new Map[]>() - /** - * The map holds reference to the excluded drag elements inside instances - * of the provided template. - * - * @internal - */ - readonly _gutterToExcludeDragElementMap = new Map[]>() - - /** - * @internal - */ - _canStartDragging(originElement: HTMLElement, gutterNum: number) { - if (this._gutterToExcludeDragElementMap.has(gutterNum)) { - const isInsideExclude = this._gutterToExcludeDragElementMap - .get(gutterNum) - .some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement)) - - if (isInsideExclude) { - return false - } - } - - if (this._gutterToHandleElementMap.has(gutterNum)) { - return this._gutterToHandleElementMap - .get(gutterNum) - .some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement)) - } - - return true - } - - /** - * @internal - */ - _addToMap(map: Map[]>, gutterNum: number, elementRef: ElementRef) { - if (map.has(gutterNum)) { - map.get(gutterNum).push(elementRef) - } else { - map.set(gutterNum, [elementRef]) - } - } - - /** - * @internal - */ - _removedFromMap(map: Map[]>, gutterNum: number, elementRef: ElementRef) { - const elements = map.get(gutterNum) - elements.splice(elements.indexOf(elementRef), 1) - - if (elements.length === 0) { - map.delete(gutterNum) - } - } - static ngTemplateContextGuard(_dir: SplitGutterDirective, ctx: unknown): ctx is SplitGutterTemplateContext { return true } diff --git a/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts b/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts new file mode 100644 index 0000000..aaa33f4 --- /dev/null +++ b/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts @@ -0,0 +1,92 @@ +import { ElementRef, Injectable, signal } from '@angular/core' + +@Injectable() +export class SplitGuttersManagerService { + /** + * The map holds reference to the drag handle elements inside instances + * of the provided template. + */ + private readonly gutterToHandleElementMap = signal(new Map[]>(), { + equal: () => false, + }) + /** + * The map holds reference to the excluded drag elements inside instances + * of the provided template. + */ + private readonly gutterToExcludeDragElementMap = signal(new Map[]>(), { + equal: () => false, + }) + + canStartDragging(originElement: HTMLElement, gutterNum: number) { + if (this.gutterToExcludeDragElementMap().has(gutterNum)) { + const isInsideExclude = this.gutterToExcludeDragElementMap() + .get(gutterNum) + .some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement)) + + if (isInsideExclude) { + return false + } + } + + if (this.gutterToHandleElementMap().has(gutterNum)) { + return this.gutterToHandleElementMap() + .get(gutterNum) + .some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement)) + } + + return true + } + + hasDragHandles(gutterNum: number) { + return this.gutterToHandleElementMap().has(gutterNum) + } + + addDragHandle(gutterNum: number, elementRef: ElementRef) { + const map = this.gutterToHandleElementMap() + this.addToMap(map, gutterNum, elementRef) + this.gutterToHandleElementMap.set(map) + } + + removeDragHandle(gutterNum: number, elementRef: ElementRef) { + const map = this.gutterToHandleElementMap() + this.removeFromMap(map, gutterNum, elementRef) + this.gutterToHandleElementMap.set(map) + } + + addExcludeDrag(gutterNum: number, elementRef: ElementRef) { + const map = this.gutterToExcludeDragElementMap() + this.addToMap(map, gutterNum, elementRef) + this.gutterToExcludeDragElementMap.set(map) + } + + removeExcludeDrag(gutterNum: number, elementRef: ElementRef) { + const map = this.gutterToExcludeDragElementMap() + this.removeFromMap(map, gutterNum, elementRef) + this.gutterToExcludeDragElementMap.set(map) + } + + private addToMap( + map: Map[]>, + gutterNum: number, + elementRef: ElementRef, + ) { + if (map.has(gutterNum)) { + map.get(gutterNum).push(elementRef) + } else { + map.set(gutterNum, [elementRef]) + } + } + + private removeFromMap( + map: Map[]>, + gutterNum: number, + elementRef: ElementRef, + ) { + const elements = map.get(gutterNum) + elements.splice(elements.indexOf(elementRef), 1) + + if (elements.length === 0) { + map.delete(gutterNum) + } + } +} diff --git a/projects/angular-split/src/lib/split/split.component.css b/projects/angular-split/src/lib/split/split.component.css index b0ef82a..a77a597 100644 --- a/projects/angular-split/src/lib/split/split.component.css +++ b/projects/angular-split/src/lib/split/split.component.css @@ -37,13 +37,19 @@ touch-action: none; :host(.as-horizontal) > & { - cursor: col-resize; height: 100%; + + &:not(.with-handles) { + cursor: col-resize; + } } :host(.as-vertical) > & { - cursor: row-resize; width: 100%; + + &:not(.with-handles) { + cursor: row-resize; + } } :host(.as-disabled) > & { diff --git a/projects/angular-split/src/lib/split/split.component.html b/projects/angular-split/src/lib/split/split.component.html index cddba06..068285b 100644 --- a/projects/angular-split/src/lib/split/split.component.html +++ b/projects/angular-split/src/lib/split/split.component.html @@ -1,9 +1,11 @@ @for (area of _areas(); track area) { @if (!$last) { + @let gutterNum = $index + 1;