Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
Split drop-down and select into different modules with multiple changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ppacher committed Apr 4, 2022
1 parent a6e6267 commit 789847a
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 135 deletions.
46 changes: 22 additions & 24 deletions modules/portmaster/src/app/shared/dropdown/dropdown.html
@@ -1,28 +1,26 @@
<!-- This button triggers the overlay and is it's origin -->
<button (click)="toggle()" [class.active]="isOpen" type="button" cdkOverlayOrigin #trigger="cdkOverlayOrigin"
class="border border-gray-400">
<span *ngIf="currentItem === null">
Select
</span>
<span *ngIf="currentItem !== null">
<ng-container *ngTemplateOutlet="currentItem.templateRef"></ng-container>
</span>
<div class="arrow">
<svg viewBox="0 0 24 24" class="arrow-icon">
<g fill="none" class="inner">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2" d="M10 16l4-4-4-4" />
</g>
<div *ngIf="!externalTrigger" class="w-full" cdkOverlayOrigin #trigger="cdkOverlayOrigin" (click)="toggle(trigger)">
<ng-template [ngTemplateOutlet]="triggerTemplate || defaultTriggerTemplate"></ng-template>
</div>

<ng-template #defaultTriggerTemplate>
<!-- TODO(ppacher): use a button rather than a div but first fix the button styling -->
<div [class.rounded-b]="!isOpen"
class="flex flex-row items-center justify-between w-full px-4 py-2 mt-6 bg-gray-100 rounded-t cursor-pointer hover:bg-gray-100 hover:bg-opacity-75 text-secondary">
{{ label }}

<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
</ng-template>

<!-- This template displays the overlay content and is connected to the button -->
<ng-template cdkConnectedOverlay [cdkConnectedOverlayMinWidth]="element.nativeElement.offsetWidth"
[cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="isOpen" (detach)="onOverlayClosed()"
[cdkConnectedOverlayScrollStrategy]="scrollStrategy" (overlayOutsideClick)="onOutsideClick($event)">
<ul class="item-list">
<li *ngFor="let item of items" (click)="selectItem(item)" [sfng-tooltip]="item.description || null" snfgTooltipPosition="left" [class.cursor-not-allowed]="item.disabled">
<ng-container *ngTemplateOutlet="item.templateRef"></ng-container>
</li>
</ul>
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOffsetY]="offsetY" [cdkConnectedOverlayMinWidth]="minWidth"
[cdkConnectedOverlayMinHeight]="minHeight" [cdkConnectedOverlayOrigin]="trigger!" [cdkConnectedOverlayOpen]="isOpen"
(detach)="onOverlayClosed()" [cdkConnectedOverlayScrollStrategy]="scrollStrategy"
(overlayOutsideClick)="onOutsideClick($event)" [cdkConnectedOverlayPositions]="positions">
<div class="w-full overflow-hidden bg-gray-200 rounded-b shadow" [style.maxHeight]="maxHeight"
[style.maxWidth]="maxWidth" [@fadeIn] [@fadeOut]>
<ng-content></ng-content>
</div>
</ng-template>
18 changes: 18 additions & 0 deletions modules/portmaster/src/app/shared/dropdown/dropdown.module.ts
@@ -0,0 +1,18 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { SfngDropdown } from "./dropdown";

@NgModule({
imports: [
CommonModule,
OverlayModule,
],
declarations: [
SfngDropdown,
],
exports: [
SfngDropdown,
]
})
export class SfngDropDownModule { }
206 changes: 120 additions & 86 deletions modules/portmaster/src/app/shared/dropdown/dropdown.ts
@@ -1,70 +1,130 @@
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { CdkOverlayOrigin, ScrollStrategy, ScrollStrategyOptions } from '@angular/cdk/overlay';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, forwardRef, HostBinding, HostListener, Input, QueryList, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DropDownValueDirective } from './item';
import { coerceBooleanProperty, coerceCssPixelValue, coerceNumberProperty } from "@angular/cdk/coercion";
import { CdkOverlayOrigin, ConnectedPosition, ScrollStrategy, ScrollStrategyOptions } from "@angular/cdk/overlay";
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, Renderer2, TemplateRef } from "@angular/core";
import { timeHours } from "d3";
import { fadeInAnimation, fadeOutAnimation } from '../animations';

