-
Notifications
You must be signed in to change notification settings - Fork 220
feat: support providing global custom gutter component #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import type { InputSignal } from '@angular/core' | ||
| import type { SplitGutterContext } from './split-gutter-context' | ||
|
|
||
| export interface SplitGutterComponent { | ||
| context: InputSignal<SplitGutterContext> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HTMLElement>>(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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HTMLElement>>(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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<number, ElementRef<HTMLElement>[]>(), { | ||||||||||||||||||||||||||||
| equal: () => false, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * The map holds reference to the excluded drag elements inside instances | ||||||||||||||||||||||||||||
| * of the provided template. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| private readonly gutterToExcludeDragElementMap = signal(new Map<number, ElementRef<HTMLElement>[]>(), { | ||||||||||||||||||||||||||||
| 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<HTMLElement>) { | ||||||||||||||||||||||||||||
| const map = this.gutterToHandleElementMap() | ||||||||||||||||||||||||||||
| this.addToMap(map, gutterNum, elementRef) | ||||||||||||||||||||||||||||
| this.gutterToHandleElementMap.set(map) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| removeDragHandle(gutterNum: number, elementRef: ElementRef<HTMLElement>) { | ||||||||||||||||||||||||||||
| const map = this.gutterToHandleElementMap() | ||||||||||||||||||||||||||||
| this.removeFromMap(map, gutterNum, elementRef) | ||||||||||||||||||||||||||||
| this.gutterToHandleElementMap.set(map) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| addExcludeDrag(gutterNum: number, elementRef: ElementRef<HTMLElement>) { | ||||||||||||||||||||||||||||
| const map = this.gutterToExcludeDragElementMap() | ||||||||||||||||||||||||||||
| this.addToMap(map, gutterNum, elementRef) | ||||||||||||||||||||||||||||
| this.gutterToExcludeDragElementMap.set(map) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| removeExcludeDrag(gutterNum: number, elementRef: ElementRef<HTMLElement>) { | ||||||||||||||||||||||||||||
| const map = this.gutterToExcludeDragElementMap() | ||||||||||||||||||||||||||||
| this.removeFromMap(map, gutterNum, elementRef) | ||||||||||||||||||||||||||||
| this.gutterToExcludeDragElementMap.set(map) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private addToMap( | ||||||||||||||||||||||||||||
| map: Map<number, ElementRef<HTMLElement>[]>, | ||||||||||||||||||||||||||||
| gutterNum: number, | ||||||||||||||||||||||||||||
| elementRef: ElementRef<HTMLElement>, | ||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||
| if (map.has(gutterNum)) { | ||||||||||||||||||||||||||||
| map.get(gutterNum).push(elementRef) | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| map.set(gutterNum, [elementRef]) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private removeFromMap( | ||||||||||||||||||||||||||||
| map: Map<number, ElementRef<HTMLElement>[]>, | ||||||||||||||||||||||||||||
| gutterNum: number, | ||||||||||||||||||||||||||||
| elementRef: ElementRef<HTMLElement>, | ||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||
| const elements = map.get(gutterNum) | ||||||||||||||||||||||||||||
| elements.splice(elements.indexOf(elementRef), 1) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+86
to
+87
|
||||||||||||||||||||||||||||
| elements.splice(elements.indexOf(elementRef), 1) | |
| if (!elements) { | |
| return | |
| } | |
| const elementIndex = elements.indexOf(elementRef) | |
| if (elementIndex === -1) { | |
| return | |
| } | |
| elements.splice(elementIndex, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SplitGutterComponentcurrently requirescontext: InputSignal<SplitGutterContext>, which forces consumers to use the signal-basedinput()API to satisfygutterComponent: Type<SplitGutterComponent>. Since*ngComponentOutletwill also work with a classic@Input() context: SplitGutterContext, consider widening the contract (e.g.,context: SplitGutterContext | InputSignal<SplitGutterContext>) so consumers aren’t artificially constrained by types.