Skip to content

Commit c0eaf83

Browse files
committed
feat: support providing global custom gutter component
1 parent eaf046c commit c0eaf83

13 files changed

Lines changed: 207 additions & 179 deletions

projects/angular-split/src/lib/angular-split-config.token.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { InjectionToken, Provider, inject } from '@angular/core'
1+
import { InjectionToken, type Provider, type Type, inject } from '@angular/core'
22
import type { SplitDir, SplitDirection, SplitUnit } from './models'
3+
import type { SplitGutterComponent } from './gutter/split-gutter-component'
34

45
export interface AngularSplitDefaultOptions {
56
dir: SplitDir
@@ -12,6 +13,7 @@ export interface AngularSplitDefaultOptions {
1213
restrictMove: boolean
1314
unit: SplitUnit
1415
useTransition: boolean
16+
gutterComponent: Type<SplitGutterComponent> | undefined
1517
}
1618

1719
const defaultOptions: AngularSplitDefaultOptions = {
@@ -25,6 +27,7 @@ const defaultOptions: AngularSplitDefaultOptions = {
2527
restrictMove: false,
2628
unit: 'percent',
2729
useTransition: false,
30+
gutterComponent: undefined,
2831
}
2932

3033
export const ANGULAR_SPLIT_DEFAULT_OPTIONS = new InjectionToken<AngularSplitDefaultOptions>(
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { InputSignal } from '@angular/core'
2+
import type { SplitGutterContext } from './split-gutter-context'
3+
4+
export interface SplitGutterComponent {
5+
context: InputSignal<SplitGutterContext>
6+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { SplitAreaComponent } from '../split-area/split-area.component'
2+
3+
export interface SplitGutterContext {
4+
/**
5+
* The area before the gutter.
6+
* In RTL the right area and in LTR the left area
7+
*/
8+
areaBefore: SplitAreaComponent
9+
/**
10+
* The area after the gutter.
11+
* In RTL the left area and in LTR the right area
12+
*/
13+
areaAfter: SplitAreaComponent
14+
/**
15+
* The absolute number of the gutter based on direction (RTL and LTR).
16+
* First gutter is 1, second is 2, etc...
17+
*/
18+
gutterNum: number
19+
/**
20+
* Whether this is the first gutter.
21+
* In RTL the most right area and in LTR the most left area
22+
*/
23+
first: boolean
24+
/**
25+
* Whether this is the last gutter.
26+
* In RTL the most left area and in LTR the most right area
27+
*/
28+
last: boolean
29+
/**
30+
* Whether the gutter is being dragged now
31+
*/
32+
isDragged: boolean
33+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { Directive, OnDestroy, ElementRef, inject } from '@angular/core'
2-
import { SplitGutterDirective } from './split-gutter.directive'
32
import { GUTTER_NUM_TOKEN } from './gutter-num-token'
3+
import { SplitGuttersManagerService } from './split-gutters-manager.service'
44

55
@Directive({
66
selector: '[asSplitGutterDragHandle]',
77
})
88
export class SplitGutterDragHandleDirective implements OnDestroy {
99
private readonly gutterNum = inject(GUTTER_NUM_TOKEN)
1010
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)
11-
private readonly gutterDir = inject(SplitGutterDirective)
11+
private readonly guttersManager = inject(SplitGuttersManagerService)
1212

1313
constructor() {
14-
this.gutterDir._addToMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef)
14+
this.guttersManager.addToMap(this.guttersManager.gutterToHandleElementMap, this.gutterNum, this.elementRef)
1515
}
1616

1717
ngOnDestroy(): void {
18-
this.gutterDir._removedFromMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef)
18+
this.guttersManager.removeFromMap(this.guttersManager.gutterToHandleElementMap, this.gutterNum, this.elementRef)
1919
}
2020
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { Directive, OnDestroy, ElementRef, inject } from '@angular/core'
2-
import { SplitGutterDirective } from './split-gutter.directive'
32
import { GUTTER_NUM_TOKEN } from './gutter-num-token'
3+
import { SplitGuttersManagerService } from './split-gutters-manager.service'
44

55
@Directive({
66
selector: '[asSplitGutterExcludeFromDrag]',
77
})
88
export class SplitGutterExcludeFromDragDirective implements OnDestroy {
99
private readonly gutterNum = inject(GUTTER_NUM_TOKEN)
1010
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)
11-
private readonly gutterDir = inject(SplitGutterDirective)
11+
private readonly guttersManager = inject(SplitGuttersManagerService)
1212

1313
constructor() {
14-
this.gutterDir._addToMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)
14+
this.guttersManager.addToMap(this.guttersManager.gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)
1515
}
1616

1717
ngOnDestroy(): void {
18-
this.gutterDir._removedFromMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)
18+
this.guttersManager.removeFromMap(
19+
this.guttersManager.gutterToExcludeDragElementMap,
20+
this.gutterNum,
21+
this.elementRef,
22+
)
1923
}
2024
}