@Component({
selector: 'app-dropdown',
selector: 'sfng-dropdown',
exportAs: 'sfngDropdown',
templateUrl: './dropdown.html',
styleUrls: ['./dropdown.scss'],
styles: [
`
:host {
display: block;
}
`
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DropdownComponent),
multi: true,
},
]
animations: [fadeInAnimation, fadeOutAnimation],
})
export class DropdownComponent<T> implements AfterViewInit, ControlValueAccessor {
@ContentChildren(DropDownValueDirective)
items: QueryList<DropDownValueDirective> | null = null;

@ViewChild(CdkOverlayOrigin)
export class SfngDropdown implements OnInit {
/** The trigger origin used to open the drop-down */
trigger: CdkOverlayOrigin | null = null;

@HostBinding('tabindex')
readonly tabindex = 0;

@HostBinding('attr.role')
readonly role = 'listbox';

value: T | null = null;
currentItem: DropDownValueDirective | null = null;
/**
* The button/drop-down label. Only when not using
* {@Link SfngDropdown.externalTrigger}
*/
@Input()
label: string = '';

isOpen = false;
/** The trigger template to use when {@Link SfngDropdown.externalTrigger} */
@Input()
triggerTemplate: TemplateRef<any> | null = null;

scrollStrategy: ScrollStrategy;
/** Set to true to provide an external dropdown trigger template using {@Link SfngDropdown.triggerTemplate} */
@Input()
set externalTrigger(v: any) {
this._externalTrigger = coerceBooleanProperty(v)
}
get externalTrigger() {
return this._externalTrigger;
}
private _externalTrigger = false;

/** Whether or not the drop-down is disabled. */
@Input()
@HostBinding('class.disabled')
set disabled(v: any) {
const disabled = coerceBooleanProperty(v);
this.setDisabledState(disabled);
this._disabled = coerceBooleanProperty(v)
}
get disabled() {
return this._disabled;
}
private _disabled = false;

/** The Y-offset of the drop-down overlay */
@Input()
set offsetY(v: any) {
this._offsetY = coerceNumberProperty(v);
}
get offsetY() { return this._offsetY }
private _offsetY = 4;

private _disabled: boolean = false;
/** The scrollStrategy of the drop-down */
@Input()
scrollStrategy!: ScrollStrategy;

/** Whether or not the pop-over is currently shown. Do not modify this directly */
isOpen = false;

trackItem(_: number, item: DropDownValueDirective) {
return item.value;
/** The minimum width of the drop-down */
@Input()
set minWidth(val: any) {
this._minWidth = coerceCssPixelValue(val)
}
get minWidth() { return this._minWidth }
private _minWidth: string | number = 0;

setDisabledState(disabled: boolean) {
this._disabled = disabled;
this.changeDetectorRef.markForCheck();
/** The maximum width of the drop-down */
@Input()
set maxWidth(val: any) {
this._maxWidth = coerceCssPixelValue(val)
}
get maxWidth() { return this._maxWidth }
private _maxWidth: string | number | null = null;

toggle() {
if (!this.isOpen && this._disabled) {
return;
}
/** The minimum height of the drop-down */
@Input()
set minHeight(val: any) {
this._minHeight = coerceCssPixelValue(val)
}
get minHeight() { return this._minHeight }
private _minHeight: string | number | null = null;

this.isOpen = !this.isOpen;
this.changeDetectorRef.markForCheck();
/** The maximum width of the drop-down */
@Input()
set maxHeight(val: any) {
this._maxHeight = coerceCssPixelValue(val)
}
get maxHeight() { return this._maxHeight }
private _maxHeight: string | number | null = null;

positions: ConnectedPosition[] = [
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom',
},
]

constructor(
public readonly elementRef: ElementRef,
private changeDetectorRef: ChangeDetectorRef,
private renderer: Renderer2,
private scrollOptions: ScrollStrategyOptions,
) {
}

ngOnInit() {
this.scrollStrategy = this.scrollStrategy || this.scrollOptions.close();
}

onOutsideClick(event: MouseEvent) {
Expand All @@ -83,64 +143,38 @@ export class DropdownComponent<T> implements AfterViewInit, ControlValueAccessor
this.close();
}

constructor(
public element: ElementRef,
private changeDetectorRef: ChangeDetectorRef,
private renderer: Renderer2,
scrollOptions: ScrollStrategyOptions,
) {
this.scrollStrategy = scrollOptions.close();
}

ngAfterViewInit(): void {
if (!!this.value && !!this.items) {
this.currentItem = this.items.find(item => item.value === this.value) || null;
this.changeDetectorRef.detectChanges();
}
onOverlayClosed() {
this.trigger = null;
}

close() {
this.isOpen = false;
this.changeDetectorRef.markForCheck();
}

@HostListener('blur')
onBlur(): void {
this.onTouch();
}
toggle(t: CdkOverlayOrigin) {
if (this.isOpen) {
this.close();

selectItem(item: DropDownValueDirective) {
if (item.disabled) {
return;
}

this.currentItem = item;
this.value = item.value;
this.isOpen = false;
this.onChange(this.value!);
this.show(t);
}

writeValue(value: T): void {
this.value = value;

if (!!this.items) {
this.currentItem = this.items.find(item => item.value === value) || null;
show(t: CdkOverlayOrigin) {
if (this.isOpen || this._disabled) {
return;
}

this.changeDetectorRef.markForCheck();
}
if (!!t) {
this.trigger = t;
const rect = (this.trigger.elementRef.nativeElement as HTMLElement).getBoundingClientRect()

onChange = (value: T): void => { }
registerOnChange(fn: (value: T) => void): void {
this.onChange = fn;
}

onTouch = (): void => { }
registerOnTouched(fn: () => void): void {
this.onTouch = fn;
}
this.minWidth = rect ? rect.width : this.trigger.elementRef.nativeElement.offsetWidth;

onOverlayClosed() {
this.close();
}
this.isOpen = true;
this.changeDetectorRef.markForCheck();
}
}
3 changes: 3 additions & 0 deletions modules/portmaster/src/app/shared/select/index.ts
@@ -0,0 +1,3 @@
export * from './select';
export * from './item';
export * from './select.module'
Expand Up @@ -2,30 +2,30 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Component, Directive, HostBinding, Input, Optional, TemplateRef } from '@angular/core';

@Component({
selector: 'app-dropdown-item',
selector: 'sfng-select-item',
template: `<ng-content></ng-content>`,
styleUrls: ['./item.scss'],
})
export class DropDownItemComponent {
export class SfngSelectItemComponent {
@HostBinding('class.disabled')
get isDisabled() {
return this.dropDownValue?.disabled || false;
return this.sfngSelectValue?.disabled || false;
}

constructor(@Optional() private dropDownValue: DropDownValueDirective) {}
constructor(@Optional() private sfngSelectValue: SfngSelectValueDirective) { }
}

@Directive({
selector: '[dropDownValue]',
selector: '[sfngSelectValue]',
})
export class DropDownValueDirective {
@Input('dropDownValue')
export class SfngSelectValueDirective {
@Input('sfngSelectValue')
value: any;

@Input('dropDownValueDescription')
@Input('sfngSelectValueDescription')
description = '';

@Input('dropDownValueDisabled')
@Input('sfngSelectValueDisabled')
set disabled(v: any) {
this._disabled = coerceBooleanProperty(v)
}
Expand Down
27 changes: 27 additions & 0 deletions modules/portmaster/src/app/shared/select/select.html
@@ -0,0 +1,27 @@
<ng-template #customTriggerTemplate>
<button [class.active]="dropdown.isOpen" type="button">
<span *ngIf="currentItem === null">
Select
</span>
<span *ngIf="currentItem !== null">
<ng-container *ngTemplateOutlet="currentItem.templateRef"></ng-container>
</span>
<div class="arrow">
<svg viewBox="0 0 24 24" class="arrow-icon">
<g fill="none" class="inner">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2" d="M10 16l4-4-4-4" />
</g>
</svg>
</div>
</button>
</ng-template>

<!-- This template displays the overlay content and is connected to the button -->
<sfng-dropdown #dropdown="sfngDropdown" [triggerTemplate]="customTriggerTemplate" class="w-full h-full">
<ul class="item-list">
<li *ngFor="let item of items" (click)="selectItem(item)" [sfng-tooltip]="item.description || null"
snfgTooltipPosition="left" [class.cursor-not-allowed]="item.disabled">
<ng-container *ngTemplateOutlet="item.templateRef"></ng-container>
</li>
</ul>
</sfng-dropdown>

0 comments on commit 789847a

Please sign in to comment.