分割面板在日常开发中经常使用,可将一片区域,分割为可以拖拽整宽度或高度的两部分区域。模仿iview的分割面板组件,用angular实现该功能,支持拖拽和[(ngModel)]双向绑定的方式控制区域的展示收起和拖拽功能。
tsimport { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { TlShrinkSplitterComponent } from "./shrink-splitter.component";
import{NzToolTipModule} from "ng-zorro-antd/tooltip"
const COMMENT = [TlShrinkSplitterComponent];
@NgModule({
declarations: [...COMMENT],
exports: [...COMMENT],
imports: [
CommonModule,
NzToolTipModule,
]
})
export class TlShrinkSplitterModule {}
import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, QueryList, TemplateRef, ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { TlTemplateDirective } from "topdsm-lib/common" import { isFalsy } from "topdsm-lib/core/util"; import { off, on } from "./util"; @Component({ selector: "tl-shrink-splitter", templateUrl: "./shrink-splitter.component.html", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TlShrinkSplitterComponent), multi: true } ], host: { class: "tl-shrink-splitter", '[class.expand]': 'tlExpand', '[class.contract]': '!tlExpand', '[class.contract-left]': 'tlColsedMode === "left"', '[class.contract-right]': 'tlColsedMode === "right"', '[class.contract-top]': 'tlColsedMode === "top"', '[class.contract-bottom]': 'tlColsedMode === "bottom"', '[style.z-index]': 'tlZIndex', } }) export class TlShrinkSplitterComponent implements OnInit, AfterContentInit, AfterViewInit, ControlValueAccessor { prefix = "tl-shrink-splitter" offset = 0 oldOffset: number | string = 0 isMoving = false initOffset = 0 _value: number | string = 0.5 isOpen = true @Input() tlZIndex = 10 // @Input() // tlMode: "horizontal" | "vertical" = "horizontal" /** 是否展示收起icon */ @Input() tlShowExpandIcon = true /** 收起容器模式,上下左右哪一个容器应用收起展开的状态 */ @Input() tlColsedMode: "left" | "right" | "top" | "bottom" = "left" @Input() tlMin = "40px" @Input() tlMax = "40px" @Input() tlExpandTooltipContent = "" @Input() tlContractTooltipContent = "" get value() { return this._value } set value(val: number | string) { this._value = val this.onChange(val) this.computeOffset() } expandValueCache: string | number = 0 /** 展开状态 */ get tlExpand() { return this.isOpen; } @Input() set tlExpand(val: boolean) { if (val !== this.isOpen) { this.isOpen = val; this.tlExpandChange.emit(val); this.changeExpand(val) } } /** 容器展开状态切换 */ changeExpand(status: boolean) { if (!status) { // 收起 this.expandValueCache = this.value if (this.tlColsedMode === "left") { this.value = 0 } else if (this.tlColsedMode === "right") { this.value = 1 } else if (this.tlColsedMode === "top") { this.value = 0 } else if (this.tlColsedMode === "bottom") { this.value = 1 } } else { // 展开 this.value = this.expandValueCache this.expandValueCache = 0 } } /** 展开收缩切换事件 */ @Output() readonly tlExpandChange = new EventEmitter<boolean>(); @Output() readonly onMoveStart = new EventEmitter(); @Output() readonly onMoving = new EventEmitter<MouseEvent>(); @Output() readonly onMoveEnd = new EventEmitter(); expandChange(e: MouseEvent) { e.stopPropagation(); e.preventDefault() this.tlExpand = !this.isOpen } @ContentChildren(TlTemplateDirective) templates?: QueryList<TlTemplateDirective> leftTemplate?: TemplateRef<void> | null = null rightTemplate?: TemplateRef<void> | null = null topTemplate?: TemplateRef<void> | null = null bottomTemplate?: TemplateRef<void> | null = null @ViewChild('outerWrapper') outerWrapper: ElementRef; get isHorizontal() { //return this.tlMode === 'horizontal'; return this.tlColsedMode === "left" || this.tlColsedMode === "right" } get computedMin() { return this.getComputedThresholdValue('tlMin'); } get computedMax() { return this.getComputedThresholdValue('tlMax'); } get anotherOffset() { return 100 - this.offset; } get valueIsPx() { return typeof this.value === 'string'; } get offsetSize() { return this.isHorizontal ? 'offsetWidth' : 'offsetHeight'; } get paneClasses() { let classes = {} classes[`${this.prefix}-pane`] = true classes[`${this.prefix}-pane-moving`] = this.isMoving return classes } /** 展开收起触发器icon */ get triggrrClass() { let classes = {} if (this.tlColsedMode === "left" && this.isOpen) { classes["icon-caret-left"] = true } else if (this.tlColsedMode === "left" && !this.isOpen) { classes["icon-caret-right"] = true } else if (this.tlColsedMode === "right" && this.isOpen) { classes["icon-caret-right"] = true } else if (this.tlColsedMode === "right" && !this.isOpen) { classes["icon-caret-left"] = true } else if (this.tlColsedMode === "top" && this.isOpen) { classes["icon-caret-left"] = true } else if (this.tlColsedMode === "top" && !this.isOpen) { classes["icon-caret-right"] = true } else if (this.tlColsedMode === "bottom" && this.isOpen) { classes["icon-caret-right"] = true } else if (this.tlColsedMode === "bottom" && !this.isOpen) { classes["icon-caret-left"] = true } return classes } get tooltipPosition() { let position = "right" if (this.tlColsedMode === "right" && !this.isOpen) { position = "left" } return position } get tooltipContent() { let tooltip = "" if (this.tlColsedMode === "left" && this.isOpen) { tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起左侧内容" : this.tlExpandTooltipContent } else if (this.tlColsedMode === "left" && !this.isOpen) { tooltip = isFalsy(this.tlContractTooltipContent) ? "展开左侧内容" : this.tlContractTooltipContent } else if (this.tlColsedMode === "right" && this.isOpen) { tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起右侧内容" : this.tlExpandTooltipContent } else if (this.tlColsedMode === "right" && !this.isOpen) { tooltip = isFalsy(this.tlContractTooltipContent) ? "展开右侧内容" : this.tlContractTooltipContent } else if (this.tlColsedMode === "top" && this.isOpen) { tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起顶部内容" : this.tlExpandTooltipContent } else if (this.tlColsedMode === "top" && !this.isOpen) { tooltip = isFalsy(this.tlContractTooltipContent) ? "展开顶部内容" : this.tlContractTooltipContent } else if (this.tlColsedMode === "bottom" && this.isOpen) { tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起底部内容" : this.tlExpandTooltipContent } else if (this.tlColsedMode === "bottom" && !this.isOpen) { tooltip = isFalsy(this.tlContractTooltipContent) ? "展开底部内容" : this.tlContractTooltipContent } return tooltip } px2percent(numerator: string | number, denominator: string | number) { return parseFloat(numerator + "") / parseFloat(denominator + ""); } computeOffset() { this.offset = (this.valueIsPx ? this.px2percent(this.value as string, this.outerWrapper.nativeElement[this.offsetSize]) : this.value) as number * 10000 / 100 } getComputedThresholdValue(type) { let size = this.outerWrapper.nativeElement[this.offsetSize]; if (this.valueIsPx) return typeof this[type] === 'string' ? this[type] : size * this[type]; else return typeof this[type] === 'string' ? this.px2percent(this[type], size) : this[type]; } getMin(value1, value2) { if (this.valueIsPx) return `${Math.min(parseFloat(value1), parseFloat(value2))}px`; else return Math.min(value1, value2); } getMax(value1, value2) { if (this.valueIsPx) return `${Math.max(parseFloat(value1), parseFloat(value2))}px`; else return Math.max(value1, value2); } getAnotherOffset(value) { let res: string | number = 0; if (this.valueIsPx) res = `${this.outerWrapper.nativeElement[this.offsetSize] - parseFloat(value)}px`; else res = 1 - value; return res; } handleMove = (e) => { let pageOffset = this.isHorizontal ? e.pageX : e.pageY; let offset = pageOffset - this.initOffset; let outerWidth = this.outerWrapper.nativeElement[this.offsetSize]; let value: string | number = "" if (this.valueIsPx) { value = `${parseFloat(this.oldOffset as string) + offset}px` } else { value = this.px2percent(outerWidth * (this.oldOffset as number) + offset, outerWidth) } let anotherValue = this.getAnotherOffset(value); if (parseFloat(value + "") <= parseFloat(this.computedMin + "")) value = this.getMax(value, this.computedMin); if (parseFloat(anotherValue + "") <= parseFloat(this.computedMax)) value = this.getAnotherOffset(this.getMax(anotherValue, this.computedMax)); e.atMin = this.value === this.computedMin; e.atMax = this.valueIsPx ? this.getAnotherOffset(this.value) === this.computedMax : (this.getAnotherOffset(this.value) as number).toFixed(5) === this.computedMax.toFixed(5); this.value = value this.onMoving.emit(e) } handleUp = (e) => { this.isMoving = false; off(document, 'mousemove', this.handleMove); off(document, 'mouseup', this.handleUp); this.onMoveEnd.emit() } onTriggerMouseDown(e) { this.initOffset = this.isHorizontal ? e.pageX : e.pageY; this.oldOffset = this.value; this.isMoving = true; on(document, 'mousemove', this.handleMove); on(document, 'mouseup', this.handleUp); this.onMoveStart.emit() } constructor(private cdr: ChangeDetectorRef) { } ngOnInit(): void { console.log("ngOnInit"); } ngAfterViewInit(): void { console.log("ngAfterViewInit"); this.computeOffset() } ngAfterContentInit() { this.templates?.forEach((item) => { switch (item.getType()) { case 'left': this.leftTemplate = item.template; break; case 'right': this.rightTemplate = item.template; break; case 'top': this.topTemplate = item.template; break; case 'bottom': this.bottomTemplate = item.template; break; default: this.leftTemplate = item.template; break; } }); } // 输入框数据变化时 onChange: (value: any) => void = () => null; onTouched: () => void = () => null; writeValue(val: number | string): void { if (val !== this.value) { this.value = val this.computeOffset(); this.cdr.markForCheck(); } } // UI界面值发生更改,调用注册的回调函数 registerOnChange(fn: any): void { this.onChange = fn; } // 在blur(等失效事件),调用注册的回调函数 registerOnTouched(fn: any): void { this.onTouched = fn; } // 设置禁用状态 setDisabledState?(isDisabled: boolean): void { } }
tsimport { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core";
import { NzSafeAny } from "topdsm-lib/core/types";
@Directive({
selector: '[tlTemplate]'
})
export class TlTemplateDirective {
@Input('tlTemplate')
name: string = "default"
// @Input()
// type: string = ""
constructor(private viewContainer: ViewContainerRef, public template: TemplateRef<NzSafeAny>) {
//this.template = templateRef;
}
ngOnInit(): void {
this.viewContainer.createEmbeddedView(this.template)
}
getType() {
return this.name;
}
}
export const on = (function() { if (document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; } })(); export const off = (function() { if (document.removeEventListener) { return function(element, event, handler) { if (element && event) { element.removeEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event) { element.detachEvent('on' + event, handler); } }; } })();
html<div [ngClass]="prefix + '-wrapper'" #outerWrapper>
<div [ngClass]="prefix + '-horizontal'" *ngIf="isHorizontal; else verticalSlot">
<div class="left-pane" [ngStyle]="{right: anotherOffset + '%'}" [ngClass]="paneClasses">
<ng-container *ngTemplateOutlet="leftTemplate"></ng-container>
</div>
<div [ngClass]="prefix + '-trigger-con'" [ngStyle]="{left: offset + '%'}" (mousedown)="onTriggerMouseDown($event)">
<div ngClass="tl-shrink-splitter-trigger tl-shrink-splitter-trigger-vertical" >
<!-- <span class="tl-shrink-splitter-trigger-bar-con vertical" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" [tTooltip]="tooltipContent" [tooltipPosition]="tooltipPosition" *ngIf="tlShowExpandIcon"></span> -->
<span class="tl-shrink-splitter-trigger-bar-con vertical" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" nz-tooltip [nzTooltipTitle]="tooltipContent" [nzTooltipPlacement]="tooltipPosition" *ngIf="tlShowExpandIcon"></span>
</div>
</div>
<div class="right-pane" [ngStyle]="{left: offset + '%'}" [ngClass]="paneClasses">
<ng-container *ngTemplateOutlet="rightTemplate"></ng-container>
</div>
</div>
<ng-template #verticalSlot>
<div [ngClass]="prefix + '-vertical'" >
<div class="top-pane" [ngStyle]="{bottom: anotherOffset + '%'}" [ngClass]="paneClasses">
<ng-container *ngTemplateOutlet="topTemplate"></ng-container>
</div>
<div [ngClass]="prefix + '-trigger-con'" [ngStyle]="{top: offset + '%'}" (mousedown)="onTriggerMouseDown($event)">
<div ngClass="tl-shrink-splitter-trigger tl-shrink-splitter-trigger-horizontal" >
<!-- <span class="tl-shrink-splitter-trigger-bar-con horizontal" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" [tTooltip]="tooltipContent" [tooltipPosition]="tooltipPosition" *ngIf="tlShowExpandIcon"></span> -->
<span class="tl-shrink-splitter-trigger-bar-con horizontal" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" nz-tooltip [nzTooltipTitle]="tooltipContent" [nzTooltipPlacement]="tooltipPosition" *ngIf="tlShowExpandIcon"></span>
</div>
</div>
<div class="bottom-pane" [ngStyle]="{top: offset + '%'}" [ngClass]="paneClasses">
<ng-container *ngTemplateOutlet="bottomTemplate"></ng-container>
</div>
</div>
</ng-template>
</div>
less@split-prefix-cls: ~"tl-shrink-splitter";
@trigger-bar-background: rgba(23, 35, 61, 0.25);
@trigger-background: #f8f8f9;
@trigger-width: 8px;
@trigger-bar-width: 4px;
@trigger-bar-offset: (@trigger-width - @trigger-bar-width) / 2;
@trigger-bar-interval: 3px;
@trigger-bar-weight: 1px;
@trigger-bar-con-height: 20px;
.tl-shrink-splitter{
position: relative;
height: 100%;
width: 100%;
}
.tl-shrink-splitter-wrapper{
position: relative;
height: 100%;
width: 100%;
}
.@{split-prefix-cls}{
background-color: #fff;
border: 1px solid #dee2e6;
&-pane{
position: absolute;
transition: all .3s ease-in;
padding: 8px;
&.tl-shrink-splitter-pane-moving{
transition: none;
}
&.left-pane, &.right-pane {
top: 0;
bottom: 0;
}
&.left-pane {
left: 0;
}
&.right-pane {
right: 0;
padding-left: 16px;
}
&.top-pane, &.bottom-pane {
left: 0;
right: 0;
}
&.top-pane {
top: 0;
}
&.bottom-pane {
bottom: 0;
padding-top: 16px;
}
&-moving{
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
&-trigger{
border: 1px solid #dcdee2;
&-con {
position: absolute;
transform: translate(-50%, -50%);
z-index: 10;
}
&-bar-con {
position: absolute;
overflow: hidden;
&:hover{
color: #000 !important;
}
&.vertical {
top: 50%;
left: -6px;
width: 20px;
height: @trigger-bar-con-height;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #b2b2b2;
font-size: 14px;
cursor: pointer;
}
&.horizontal {
left: 50%;
top: -4px;
width: @trigger-bar-con-height;
height: 20px;
//transform: translate(-50%, 0);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #b2b2b2;
font-size: 14px;
cursor: pointer;
}
}
&-vertical {
width: @trigger-width;
height: 100%;
background: @trigger-background;
border-top: none;
border-bottom: none;
cursor: col-resize;
.@{split-prefix-cls}-trigger-bar {
width: @trigger-bar-width;
height: 1px;
background: @trigger-bar-background;
float: left;
margin-top: @trigger-bar-interval;
}
}
&-horizontal {
height: @trigger-width;
width: 100%;
background: @trigger-background;
border-left: none;
border-right: none;
cursor: row-resize;
.@{split-prefix-cls}-trigger-bar {
height: @trigger-bar-width;
width: 1px;
background: @trigger-bar-background;
float: left;
margin-right: @trigger-bar-interval;
}
}
}
&-horizontal {
.@{split-prefix-cls}-trigger-con {
top: 50%;
height: 100%;
width: 0;
}
}
&-vertical {
.@{split-prefix-cls}-trigger-con {
left: 50%;
height: 0;
width: 100%;
}
}
}
.tl-shrink-splitter.contract{
.tl-shrink-splitter-trigger-vertical{
width: 0;
padding-left: 0;
}
.tl-shrink-splitter-trigger-horizontal{
height: 0;
padding-top: 0;
}
.tl-shrink-splitter-trigger{
border: 0;
}
&.contract-left{
.tl-shrink-splitter-pane.left-pane{
width: 0;
padding: 0;
overflow: hidden;
}
.right-pane{
padding-left: 8px;
}
}
.tl-shrink-splitter-trigger-bar-con{
&.vertical{
left: -6px;
}
}
&.contract-right{
.tl-shrink-splitter-trigger-bar-con{
&.vertical{
left: -16px;
}
}
}
&.contract-top{
.tl-shrink-splitter-pane.top-pane{
overflow: hidden;
height: 0;
padding: 0;
}
.bottom-pane{
padding-top: 8px;
}
.tl-shrink-splitter-trigger-bar-con.horizontal{
transform: rotate(90deg);
}
}
&.contract-bottom{
.tl-shrink-splitter-trigger-bar-con{
&.horizontal{
top: -16px;
}
}
.tl-shrink-splitter-pane.bottom-pane{
overflow: hidden;
height: 0;
padding: 0;
}
.top-pane{
padding-top: 8px;
}
.tl-shrink-splitter-trigger-bar-con.horizontal{
transform: rotate(90deg);
}
}
}
.tl-shrink-splitter.expand{
.tl-shrink-splitter-trigger-bar-con{
&.vertical{
left: -8px;
}
}
&.contract-top{
.tl-shrink-splitter-trigger-bar-con.horizontal{
transform: rotate(90deg);
}
}
&.contract-bottom{
.tl-shrink-splitter-trigger-bar-con.horizontal{
transform: rotate(90deg);
}
}
}





本文作者:千寻
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!