projects/angular-split/src/lib/gutter/split-gutter.directive.ts

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,14 @@
1-
import { Directive, ElementRef, inject, TemplateRef } from '@angular/core'
2-
import type { SplitAreaComponent } from '../split-area/split-area.component'
1+
import { Directive, inject, TemplateRef } from '@angular/core'
2+
import type { SplitGutterContext } from './split-gutter-context'
33

4-
export interface SplitGutterTemplateContext {
5-
/**
6-
* The area before the gutter.
7-
* In RTL the right area and in LTR the left area
8-
*/
9-
areaBefore: SplitAreaComponent
10-
/**
11-
* The area after the gutter.
12-
* In RTL the left area and in LTR the right area
13-
*/
14-
areaAfter: SplitAreaComponent
15-
/**
16-
* The absolute number of the gutter based on direction (RTL and LTR).
17-
* First gutter is 1, second is 2, etc...
18-
*/
19-
gutterNum: number
20-
/**
21-
* Whether this is the first gutter.
22-
* In RTL the most right area and in LTR the most left area
23-
*/
24-
first: boolean
25-
/**
26-
* Whether this is the last gutter.
27-
* In RTL the most left area and in LTR the most right area
28-
*/
29-
last: boolean
30-
/**
31-
* Whether the gutter is being dragged now
32-
*/
33-
isDragged: boolean
34-
}
4+
export type SplitGutterTemplateContext = SplitGutterContext
355

