From c0eaf830cd33496baa3764e3d259a79ff6bcdf00 Mon Sep 17 00:00:00 2001 From: Harpush Date: Thu, 2 Apr 2026 21:33:05 +0300 Subject: [PATCH 1/2] feat: support providing global custom gutter component --- .../src/lib/angular-split-config.token.ts | 5 +- .../src/lib/gutter/split-gutter-component.ts | 6 ++ .../src/lib/gutter/split-gutter-context.ts | 33 +++++++ .../split-gutter-drag-handle.directive.ts | 8 +- ...plit-gutter-exclude-from-drag.directive.ts | 12 ++- .../src/lib/gutter/split-gutter.directive.ts | 97 +------------------ .../gutter/split-gutters-manager.service.ts | 52 ++++++++++ .../src/lib/split/split.component.html | 32 +++--- .../src/lib/split/split.component.ts | 24 +++-- projects/angular-split/src/public_api.ts | 2 + .../custom-gutter-style.component.scss | 46 --------- .../custom-gutter-style.component.ts | 8 +- .../custom-gutter.component.ts | 61 ++++++++++++ 13 files changed, 207 insertions(+), 179 deletions(-) create mode 100644 projects/angular-split/src/lib/gutter/split-gutter-component.ts create mode 100644 projects/angular-split/src/lib/gutter/split-gutter-context.ts create mode 100644 projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts create mode 100644 src/app/examples/custom-gutter-style/custom-gutter.component.ts 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 fb4a831f..905cba4b 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 00000000..ea9fcaae --- /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 00000000..cd35b781 --- /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 98c37dfc..06bd7308 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,6 +1,6 @@ import { Directive, OnDestroy, ElementRef, inject } from '@angular/core' -import { SplitGutterDirective } from './split-gutter.directive' import { GUTTER_NUM_TOKEN } from './gutter-num-token' +import { SplitGuttersManagerService } from './split-gutters-manager.service' @Directive({ selector: '[asSplitGutterDragHandle]', @@ -8,13 +8,13 @@ import { GUTTER_NUM_TOKEN } from './gutter-num-token' 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) constructor() { - this.gutterDir._addToMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addToMap(this.guttersManager.gutterToHandleElementMap, this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.gutterDir._removedFromMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef) + this.guttersManager.removeFromMap(this.guttersManager.gutterToHandleElementMap, 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 cbc8f0d2..afb102a2 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,6 +1,6 @@ import { Directive, OnDestroy, ElementRef, inject } from '@angular/core' -import { SplitGutterDirective } from './split-gutter.directive' import { GUTTER_NUM_TOKEN } from './gutter-num-token' +import { SplitGuttersManagerService } from './split-gutters-manager.service' @Directive({ selector: '[asSplitGutterExcludeFromDrag]', @@ -8,13 +8,17 @@ import { GUTTER_NUM_TOKEN } from './gutter-num-token' 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) constructor() { - this.gutterDir._addToMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addToMap(this.guttersManager.gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.gutterDir._removedFromMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) + this.guttersManager.removeFromMap( + this.guttersManager.gutterToExcludeDragElementMap, + 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 534bb7c7..d2cf26e5 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 00000000..0024e3ec --- /dev/null +++ b/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts @@ -0,0 +1,52 @@ +import { ElementRef, Injectable } from '@angular/core' + +@Injectable() +export class SplitGuttersManagerService { + /** + * The map holds reference to the drag handle elements inside instances + * of the provided template. + */ + readonly gutterToHandleElementMap = new Map[]>() + /** + * The map holds reference to the excluded drag elements inside instances + * of the provided template. + */ + readonly gutterToExcludeDragElementMap = new Map[]>() + + 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 + } + + addToMap(map: Map[]>, gutterNum: number, elementRef: ElementRef) { + if (map.has(gutterNum)) { + map.get(gutterNum).push(elementRef) + } else { + map.set(gutterNum, [elementRef]) + } + } + + 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.html b/projects/angular-split/src/lib/split/split.component.html index cddba06b..a3db3738 100644 --- a/projects/angular-split/src/lib/split/split.component.html +++ b/projects/angular-split/src/lib/split/split.component.html @@ -22,22 +22,24 @@ (asSplitCustomMouseDown)="gutterMouseDown($event, gutter, $index, $index, $index + 1)" (asSplitCustomKeyDown)="gutterKeyDown($event, $index, $index, $index + 1)" > - @if (customGutter()?.template) { + @if (customGutter()?.template || defaultOptions.gutterComponent) { + @let context = + { + areaBefore: area, + areaAfter: _areas()[$index + 1], + gutterNum: $index + 1, + first: $first, + last: $index === _areas().length - 2, + isDragged: draggedGutterIndex() === $index, + }; - + @if (customGutter()?.template; as tpl) { + + } @else { + + } } @else {
diff --git a/projects/angular-split/src/lib/split/split.component.ts b/projects/angular-split/src/lib/split/split.component.ts index 945bc628..b3c35523 100644 --- a/projects/angular-split/src/lib/split/split.component.ts +++ b/projects/angular-split/src/lib/split/split.component.ts @@ -1,4 +1,4 @@ -import { NgStyle, NgTemplateOutlet } from '@angular/common' +import { NgComponentOutlet, NgStyle, NgTemplateOutlet } from '@angular/common' import { ChangeDetectionStrategy, Component, @@ -52,6 +52,7 @@ import { toRecord, } from '../utils' import { areAreasValid } from '../validations' +import { SplitGuttersManagerService } from '../gutter/split-gutters-manager.service' interface MouseDownContext { mouseDownEvent: MouseEvent | TouchEvent @@ -79,11 +80,18 @@ export const SPLIT_AREA_CONTRACT = new InjectionToken('Split @Component({ selector: 'as-split', - imports: [NgStyle, SplitCustomEventsBehaviorDirective, SplitGutterDynamicInjectorDirective, NgTemplateOutlet], + imports: [ + NgStyle, + SplitCustomEventsBehaviorDirective, + SplitGutterDynamicInjectorDirective, + NgTemplateOutlet, + NgComponentOutlet, + ], exportAs: 'asSplit', templateUrl: './split.component.html', styleUrl: './split.component.css', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SplitGuttersManagerService], host: { '[class]': 'hostClasses()', '[dir]': 'dir()', @@ -93,7 +101,8 @@ export class SplitComponent { private readonly document = inject(DOCUMENT) private readonly elementRef = inject>(ElementRef) private readonly ngZone = inject(NgZone) - private readonly defaultOptions = inject(ANGULAR_SPLIT_DEFAULT_OPTIONS) + protected readonly defaultOptions = inject(ANGULAR_SPLIT_DEFAULT_OPTIONS) + private readonly guttersManager = inject(SplitGuttersManagerService) private readonly gutterMouseDownSubject = new Subject() private readonly dragProgressSubject = new Subject() @@ -184,13 +193,8 @@ export class SplitComponent { this.gutterMouseDownSubject .pipe( - filter( - (context) => - !this.customGutter() || - this.customGutter()._canStartDragging( - context.mouseDownEvent.target as HTMLElement, - context.gutterIndex + 1, - ), + filter((context) => + this.guttersManager.canStartDragging(context.mouseDownEvent.target as HTMLElement, context.gutterIndex + 1), ), switchMap((mouseDownContext) => // As we have gutterClickDeltaPx we can't just start the drag but need to make sure diff --git a/projects/angular-split/src/public_api.ts b/projects/angular-split/src/public_api.ts index e9612101..e0a8eed7 100644 --- a/projects/angular-split/src/public_api.ts +++ b/projects/angular-split/src/public_api.ts @@ -7,6 +7,8 @@ export { SplitGutterDragHandleDirective } from './lib/gutter/split-gutter-drag-h export { SplitGutterExcludeFromDragDirective } from './lib/gutter/split-gutter-exclude-from-drag.directive' export { SplitGutterDirective } from './lib/gutter/split-gutter.directive' export type { SplitGutterTemplateContext } from './lib/gutter/split-gutter.directive' +export type { SplitGutterComponent } from './lib/gutter/split-gutter-component' +export type { SplitGutterContext } from './lib/gutter/split-gutter-context' export type { SplitAreaSize, SplitAreaSizeInput, diff --git a/src/app/examples/custom-gutter-style/custom-gutter-style.component.scss b/src/app/examples/custom-gutter-style/custom-gutter-style.component.scss index 6c318054..e1017144 100644 --- a/src/app/examples/custom-gutter-style/custom-gutter-style.component.scss +++ b/src/app/examples/custom-gutter-style/custom-gutter-style.component.scss @@ -1,49 +1,3 @@ -// #region example a - -.custom-hand-gutter { - width: 100%; - height: 100%; - display: flex; - background-color: #ff0; - cursor: default; - - .as-horizontal & { - align-items: flex-end; - justify-content: center; - } - - .as-vertical & { - align-items: center; - justify-content: flex-end; - } -} - -.custom-hand-gutter-icon { - opacity: 0.6; - transition: opacity 0.3s; - cursor: grab; - - .as-horizontal & { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAjCAYAAABl/XGVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANkSURBVEiJrddfaJdlFAfwzztda6tdZNo/pZstWlmhSy8siiwhGfQP3IWRIGUEJRgFQXTbZRBeFBhlBv25iFD6s4zCqI0GaWXEhNK5tTCUsJGUWQtOF++zeH7vfnO/3/JcPef7nOd8z3ne5znPeWlCgi3BeHAieDoomlnfDNGy4HBwabAoGAieOReOW4Lu4IIMuyd4LdMXBceCJY36bamXAb7C2/g+eC5YgFPonLYr+DXZ3J/WtQZ9wfrgvEazejHYmsbtwa7g1eDC4OegPbNdGXwdtAXDwVvBm8Fg0DpnZliMsRT9n9iCbmzAPtybZfeNMutnMVawsSgz/QmbGsnsoeD1CrYs+DHYFuytzD0RTAWPZtiGYGcjZK3BgaC/gvcFI+nYL60EEsGTGbYq+CRYF7wQPBWcPxthV8qkq4K/FJwJNmdYWyLbnmGXBKeD74L7gpeDV86WYX+wP2jLsDuS460V26lgT6YXiWxV0tuDk/UOCCjKYz2OuzN4EL+ho2L+Oy6qXe54wYGkT1H/NOayG32Zh7/xkez4JzmN/Zl+PX7J9CsxMRfZXtwZtUG9X4fsD3yW6bfgaKZ3YfSsZKlKjEp7n2RA9h2TnMJQpt+sNtPuOcky5+uzAE5ipGIzVjCZ6V3YQXlY8DDea4RsBFdVsOHpQdriLzL9Guwqyq2lvK/HC4YEPcGHwQdR7nWNBL3BpxWsJRsvqLwOj0/XxTR3KOidnvw42BTcFoxWCYOlwaEGdmDaviMbb47yCv0HTGaRrA2+rCy+Lvi2UbJs3ZJgLOipIasYHQmuyPT+4I15kO0OtlXBo8HFFaPbg4XBAym6m+ZBdiTK56oG3BusyfTH0kcdD3YGy5slSn5ORPXFDrbnVTxhq/Ns50HUGeUDWiML8QOuzsGi9vbPR1aoc4JblIV1Y6TG5RzJGtnFr5H02g4HO2bs8zwk2BOsO5tBWyIbCi7/H0RFlP1kZyPGD6brMKN0NUjWE2Xf2fCCG1N0i+e2nrH2keD5enN1q35RRvYu7mqWDLfi84bJkgxi9TzIenGwWbIJWX/YhFyGY82SjeHaqNOzzybBSkymxqg5SaXsYJQt+fIo+/p8viNYkd6td4KJYO1s/ub8c0wVvx83KJ/8FuXF/wdncFhZmgawr+Cv2Xz9Cy1ParntgE8FAAAAAElFTkSuQmCC') !important; - width: 27px; - height: 35px; - margin-bottom: 5px; - } - - .as-vertical & { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAdCAYAAAAgqdWEAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOxSURBVEiJtddtzNdTGAfwz7+6y91qKpaGF0UjY8JmrIw1sdj0YFMeMzYLJWaUmac1bfQiw0hkkjErTy+whrARm0zztHmYp3mayjyFkny9+J3bfv7uf3e3/p03v3Od872u8z3Xua7rnB9tamFQmB1eDhvC7+G7sCZcHw5u11o9ETkmPBBODgeGjtBZ+qeEZWFjeCKM2Z1ExodN4eKwPBzdAtc/XB6+DbPbsfCwcEI5jqXhvfBLeCacFfrvhI2R4f1wW2jU5xotFPrhMIwt38PLd198ibexHi9hbYM/ermp4ViD1Q2ubgU6LjwafgwJH4cHw5XhxDCsN4v2QGifEuDTmic6wpJC4KewIBzSwkiPR9ELQqeH7/+1ybCoEFka9qqNd4ZTwx0lZW8Pg9tFpqzxdLilSzggbA831gATwqrC+vkwP4xuJ4naWhPD5jBIuDG8HfqGGaW/KkwJA5sURxV822pFaISPwpmNsBq/YgS2YVaDD+tgnIQ5GIXzG7y1iwQGNNhak+9Dp/BpiZcVoW8NMDjMDR+UenJuSfldbs0ZFC4K65XsWd+1UBgabi3p/UqYXC9O4YhwXbghdPxPMvPCkTV5fPhFqaCTyuBp4ZvwQji2ycDk8E54N8xpjqdekrkszKrJR4Ytwlep7o1zwp+p7o+6J4aEx8I9qarwLrfimcU1eVL4vB9+wBDcjXkNbq+B9sMjuKJRlf92tYH4syaPwzrhs+KNN5o8MrSk+fFtJNFle3mYX/qdJYlmCJ8UQjObFFaEle0mUmx/HMaFPqneQe+GDiVYE/avgSeEv8IRO2m8IxwVLgn3hyk7wB4Tfk715Fib6lV4aNfkveHzJoVnw3M9LD41LA6vht/Khr4MN4XOFnqN2ua/CHemepb8A7gmrK3JI4pXprYwODJV+V4TFtZInB36tNpA0V0YtpQC+t+3VJge3qzJ0wrrvt1gO8K6cs57hNfDi6nd9C1IDC0xuK3VJruAo4snpoc9w5Ph2hbY21JdDQPCXeWI9tiB7WHF8xtSVfTWRGpKTxV3p7hxeDeYS0tsjC1BuDHs08LeQYXs5lTPkEWtsN0pd4bVhczD3cxflao6n1fkh8KCbnBjUv2ObE91r81sFcw9Edq7kJlaGxseVoat4cLa+DvN6ZvqV2VLOcaJvSbQDaGvS0BOS3UrbyqZM74J91q4uSZfUDby+I5iqLdkJqSqyNtTPSvmhgHd4OaWzFiWqtZsLTVjh6m9W1qqMr6kBPRH4Yx22P0bNcolauCjiTYAAAAASUVORK5CYII=') !important; - width: 35px; - height: 29px; - margin-right: 5px; - } - - &:hover { - opacity: 1; - } -} - -// #endregion - // #region example b .ex-b { diff --git a/src/app/examples/custom-gutter-style/custom-gutter-style.component.ts b/src/app/examples/custom-gutter-style/custom-gutter-style.component.ts index 6f4aab3c..5625dc63 100644 --- a/src/app/examples/custom-gutter-style/custom-gutter-style.component.ts +++ b/src/app/examples/custom-gutter-style/custom-gutter-style.component.ts @@ -4,13 +4,14 @@ import { SplitAreaSize, SplitComponent, SplitDirection, - SplitGutterDragHandleDirective, SplitGutterInteractionEvent, SplitGutterDirective, SplitGutterExcludeFromDragDirective, + provideAngularSplitOptions, } from 'angular-split' import { ExampleTitleComponent } from 'src/app/ui/components/exampleTitle.component' import { AComponent } from '../../ui/components/AComponent' +import { CustomGutterComponent } from './custom-gutter.component' @Component({ selector: 'sp-ex-custom-gutter-style', @@ -20,9 +21,9 @@ import { AComponent } from '../../ui/components/AComponent' SplitComponent, ExampleTitleComponent, SplitGutterExcludeFromDragDirective, - SplitGutterDragHandleDirective, SplitGutterDirective, ], + providers: [provideAngularSplitOptions({ gutterComponent: CustomGutterComponent })], styleUrls: [`./custom-gutter-style.component.scss`], template: ` {{ testChangeDetectorRun() }} @@ -30,9 +31,6 @@ import { AComponent } from '../../ui/components/AComponent'
-
-
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tiam, quis nostrud exercitation diff --git a/src/app/examples/custom-gutter-style/custom-gutter.component.ts b/src/app/examples/custom-gutter-style/custom-gutter.component.ts new file mode 100644 index 00000000..51053289 --- /dev/null +++ b/src/app/examples/custom-gutter-style/custom-gutter.component.ts @@ -0,0 +1,61 @@ +import { Component, computed, inject, input } from '@angular/core' +import { SplitComponent, SplitGutterComponent, SplitGutterContext, SplitGutterDragHandleDirective } from 'angular-split' + +@Component({ + selector: 'sp-ex-custom-gutter', + template: '

', + styles: ` + :host { + width: 100%; + height: 100%; + display: flex; + cursor: default; + } + + :host(.horizontal) { + align-items: flex-end; + justify-content: center; + } + + :host(.vertical) { + align-items: center; + justify-content: flex-end; + } + + .custom-hand-gutter-icon { + opacity: 0.6; + transition: opacity 0.3s; + cursor: grab; + + :host(.horizontal) & { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAjCAYAAABl/XGVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANkSURBVEiJrddfaJdlFAfwzztda6tdZNo/pZstWlmhSy8siiwhGfQP3IWRIGUEJRgFQXTbZRBeFBhlBv25iFD6s4zCqI0GaWXEhNK5tTCUsJGUWQtOF++zeH7vfnO/3/JcPef7nOd8z3ne5znPeWlCgi3BeHAieDoomlnfDNGy4HBwabAoGAieOReOW4Lu4IIMuyd4LdMXBceCJY36bamXAb7C2/g+eC5YgFPonLYr+DXZ3J/WtQZ9wfrgvEazejHYmsbtwa7g1eDC4OegPbNdGXwdtAXDwVvBm8Fg0DpnZliMsRT9n9iCbmzAPtybZfeNMutnMVawsSgz/QmbGsnsoeD1CrYs+DHYFuytzD0RTAWPZtiGYGcjZK3BgaC/gvcFI+nYL60EEsGTGbYq+CRYF7wQPBWcPxthV8qkq4K/FJwJNmdYWyLbnmGXBKeD74L7gpeDV86WYX+wP2jLsDuS460V26lgT6YXiWxV0tuDk/UOCCjKYz2OuzN4EL+ho2L+Oy6qXe54wYGkT1H/NOayG32Zh7/xkez4JzmN/Zl+PX7J9CsxMRfZXtwZtUG9X4fsD3yW6bfgaKZ3YfSsZKlKjEp7n2RA9h2TnMJQpt+sNtPuOcky5+uzAE5ipGIzVjCZ6V3YQXlY8DDea4RsBFdVsOHpQdriLzL9Guwqyq2lvK/HC4YEPcGHwQdR7nWNBL3BpxWsJRsvqLwOj0/XxTR3KOidnvw42BTcFoxWCYOlwaEGdmDaviMbb47yCv0HTGaRrA2+rCy+Lvi2UbJs3ZJgLOipIasYHQmuyPT+4I15kO0OtlXBo8HFFaPbg4XBAym6m+ZBdiTK56oG3BusyfTH0kcdD3YGy5slSn5ORPXFDrbnVTxhq/Ns50HUGeUDWiML8QOuzsGi9vbPR1aoc4JblIV1Y6TG5RzJGtnFr5H02g4HO2bs8zwk2BOsO5tBWyIbCi7/H0RFlP1kZyPGD6brMKN0NUjWE2Xf2fCCG1N0i+e2nrH2keD5enN1q35RRvYu7mqWDLfi84bJkgxi9TzIenGwWbIJWX/YhFyGY82SjeHaqNOzzybBSkymxqg5SaXsYJQt+fIo+/p8viNYkd6td4KJYO1s/ub8c0wVvx83KJ/8FuXF/wdncFhZmgawr+Cv2Xz9Cy1ParntgE8FAAAAAElFTkSuQmCC'); + width: 27px; + height: 35px; + margin-bottom: 5px; + } + + :host(.vertical) & { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAdCAYAAAAgqdWEAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOxSURBVEiJtddtzNdTGAfwz7+6y91qKpaGF0UjY8JmrIw1sdj0YFMeMzYLJWaUmac1bfQiw0hkkjErTy+whrARm0zztHmYp3mayjyFkny9+J3bfv7uf3e3/p03v3Od872u8z3Xua7rnB9tamFQmB1eDhvC7+G7sCZcHw5u11o9ETkmPBBODgeGjtBZ+qeEZWFjeCKM2Z1ExodN4eKwPBzdAtc/XB6+DbPbsfCwcEI5jqXhvfBLeCacFfrvhI2R4f1wW2jU5xotFPrhMIwt38PLd198ibexHi9hbYM/ermp4ViD1Q2ubgU6LjwafgwJH4cHw5XhxDCsN4v2QGifEuDTmic6wpJC4KewIBzSwkiPR9ELQqeH7/+1ybCoEFka9qqNd4ZTwx0lZW8Pg9tFpqzxdLilSzggbA831gATwqrC+vkwP4xuJ4naWhPD5jBIuDG8HfqGGaW/KkwJA5sURxV822pFaISPwpmNsBq/YgS2YVaDD+tgnIQ5GIXzG7y1iwQGNNhak+9Dp/BpiZcVoW8NMDjMDR+UenJuSfldbs0ZFC4K65XsWd+1UBgabi3p/UqYXC9O4YhwXbghdPxPMvPCkTV5fPhFqaCTyuBp4ZvwQji2ycDk8E54N8xpjqdekrkszKrJR4Ytwlep7o1zwp+p7o+6J4aEx8I9qarwLrfimcU1eVL4vB9+wBDcjXkNbq+B9sMjuKJRlf92tYH4syaPwzrhs+KNN5o8MrSk+fFtJNFle3mYX/qdJYlmCJ8UQjObFFaEle0mUmx/HMaFPqneQe+GDiVYE/avgSeEv8IRO2m8IxwVLgn3hyk7wB4Tfk715Fib6lV4aNfkveHzJoVnw3M9LD41LA6vht/Khr4MN4XOFnqN2ua/CHemepb8A7gmrK3JI4pXprYwODJV+V4TFtZInB36tNpA0V0YtpQC+t+3VJge3qzJ0wrrvt1gO8K6cs57hNfDi6nd9C1IDC0xuK3VJruAo4snpoc9w5Ph2hbY21JdDQPCXeWI9tiB7WHF8xtSVfTWRGpKTxV3p7hxeDeYS0tsjC1BuDHs08LeQYXs5lTPkEWtsN0pd4bVhczD3cxflao6n1fkh8KCbnBjUv2ObE91r81sFcw9Edq7kJlaGxseVoat4cLa+DvN6ZvqV2VLOcaJvSbQDaGvS0BOS3UrbyqZM74J91q4uSZfUDby+I5iqLdkJqSqyNtTPSvmhgHd4OaWzFiWqtZsLTVjh6m9W1qqMr6kBPRH4Yx22P0bNcolauCjiTYAAAAASUVORK5CYII='); + width: 35px; + height: 29px; + margin-right: 5px; + } + + &:hover { + opacity: 1; + } + } + `, + imports: [SplitGutterDragHandleDirective], + host: { + '[class]': 'splitComponent.direction()', + '[style.background-color]': 'bgColor()', + }, +}) +export class CustomGutterComponent implements SplitGutterComponent { + splitComponent = inject(SplitComponent) + + context = input.required() + + bgColor = computed(() => (this.context().isDragged ? '#d1e3ff' : '#ff0')) +} From fb8a74058c68d888d329d393b87dbc383357e676 Mon Sep 17 00:00:00 2001 From: Harpush Date: Fri, 3 Apr 2026 00:43:35 +0300 Subject: [PATCH 2/2] feat: add better cursor css control with custom gutters --- .../split-gutter-drag-handle.directive.ts | 31 +++++++++- ...plit-gutter-exclude-from-drag.directive.ts | 17 +++--- .../gutter/split-gutters-manager.service.ts | 58 ++++++++++++++++--- .../src/lib/split/split.component.css | 10 +++- .../src/lib/split/split.component.html | 4 +- .../src/lib/split/split.component.ts | 2 +- .../custom-gutter-style.component.ts | 12 +++- .../custom-gutter.component.ts | 2 +- 8 files changed, 110 insertions(+), 26 deletions(-) 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 06bd7308..c75c54d5 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 { 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 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.guttersManager.addToMap(this.guttersManager.gutterToHandleElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addDragHandle(this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.guttersManager.removeFromMap(this.guttersManager.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 afb102a2..5191392b 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,24 +1,27 @@ -import { Directive, OnDestroy, ElementRef, inject } from '@angular/core' +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 guttersManager = inject(SplitGuttersManagerService) + readonly suppressDefaultCursor = input(false, { transform: booleanAttribute }) + + cursor = computed(() => (this.suppressDefaultCursor() ? undefined : 'default')) + constructor() { - this.guttersManager.addToMap(this.guttersManager.gutterToExcludeDragElementMap, this.gutterNum, this.elementRef) + this.guttersManager.addExcludeDrag(this.gutterNum, this.elementRef) } ngOnDestroy(): void { - this.guttersManager.removeFromMap( - this.guttersManager.gutterToExcludeDragElementMap, - this.gutterNum, - this.elementRef, - ) + this.guttersManager.removeExcludeDrag(this.gutterNum, this.elementRef) } } 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 index 0024e3ec..aaa33f4f 100644 --- a/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts +++ b/projects/angular-split/src/lib/gutter/split-gutters-manager.service.ts @@ -1,4 +1,4 @@ -import { ElementRef, Injectable } from '@angular/core' +import { ElementRef, Injectable, signal } from '@angular/core' @Injectable() export class SplitGuttersManagerService { @@ -6,16 +6,20 @@ export class SplitGuttersManagerService { * The map holds reference to the drag handle elements inside instances * of the provided template. */ - readonly gutterToHandleElementMap = new Map[]>() + private readonly gutterToHandleElementMap = signal(new Map[]>(), { + equal: () => false, + }) /** * The map holds reference to the excluded drag elements inside instances * of the provided template. */ - readonly gutterToExcludeDragElementMap = new Map[]>() + private readonly gutterToExcludeDragElementMap = signal(new Map[]>(), { + equal: () => false, + }) canStartDragging(originElement: HTMLElement, gutterNum: number) { - if (this.gutterToExcludeDragElementMap.has(gutterNum)) { - const isInsideExclude = this.gutterToExcludeDragElementMap + if (this.gutterToExcludeDragElementMap().has(gutterNum)) { + const isInsideExclude = this.gutterToExcludeDragElementMap() .get(gutterNum) .some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement)) @@ -24,8 +28,8 @@ export class SplitGuttersManagerService { } } - if (this.gutterToHandleElementMap.has(gutterNum)) { - return this.gutterToHandleElementMap + if (this.gutterToHandleElementMap().has(gutterNum)) { + return this.gutterToHandleElementMap() .get(gutterNum) .some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement)) } @@ -33,7 +37,39 @@ export class SplitGuttersManagerService { return true } - addToMap(map: Map[]>, gutterNum: number, elementRef: ElementRef) { + 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 { @@ -41,7 +77,11 @@ export class SplitGuttersManagerService { } } - removeFromMap(map: Map[]>, gutterNum: number, elementRef: ElementRef) { + private removeFromMap( + map: Map[]>, + gutterNum: number, + elementRef: ElementRef, + ) { const elements = map.get(gutterNum) elements.splice(elements.indexOf(elementRef), 1) diff --git a/projects/angular-split/src/lib/split/split.component.css b/projects/angular-split/src/lib/split/split.component.css index b0ef82a5..a77a5976 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 a3db3738..068285b8 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;