Skip to content

Commit 9a810d5

Browse files
committed
feat: support custom gutter template
1 parent 4fbf622 commit 9a810d5

14 files changed

Lines changed: 609 additions & 169 deletions

cypress/e2e/5.style.cy.js

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,53 @@
33
import { moveGutterByMouse, checkSplitDirAndSizes } from '../support/splitUtils'
44

55
context('Custom split style example page tests', () => {
6-
const W = 1070
6+
const W = 1076
77
const H = 300
8-
const GUTTER = 35
9-
const COLOR = 'rgb(255, 255, 0)'
10-
const IMGH = `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")`
11-
const IMGV = `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=")`
128

139
beforeEach(() => {
1410
cy.visit('/examples/custom-gutter-style')
1511
})
1612

17-
it('Verify gutter size color and horizontal image', () => {
18-
checkSplitDirAndSizes('.ex-a as-split', 'horizontal', W, H, GUTTER, [312.296875, 728.6875])
13+
// ----- EXAMPLE A
1914

20-
cy.get('.ex-a as-split > .as-split-gutter').should('have.css', 'background-color', COLOR)
21-
cy.get('.ex-a as-split > .as-split-gutter > .as-split-gutter-icon').should('have.css', 'background-image', IMGH)
15+
it('should display initial state for example a', () => {
16+
checkSplitDirAndSizes('.ex-a > as-split', 'horizontal', W, H, 35, [301.796875, 402.390625, 301.8125])
2217
})
2318

24-
xit('Change direction', () => {
25-
cy.get('.btns > .btn').click()
26-
checkSplitDirAndSizes('.ex-a as-split', 'vertical', W, H, GUTTER, [79.5, 185.5])
19+
it('should not move from non handle for example a', () => {
20+
moveGutterByMouse('.ex-a .as-split-gutter', 0, 280, 0)
21+
checkSplitDirAndSizes('.ex-a > as-split', 'horizontal', W, H, 35, [301.796875, 402.390625, 301.8125])
22+
})
23+
24+
it('should move from handle for example a', () => {
25+
moveGutterByMouse('.ex-a .as-split-gutter .custom-hand-gutter-icon', 0, 280, 0)
26+
checkSplitDirAndSizes('.ex-a > as-split', 'horizontal', W, H, 35, [581.796875, 122.390625, 301.8125])
27+
})
28+
29+
// ----- EXAMPLE B
30+
31+
it('should display initial state for example b', () => {
32+
checkSplitDirAndSizes('.ex-b > as-split', 'horizontal', W, H, 1, [322.1875, 537, 214.796875])
33+
})
34+
35+
// ----- EXAMPLE C
36+
37+
it('should display initial state for example c', () => {
38+
checkSplitDirAndSizes('.ex-c > as-split', 'horizontal', W, H, 25, [300.296875, 100.09375, 400.390625, 200.1875])
39+
})
40+
41+
it('should not move from collapse button for example c', () => {
42+
moveGutterByMouse('.ex-c .as-split-gutter .custom-collapse-gutter-header div', 0, 50, 0)
43+
checkSplitDirAndSizes('.ex-c > as-split', 'horizontal', W, H, 25, [300.296875, 100.09375, 400.390625, 200.1875])
44+
})
45+
46+
it('should move from anywhere other than buttons for example c', () => {
47+
moveGutterByMouse('.ex-c .as-split-gutter', 0, 50, 0)
48+
checkSplitDirAndSizes('.ex-c > as-split', 'horizontal', W, H, 25, [350.296875, 50.09375, 400.390625, 200.1875])
49+
})
2750

28-
cy.get('.ex-a as-split > .as-split-gutter').should('have.css', 'background-color', COLOR)
29-
cy.get('.ex-a as-split > .as-split-gutter > .as-split-gutter-icon').should('have.css', 'background-image', IMGV)
51+
it('should collapse left area on collapse button click', () => {
52+
cy.get('.ex-c .as-split-gutter:first-of-type .custom-collapse-gutter-header div:first-of-type').click()
53+
checkSplitDirAndSizes('.ex-c > as-split', 'horizontal', W, H, 25, [0, 400.390625, 400.390625, 200.1875])
3054
})
3155
})

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ViewEncapsulation,
1616
Inject,
1717
Optional,
18+
ContentChild,
1819
} from '@angular/core'
1920
import { Observable, Subscriber, Subject } from 'rxjs'
2021
import { debounceTime } from 'rxjs/operators'
@@ -46,6 +47,7 @@ import {
4647
getKeyboardEndpoint,
4748
} from '../utils'
4849
import { ANGULAR_SPLIT_DEFAULT_OPTIONS } from '../angular-split-config.token'
50+
import { SplitGutterDirective } from '../gutter/split-gutter.directive'
4951

5052
/**
5153
* angular-split
@@ -83,13 +85,21 @@ import { ANGULAR_SPLIT_DEFAULT_OPTIONS } from '../angular-split-config.token'
8385
changeDetection: ChangeDetectionStrategy.OnPush,
8486
styleUrls: [`./split.component.scss`],
8587
template: ` <ng-content></ng-content>
86-
<ng-template ngFor [ngForOf]="displayedAreas" let-area="$implicit" let-index="index" let-last="last">
88+
<ng-template
89+
ngFor
90+
[ngForOf]="displayedAreas"
91+
let-area="$implicit"
92+
let-index="index"
93+
let-first="first"
94+
let-last="last"
95+
>
8796
<div
8897
role="separator"
8998
tabindex="0"
9099
*ngIf="last === false"
91100
#gutterEls
92101
class="as-split-gutter"
102+
[class.as-dragged]="draggedGutterNum === index + 1"
93103
[style.flex-basis.px]="gutterSize"
94104
[style.order]="index * 2 + 1"
95105
(keydown)="startKeyboardDrag($event, index * 2 + 1, index + 1)"
@@ -104,12 +114,33 @@ import { ANGULAR_SPLIT_DEFAULT_OPTIONS } from '../angular-split-config.token'
104114
[attr.aria-valuenow]="area.size"
105115
[attr.aria-valuetext]="getAriaAreaSizeText(area.size)"
106116
>
107-
<div class="as-split-gutter-icon"></div>
117+
<ng-container *ngIf="customGutter?.template; else defaultGutterTpl">
118+
<ng-container *asSplitGutterDynamicInjector="index + 1; let injector">
119+
<ng-container
120+
*ngTemplateOutlet="
121+
customGutter.template;
122+
context: {
123+
areaBefore: area,
124+
areaAfter: displayedAreas[index + 1],
125+
gutterNum: index + 1,
126+
first,
127+
last: index === displayedAreas.length - 2,
128+
isDragged: draggedGutterNum === index + 1
129+
};
130+
injector: injector
131+
"
132+
></ng-container>
133+
</ng-container>
134+
</ng-container>
135+
<ng-template #defaultGutterTpl>
136+
<div class="as-split-gutter-icon"></div>
137+
</ng-template>
108138
</div>
109139
</ng-template>`,
110140
encapsulation: ViewEncapsulation.Emulated,
111141
})
112142
export class SplitComponent implements AfterViewInit, OnDestroy {
143+
@ContentChild(SplitGutterDirective) customGutter: SplitGutterDirective
113144
@Input() set direction(v: ISplitDirection) {
114145
this._direction = v === 'vertical' ? 'vertical' : 'horizontal'
115146

@@ -288,6 +319,7 @@ export class SplitComponent implements AfterViewInit, OnDestroy {
288319
@ViewChildren('gutterEls') private gutterEls: QueryList<ElementRef>
289320

290321
_clickTimeout: number | null = null
322+
draggedGutterNum: number = undefined
291323

292324
public ngAfterViewInit() {
293325
this.ngZone.runOutsideAngular(() => {
@@ -584,6 +616,10 @@ export class SplitComponent implements AfterViewInit, OnDestroy {
584616
}
585617

586618
public startMouseDrag(event: MouseEvent | TouchEvent, gutterOrder: number, gutterNum: number): void {
619+
if (this.customGutter && !this.customGutter.canStartDragging(event.target as HTMLElement, gutterNum)) {
620+
return
621+
}
622+
587623
event.preventDefault()
588624
event.stopPropagation()
589625

@@ -709,7 +745,8 @@ export class SplitComponent implements AfterViewInit, OnDestroy {
709745
this.isWaitingInitialMove = false
710746

711747
this.renderer.addClass(this.elRef.nativeElement, 'as-dragging')
712-
this.renderer.addClass(this.gutterEls.toArray()[this.snapshot.gutterNum - 1].nativeElement, 'as-dragged')
748+
this.draggedGutterNum = this.snapshot.gutterNum
749+
this.cdRef.markForCheck()
713750

714751
this.notify('start', this.snapshot.gutterNum)
715752
})
@@ -848,7 +885,9 @@ export class SplitComponent implements AfterViewInit, OnDestroy {
848885
}
849886

850887
this.renderer.removeClass(this.elRef.nativeElement, 'as-dragging')
851-
this.renderer.removeClass(this.gutterEls.toArray()[this.snapshot.gutterNum - 1].nativeElement, 'as-dragged')
888+
this.draggedGutterNum = undefined
889+
this.cdRef.markForCheck()
890+
852891
this.snapshot = null
853892
this.isWaitingClear = true
854893

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { InjectionToken } from '@angular/core'
2+
3+
/**
4+
* Identifies the gutter by number through DI
5+
* to allow SplitGutterDragHandleDirective and SplitGutterExcludeFromDragDirective to know
6+
* the gutter template context without inputs
7+
*/
8+
export const GUTTER_NUM_TOKEN = new InjectionToken<number>('Gutter num')
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, OnInit, OnDestroy, Inject, ElementRef } from '@angular/core'
2+
import { SplitGutterDirective } from './split-gutter.directive'
3+
import { GUTTER_NUM_TOKEN } from './gutter-num-token'
4+
5+
@Directive({
6+
selector: '[asSplitGutterDragHandle]',
7+
})
8+
export class SplitGutterDragHandleDirective implements OnInit, OnDestroy {
9+
constructor(
10+
@Inject(GUTTER_NUM_TOKEN) private gutterNum: number,
11+
private elementRef: ElementRef<HTMLElement>,
12+
private gutterDir: SplitGutterDirective,
13+
) {}
14+
15+
ngOnInit(): void {
16+
this.gutterDir.addToMap(this.gutterDir.gutterToHandleElementMap, this.gutterNum, this.elementRef)
17+
}
18+
19+
ngOnDestroy(): void {
20+
this.gutterDir.removedFromMap(this.gutterDir.gutterToHandleElementMap, this.gutterNum, this.elementRef)
21+
}
22+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Injector, Directive, Input, ViewContainerRef, TemplateRef } from '@angular/core'
2+
import { GUTTER_NUM_TOKEN } from './gutter-num-token'
3+
4+
interface SplitGutterDynamicInjectorTemplateContext {
5+
$implicit: Injector
6+
}
7+
8+
/**
9+
* This directive allows creating a dynamic injector inside ngFor
10+
* with dynamic gutter num and expose the injector for ngTemplateOutlet usage
11+
*/
12+
@Directive({
13+
selector: '[asSplitGutterDynamicInjector]',
14+
})
15+
export class SplitGutterDynamicInjectorDirective {
16+
@Input('asSplitGutterDynamicInjector')
17+
public set gutterNum(value: number) {
18+
this.vcr.clear()
19+
20+
const injector = Injector.create({
21+
providers: [
22+
{
23+
provide: GUTTER_NUM_TOKEN,
24+
useValue: value,
25+
},
26+
],
27+
parent: this.vcr.injector,
28+
})
29+
30+
this.vcr.createEmbeddedView(this.templateRef, { $implicit: injector })
31+
}
32+
33+
constructor(
34+
private vcr: ViewContainerRef,
35+
private templateRef: TemplateRef<SplitGutterDynamicInjectorTemplateContext>,
36+
) {}
37+
38+
static ngTemplateContextGuard(
39+
dir: SplitGutterDynamicInjectorDirective,
40+
ctx: unknown,
41+
): ctx is SplitGutterDynamicInjectorTemplateContext {
42+
return true
43+
}
44+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, OnInit, OnDestroy, Inject, ElementRef } from '@angular/core'
2+
import { SplitGutterDirective } from './split-gutter.directive'
3+
import { GUTTER_NUM_TOKEN } from './gutter-num-token'
4+
5+
@Directive({
6+
selector: '[asSplitGutterExcludeFromDrag]',
7+
})
8+
export class SplitGutterExcludeFromDragDirective implements OnInit, OnDestroy {
9+
constructor(
10+
@Inject(GUTTER_NUM_TOKEN) private gutterNum: number,
11+
private elementRef: ElementRef<HTMLElement>,
12+
private gutterDir: SplitGutterDirective,
13+
) {}
14+
15+
ngOnInit(): void {
16+
this.gutterDir.addToMap(this.gutterDir.gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)
17+
}
18+
19+
ngOnDestroy(): void {
20+
this.gutterDir.removedFromMap(this.gutterDir.gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)
21+
}
22+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Directive, ElementRef, TemplateRef } from '@angular/core'
2+
import { IArea } from '../interface'
3+
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: IArea
10+
/**
11+
* The area after the gutter.
12+
* In RTL the left area and in LTR the right area
13+
*/
14+
areaAfter: IArea
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+
}
35+
36+
@Directive({
37+
selector: '[asSplitGutter]',
38+
})
39+
export class SplitGutterDirective {
40+
/**
41+
* The map holds reference to the drag handle elements inside instances
42+
* of the provided template.
43+
*/
44+
public gutterToHandleElementMap = new Map<number, ElementRef<HTMLElement>[]>()
45+
/**
46+
* The map holds reference to the excluded drag elements inside instances
47+
* of the provided template.
48+
*/
49+
public gutterToExcludeDragElementMap = new Map<number, ElementRef<HTMLElement>[]>()
50+
51+
constructor(public template: TemplateRef<SplitGutterTemplateContext>) {}
52+
53+
public canStartDragging(originElement: HTMLElement, gutterNum: number) {
54+
if (this.gutterToExcludeDragElementMap.has(gutterNum)) {
55+
const isInsideExclude = this.gutterToExcludeDragElementMap
56+
.get(gutterNum)
57+
.some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement))
58+
59+
if (isInsideExclude) {
60+
return false
61+
}
62+
}
63+
64+
if (this.gutterToHandleElementMap.has(gutterNum)) {
65+
return this.gutterToHandleElementMap
66+
.get(gutterNum)
67+
.some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement))
68+
}
69+
70+
return true
71+
}
72+
73+
public addToMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {
74+
if (map.has(gutterNum)) {
75+
map.get(gutterNum).push(elementRef)
76+
} else {
77+
map.set(gutterNum, [elementRef])
78+
}
79+
}
80+
81+
public removedFromMap(
82+
map: Map<number, ElementRef<HTMLElement>[]>,
83+
gutterNum: number,
84+
elementRef: ElementRef<HTMLElement>,
85+
) {
86+
const elements = map.get(gutterNum)
87+
elements.splice(elements.indexOf(elementRef), 1)
88+
89+
if (elements.length === 0) {
90+
map.delete(gutterNum)
91+
}
92+
}
93+
94+
static ngTemplateContextGuard(dir: SplitGutterDirective, ctx: unknown): ctx is SplitGutterTemplateContext {
95+
return true
96+
}
97+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SplitAreaDirective } from './directive/split-area.directive'
1+
import type { SplitAreaDirective } from './directive/split-area.directive'
22

33
export type ISplitDirection = 'horizontal' | 'vertical'
44

0 commit comments

Comments
 (0)