366
@Directive({
377
selector: '[asSplitGutter]',
388
})
399
export class SplitGutterDirective {
4010
readonly template = inject<TemplateRef<SplitGutterTemplateContext>>(TemplateRef)
4111

42-
/**
43-
* The map holds reference to the drag handle elements inside instances
44-
* of the provided template.
45-
*
46-
* @internal
47-
*/
48-
readonly _gutterToHandleElementMap = new Map<number, ElementRef<HTMLElement>[]>()
49-
/**
50-
* The map holds reference to the excluded drag elements inside instances
51-
* of the provided template.
52-
*
53-
* @internal
54-
*/
55-
readonly _gutterToExcludeDragElementMap = new Map<number, ElementRef<HTMLElement>[]>()
56-
57-
/**
58-
* @internal
59-
*/
60-
_canStartDragging(originElement: HTMLElement, gutterNum: number) {
61-
if (this._gutterToExcludeDragElementMap.has(gutterNum)) {
62-
const isInsideExclude = this._gutterToExcludeDragElementMap
63-
.get(gutterNum)
64-
.some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement))
65-
66-
if (isInsideExclude) {
67-
return false
68-
}
69-
}
70-
71-
if (this._gutterToHandleElementMap.has(gutterNum)) {
72-
return this._gutterToHandleElementMap
73-
.get(gutterNum)
74-
.some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement))
75-
}
76-
77-
return true
78-
}
79-
80-
/**
81-
* @internal
82-
*/
83-
_addToMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {
84-
if (map.has(gutterNum)) {
85-
map.get(gutterNum).push(elementRef)
86-
} else {
87-
map.set(gutterNum, [elementRef])
88-
}
89-
}
90-
91-
/**
92-
* @internal
93-
*/
94-
_removedFromMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {
95-
const elements = map.get(gutterNum)
96-
elements.splice(elements.indexOf(elementRef), 1)
97-
98-
if (elements.length === 0) {
99-
map.delete(gutterNum)
100-
}
101-
}
102-
10312
static ngTemplateContextGuard(_dir: SplitGutterDirective, ctx: unknown): ctx is SplitGutterTemplateContext {
10413
return true
10514
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ElementRef, Injectable } from '@angular/core'
2+
3+
@Injectable()
4+
export class SplitGuttersManagerService {
5+
/**
6+
* The map holds reference to the drag handle elements inside instances
7+
* of the provided template.
8+
*/
9+
readonly gutterToHandleElementMap = new Map<number, ElementRef<HTMLElement>[]>()
10+
/**
11+
* The map holds reference to the excluded drag elements inside instances
12+
* of the provided template.
13+
*/
14+
readonly gutterToExcludeDragElementMap = new Map<number, ElementRef<HTMLElement>[]>()
15+
16+
canStartDragging(originElement: HTMLElement, gutterNum: number) {
17+
if (this.gutterToExcludeDragElementMap.has(gutterNum)) {
18+
const isInsideExclude = this.gutterToExcludeDragElementMap
19+
.get(gutterNum)
20+
.some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement))
21+
22+
if (isInsideExclude) {
23+
return false
24+
}
25+
}
26+
27+
if (this.gutterToHandleElementMap.has(gutterNum)) {
28+
return this.gutterToHandleElementMap
29+
.get(gutterNum)
30+
.some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement))
31+
}
32+
33+
return true
34+
}
35+
36+
addToMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {
37+
if (map.has(gutterNum)) {
38+
map.get(gutterNum).push(elementRef)
39+
} else {
40+
map.set(gutterNum, [elementRef])
41+
}
42+
}
43+
44+
removeFromMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {
45+
const elements = map.get(gutterNum)
46+
elements.splice(elements.indexOf(elementRef), 1)
47+
48+
if (elements.length === 0) {
49+
map.delete(gutterNum)
50+
}
51+
}
52+
}

