Angular组件(二) 分割面板ShrinkSplitter

news2024/9/21 22:50:07

Angular组件(二) 分割面板ShrinkSplitter

前言

在Angular组件(一) 分割面板ShrinkSplitter文章中我们实现了Splitter组件,后来在业务场景中发现在开关右侧容器和底部容器时,使用起来不方便,ngModel绑定的值始终是左侧容器和顶部容器的大小,然而有时我们关注的是右侧容器和底部容器的大小,让左侧自适应。于是修改组件代码,让ngmodel绑定的容器大小和tlColsedMode关联,举例: tlColsedMode = “right”,ngModel绑定的值就是右侧容器的大小。

组件Splitter

module.ts

import { 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 {}

component.ts

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.dragable]': 'tlDragable',
        '[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
    viewRender = false

    @Input()
    tlZIndex = 10
    /** 是否展示收起icon */
    @Input()
    tlShowExpandIcon = true

    /** 收起容器模式,上下左右哪一个容器应用收起展开的状态 */
    @Input()
    tlColsedMode: "left" | "right" | "top" | "bottom" = "left"

    @Input()
    tlMin = "40px"

    @Input()
    tlMax = "40px"

    @Input()
    tlExpandTooltipContent = ""

    @Input()
    tlContractTooltipContent = ""

    /** 是否可拖拽调整大小 */
    @Input()
    tlDragable = true

    get value() {
        return this._value
    }

    set value(val: number | string) {
        if(!this.viewRender && !this.tlExpand){           
            this.expandValueCache = val
            val = 0
        }
  
        this._value = val
        this.onChange(val)
        this.computeOffset()

        setTimeout(() => {
            this.viewRender = true
        }, 0);
    }

    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
            this.value = 0
        } 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.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-transition`] = this.viewRender
        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() {
        if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){           
            this.offset = (this.valueIsPx ? this.px2percent(this.value as string, this.outerWrapper.nativeElement[this.offsetSize]) : this.value) as number * 10000 / 100
        }else{
            this.offset = (this.valueIsPx ? 1 - this.px2percent(this.value as string, this.outerWrapper.nativeElement[this.offsetSize]) : 1- (this.value as number)) 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) {           
            if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){
                value = `${parseFloat(this.oldOffset as string) + offset}px`
            }else{
                value = `${parseFloat(this.oldOffset as string) - offset}px`
            }
        } else {
            if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){
                value = this.px2percent(outerWidth * (this.oldOffset as number) + offset, outerWidth)
            }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) {
        if(!this.tlDragable){
            return
        }
        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 {

    }
}

TlTemplateDirective指令实现

import { 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);
            }
        };
    }
})();

component.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)" 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)" 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>

component.less

@split-prefix-cls: ~"tl-shrink-splitter";
@trigger-bar-background: rgba(23, 35, 61, 0.25);
@trigger-background: #f3f4f7;
@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: 24px;
@trigger-bar-con-width: 24px;
.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;
        }
        &-transition{
            transition: all .3s ease-in; 
        }
    }

    &-trigger{
        border: 1px solid #dcdee2;
        &-con {
            position: absolute;
            transform: translate(-50%, -50%);
            z-index: 10;
        }
        &-bar-con {
            position: absolute;
            overflow: hidden;
            background-image: linear-gradient(90deg, #dcf3fc, #f8fdff);
            border: 1px solid #76b9d6;
            color: #76b9d6;
            &:hover{
                color: #000 !important;
            }
            &.vertical {
                top: 50%;
                left: -6px;
                width: @trigger-bar-con-width;
                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: @trigger-bar-con-width;
                //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.dragable{
    .tl-shrink-splitter-trigger-vertical{
        cursor: col-resize;
    }
    .tl-shrink-splitter-trigger-horizontal{
        cursor: row-resize;
    }
}


.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);
        }
    }
}

页面效果

左右容器和上下容器
image.png

demo

左侧容器

import { Component } from '@angular/core';

@Component({
  selector: 'tl-demo-shrink-splitter-basic',
  template: `
  <button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button>
    <div class="split-box">
      <tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value" (onMoving)="triggerMoveHnadle($event)">
        <ng-template tlTemplate="left">
          <div>左侧区域自定义</div>
        </ng-template>
        <ng-template tlTemplate="right">
          <div>右侧区域自定义</div>
        </ng-template>
      </tl-shrink-splitter>  
    </div>
  `,
  styles: [
    `
      .split-box{
        height: 200px;
        display: flex;
        position: relative;
        overflow: hidden;
      }
      .split-right{
        margin-left: 10px;
        border: 1px solid #e3e3e3;
        flex:1;
      }
    `
  ]
})
export class TlDemoShrinkSplitterBasicComponent {

  expand = true

  value = 0.3

  expandChange(){
    this.expand = !this.expand
  }

  triggerMoveHnadle(e){
    console.log(e);
    console.log(this.value);  
  }
}

image.png

image.png

右侧容器

import { Component } from '@angular/core';

@Component({
  selector: 'tl-demo-shrink-splitter-right',
  template: `
  <button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button>
    <div class="split-box">
      <tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value" tlColsedMode="right">
        <ng-template tlTemplate="left">
          <div>左侧区域自定义</div>
        </ng-template>
        <ng-template tlTemplate="right">
          <div>右侧区域自定义</div>
        </ng-template>
      </tl-shrink-splitter>  
    </div>
  `,
  styles: [
    `
      .split-box{
        height: 300px;
        display: flex;
        position: relative;
        overflow: hidden;
      }
      .split-right{
        margin-left: 10px;
        border: 1px solid #e3e3e3;
        flex:1;
      }
    `
  ]
})
export class TlDemoShrinkSplitterRightComponent {

  expand = true

  value = "200px"

  expandChange(){
    this.expand = !this.expand
  }
}

在这里插入图片描述

顶部容器

import { Component } from '@angular/core';

@Component({
  selector: 'tl-demo-shrink-splitter-top',
  template: `
  <button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button>
    <div class="split-box">
      <tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value"  tlColsedMode="top">
        <ng-template tlTemplate="top">
          <div>顶部区域自定义</div>
        </ng-template>
        <ng-template tlTemplate="bottom">
          <div>底部区域自定义</div>
        </ng-template>
      </tl-shrink-splitter>  
    </div>
  `,
  styles: [
    `
      .split-box{
        height: 300px;
        display: flex;
        position: relative;
        overflow: hidden;
      }
      .split-right{
        margin-left: 10px;
        border: 1px solid #e3e3e3;
        flex:1;
      }
    `
  ]
})
export class TlDemoShrinkSplitterTopComponent {

  expand = true

  value = "100px"

  expandChange(){
    this.expand = !this.expand
  }
}

在这里插入图片描述

底部容器

import { Component } from '@angular/core';

@Component({
    selector: 'tl-demo-shrink-splitter-bottom',
    template: `
    <button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button>
    <div class="split-box">
        <tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value"  tlColsedMode="bottom">
            <ng-template tlTemplate="top">
                <div>顶部区域自定义</div>
            </ng-template>
            <ng-template tlTemplate="bottom">
                <div>底部区域自定义</div>
            </ng-template>
        </tl-shrink-splitter>  
    </div>
  `,
    styles: [
        `
      .split-box{
        height: 300px;
        display: flex;
        position: relative;
        overflow: hidden;
      }
      .split-right{
        margin-left: 10px;
        border: 1px solid #e3e3e3;
        flex:1;
      }
    `
    ]
})
export class TlDemoShrinkSplitterBottomComponent {

    expand = true

    value = "100px"

    expandChange() {
        this.expand = !this.expand
    }
}


在这里插入图片描述

API

tl-shrink-splitter

参数说明类型默认值
[ngModel]面板位置,可以是 0~1 代表百分比,或具体数值的像素,可用 ngModel 双向绑定string | number0.5
[tlExpand]是否展开容器booleantrue
[tlDragable]是否可以拖拽拖拽容器大小booleantrue
[tlZIndex]组件z-indexnumber10
[tlShowExpandIcon]是否展示收起展开切换状态的按钮iconbooleantrue
[tlColsedMode]收起容器模式,上下左右哪一个容器应用收起展开的状态'left' | 'right' | 'top' | 'bottom'left
[tlMin]最小阈值,类型为number时,数值范围为0 ~ 1 表示宽度百分比string | number40px
[tlMax]最大阈值,类型为number时,数值范围为0 ~ 1 表示宽度百分比string| number40px
[tlExpandTooltipContent]展开状态时的tooltip
[tlContractTooltipContent]收起状态时的tooltip

事件

事件名参数描述
tlExpandChangeevt(Boolean)容器展开/收起 change
onMoveStart拖拽开始
onMovingevt(MouseEvent)拖拽中
onMoveEnd拖拽结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1421683.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电路分享 —— 单片机 1个IO口检测多路按键

一、设计要求 设计要求&#xff1a;因为单片机的IO口数量较少&#xff0c;要求使用1路IO口检测4路按键。 二、设计思路 设计思路&#xff1a;单片机的IO口数量较少的情况下&#xff0c;使用1路IO口检测4路按键&#xff0c;可以通过检测电压值来进行区分&#xff0c;即使用IO口…

如何纯前端实现文件下载

业务场景 有一个下载文件的功能&#xff0c;不引入后端资源&#xff0c;纯前端应该如何实现&#xff1f; 解决方案 在vue2或者vue3项目中&#xff0c;可以把文件放在 public 文件夹下&#xff0c;然后使用a标签进行文件下载。 如&#xff1a;我要下载的文件是模版.xlsx 。首…

STM32学习笔记二——STM32时钟源时钟树

目录 STM32芯片内部系统架构详细讲解&#xff1a; 1.芯片内部混乱电信号解决方案&#xff1a; 2.时钟树&#xff1a; 1.内部RC振荡器与外部晶振的选择 2. STM32 时钟源 3.STM32中几个与时钟相关的概念 4.时钟输出的使能及其流程 5.时钟设置的基本流程 时钟源——单片机…

应用下载页源码简洁高大尚下载单页

首先可以设置多个下载源 软件截图可以滚动 暗夜主题交换 图片都可以自定义配置 精美的页面会让你眼前一亮的 源代码学习资料&#xff1a;https://pan.baidu.com/s/1ulE9zde2XMs-6E7eF3D3kg?pwdfhxq 密码&#xff1a;123

1178:单词数

题目描述 统计一篇文章里不同单词的总数。 输入 有多组数据&#xff0c;每组一行&#xff0c;每组就是一篇小文章。每篇小文章都是由大小写字母和空格组成&#xff0c;没有标点符号&#xff0c;遇到#时表示输入结束。每篇文章的单词数小于1000&#xff0c;每个单词最多由30个…

系统分析师-22年-下午题目

系统分析师-22年-下午题目 更多软考知识请访问 https://ruankao.blog.csdn.net/ 试题一必答&#xff0c;二、三、四、五题中任选其中两题作答 试题一 (25分) 说明 某软件公司拟开发一套博客系统&#xff0c;要求能够向用户提供一个便捷发布自已心得&#xff0c;及时有效的…

Cmake编译Opencv3.3.1遇到有些文件无法下载的错误解决:

前言&#xff1a; 对于&#xff0c;opencv有些配置文件错误并未致命&#xff0c;所以&#xff0c;有错误也不影响后续的编译&#xff1a;但是&#xff0c;后引用如果要用&#xff0c;在回过头来还是要解决的。 问题表述&#xff1a; 比如&#xff0c;有些文件下载的错误&am…

穷游网酒店数据采集与可视化分析与实现

摘 要 穷游网酒店数据采集与可视化分析大屏的背景是为了满足用户对酒店数据的需求以及提供数据洞察和决策支持。随着旅游业的快速发展&#xff0c;人们对酒店信息的需求日益增加&#xff0c;而穷游网作为一家专注于旅游信息的网站&#xff0c;拥有丰富的酒店数据资源。 这个大…

JS基础 - 遍历对象方法(6种)

初始值&#xff1a; var obj {a: 1,b: 2,c: 3,d: 4,e: 5,}; 第一种&#xff1a;for in for (let key in obj) {console.log(key ":" obj[key]);} 第二种&#xff1a;Object.keys 获取key Object.keys(obj).forEach((key) > {console.log(key ":" …

【乳腺肿瘤诊断分类及预测】基于Elman神经网络

课题名称&#xff1a;基于Elman神经网络的乳腺肿瘤诊断分类及预测 版本日期&#xff1a;2023-05-15 运行方式: 直接运行Elman0501.m 文件即可 代码获取方式&#xff1a;私信博主或QQ&#xff1a;491052175 模型描述&#xff1a; 威斯康辛大学医学院经过多年的收集和整理&a…

关于TongWeb部署应用报错java.lang.NoSuchFieldError: REFLECTION (by liuhui)

关于TongWeb部署应用报错java.lang.NoSuchFieldError: REFLECTION &#xff08;by liuhui&#xff09; 关于TongWeb部署应用报错java.lang.NoSuchFieldError: REFLECTION 问题现象&#xff1a;xml解析对象工厂类错误导致解析失败 解决办法&#xff1a;增加配置参数问题解决 -…

学习鸿蒙基础(2)

arkts是声名式UI DevEcoStudio的右侧预览器可以预览。有个TT的图标可以看布局的大小。和html的布局浏览很像。 上图布局对应的代码&#xff1a; Entry //入口 Component struct Index {State message: string Hello Harmonyos //State 数据改变了也刷新的标签build() {Row()…

MATLAB知识点:MATLAB的文件管理

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第2章 上一章我们说过&#xff0c;MATLAB是一款非常强…

opencv-python计算视频光流

光流基本概念 光流表示的是相邻两帧图像中每个像素的运动速度和运动方向。具体&#xff1a;光流是空间运动物体在观察成像平面上的像素运动的瞬时速度&#xff0c;是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系&#xf…

软考复习之数据结构篇

算法设计 迭代法&#xff1a;用于求方程的近似根。 1、若方程无解&#xff0c;则算法求出的近似根序列就不会收敛&#xff0c;迭代过程会变成死循环&#xff0c;因此在使用迭代算法前应先考查方程是否有解&#xff0c;并在程序中对迭代的次数给予限制。 2、方程虽有解&#…

B3626 跳跃机器人——洛谷(疑问)

题目描述 地上有一排格子&#xff0c;共 &#xfffd;n 个位置。机器猫站在第一个格子上&#xff0c;需要取第 &#xfffd;n 个格子里的东西。 机器猫当然不愿意自己跑过去&#xff0c;所以机器猫从口袋里掏出了一个机器人&#xff01;这个机器人的行动遵循下面的规则&#…

使用 Paimon + StarRocks 极速批流一体湖仓分析

摘要&#xff1a;本文整理自阿里云智能高级开发工程师王日宇&#xff0c;在 Flink Forward Asia 2023 流式湖仓&#xff08;二&#xff09;专场的分享。本篇内容主要分为以下四部分&#xff1a; StarRocksPaimon 湖仓分析的发展历程使用 StarRocksPaimon 进行湖仓分析主要场景和…

读AI3.0笔记10_读后总结与感想兼导读

1. 基本信息 AI 3.0 (美)梅拉妮米歇尔 著 四川科学技术出版社,2021年2月出版 1.1. 读薄率 书籍总字数355千字&#xff0c;笔记总字数33830字。 读薄率33830355000≈9.53% 1.2. 读厚方向 千脑智能 脑机穿越 未来呼啸而来 虚拟人 新机器人 如何创造可信的AI 新机器智…

【极数系列】Flink集成DataSource读取Socket请求数据(09)

文章目录 01 引言02 简介概述03 基于socket套接字读取数据3.1 从套接字读取。元素可以由分隔符分隔。3.2 windows安装netcat工具&#xff08;1&#xff09;下载netcat工具&#xff08;2&#xff09;安装部署&#xff08;3&#xff09;启动socket端口监听 04 源码实战demo4.1 po…

回归预测 | Matlab实现CPO-SVR冠豪猪优化支持向量机的数据多输入单输出回归预测

回归预测 | Matlab实现CPO-SVR冠豪猪优化支持向量机的数据多输入单输出回归预测 目录 回归预测 | Matlab实现CPO-SVR冠豪猪优化支持向量机的数据多输入单输出回归预测预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab实现CPO-SVR冠豪猪优化支持向量机的数据多输入…