projects/angular-split/src/lib/split/split.component.html

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,24 @@
2222
(asSplitCustomMouseDown)="gutterMouseDown($event, gutter, $index, $index, $index + 1)"
2323
(asSplitCustomKeyDown)="gutterKeyDown($event, $index, $index, $index + 1)"
2424
>
25-
@if (customGutter()?.template) {
25+
@if (customGutter()?.template || defaultOptions.gutterComponent) {
26+
@let context =
27+
{
28+
areaBefore: area,
29+
areaAfter: _areas()[$index + 1],
30+
gutterNum: $index + 1,
31+
first: $first,
32+
last: $index === _areas().length - 2,
33+
isDragged: draggedGutterIndex() === $index,
34+
};
2635
<ng-container *asSplitGutterDynamicInjector="$index + 1; let injector">
27-
<ng-container
28-
*ngTemplateOutlet="
29-
customGutter().template;
30-
context: {
31-
areaBefore: area,
32-
areaAfter: _areas()[$index + 1],
33-
gutterNum: $index + 1,
34-
first: $first,
35-
last: $index === _areas().length - 2,
36-
isDragged: draggedGutterIndex() === $index
37-
};
38-
injector: injector
39-
"
40-
></ng-container>
36+
@if (customGutter()?.template; as tpl) {
37+
<ng-container *ngTemplateOutlet="tpl; context: context; injector: injector"></ng-container>
38+
} @else {
39+
<ng-container
40+
*ngComponentOutlet="defaultOptions.gutterComponent; inputs: { context }; injector: injector"
41+
></ng-container>
42+
}
4143
</ng-container>
4244
} @else {
4345
<div class="as-split-gutter-icon"></div>

projects/angular-split/src/lib/split/split.component.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NgStyle, NgTemplateOutlet } from '@angular/common'
1+
import { NgComponentOutlet, NgStyle, NgTemplateOutlet } from '@angular/common'
22
import {
33
ChangeDetectionStrategy,
44
Component,
@@ -52,6 +52,7 @@ import {
5252
toRecord,
5353
} from '../utils'
5454
import { areAreasValid } from '../validations'
55+
import { SplitGuttersManagerService } from '../gutter/split-gutters-manager.service'
5556

5657
interface MouseDownContext {
5758
mouseDownEvent: MouseEvent | TouchEvent
@@ -79,11 +80,18 @@ export const SPLIT_AREA_CONTRACT = new InjectionToken<SplitAreaComponent>('Split
7980

8081
@Component({
8182
selector: 'as-split',
82-
imports: [NgStyle, SplitCustomEventsBehaviorDirective, SplitGutterDynamicInjectorDirective, NgTemplateOutlet],
83+
imports: [
84+
NgStyle,
85+
SplitCustomEventsBehaviorDirective,
86+
SplitGutterDynamicInjectorDirective,
87+
NgTemplateOutlet,
88+
NgComponentOutlet,
89+
],
8390
exportAs: 'asSplit',
8491
templateUrl: './split.component.html',
8592
styleUrl: './split.component.css',
8693
changeDetection: ChangeDetectionStrategy.OnPush,
94+
providers: [SplitGuttersManagerService],
8795
host: {
8896
'[class]': 'hostClasses()',
8997
'[dir]': 'dir()',
@@ -93,7 +101,8 @@ export class SplitComponent {
93101
private readonly document = inject(DOCUMENT)
94102
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)
95103
private readonly ngZone = inject(NgZone)
96-
private readonly defaultOptions = inject(ANGULAR_SPLIT_DEFAULT_OPTIONS)
104+
protected readonly defaultOptions = inject(ANGULAR_SPLIT_DEFAULT_OPTIONS)
105+
private readonly guttersManager = inject(SplitGuttersManagerService)
97106

98107
private readonly gutterMouseDownSubject = new Subject<MouseDownContext>()
99108
private readonly dragProgressSubject = new Subject<SplitGutterInteractionEvent>()
@@ -184,13 +193,8 @@ export class SplitComponent {
184193

185194
this.gutterMouseDownSubject
186195
.pipe(
187-
filter(
188-
(context) =>
189-
!this.customGutter() ||
190-
this.customGutter()._canStartDragging(
191-
context.mouseDownEvent.target as HTMLElement,
192-
context.gutterIndex + 1,
193-
),
196+
filter((context) =>
197+
this.guttersManager.canStartDragging(context.mouseDownEvent.target as HTMLElement, context.gutterIndex + 1),
194198
),
195199
switchMap((mouseDownContext) =>
196200
// As we have gutterClickDeltaPx we can't just start the drag but need to make sure

projects/angular-split/src/public_api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export { SplitGutterDragHandleDirective } from './lib/gutter/split-gutter-drag-h
77
export { SplitGutterExcludeFromDragDirective } from './lib/gutter/split-gutter-exclude-from-drag.directive'
88
export { SplitGutterDirective } from './lib/gutter/split-gutter.directive'
99
export type { SplitGutterTemplateContext } from './lib/gutter/split-gutter.directive'
10+
export type { SplitGutterComponent } from './lib/gutter/split-gutter-component'
11+
export type { SplitGutterContext } from './lib/gutter/split-gutter-context'
1012
export type {
1113
SplitAreaSize,
1214
SplitAreaSizeInput,

0 commit comments

Comments
 (0)