[Angular] 自建Duel List

在設計介面時, 有時會利用duel list 去顯示現有item 及可以加入的item. 在Angular 中, 可以安裝 angular-dual-listbox 實現. 而它亦可以自己修改設計. 示範中會利用它自建主題及加入add/ remove all 功能.安裝 angular-dual-listbox 指令

npm install angular-dual-listbox

ItemAddRemoveEventArgs

import { BasicList } from 'angular-dual-listbox';

export class ItemAddRemoveEventArgs {
    public items: any;
    constructor() {
        // this.items = new Array<any>();
    }
    public static createItem(targetItem): ItemAddRemoveEventArgs {
        const itemAddRemoveEventArgs: ItemAddRemoveEventArgs = new ItemAddRemoveEventArgs();
        itemAddRemoveEventArgs.items = targetItem;
        return itemAddRemoveEventArgs;
    }
    public static createItems(targetItems: BasicList): ItemAddRemoveEventArgs {
        const itemAddRemoveEventArgs: ItemAddRemoveEventArgs = new ItemAddRemoveEventArgs();
        itemAddRemoveEventArgs.items = new Array<any>();
        itemAddRemoveEventArgs.items.concat(targetItems);
        return itemAddRemoveEventArgs;
    }
}

duel-list-component.scss

div.record-picker {
  overflow-x: hidden;
  overflow-y: auto;
  border: 1px solid #ddd;
  position: relative;
  cursor: pointer;
  font-family: sans-serif;
}

div.record-picker::-webkit-scrollbar {
  width: 12px;
}

div.record-picker::-webkit-scrollbar-button {
  width: 0px;
  height: 0px;
}

// div.record-picker {
// 	scrollbar-base-color: indigo;
// 	scrollbar-3dlight-color: indigo;
// 	scrollbar-highlight-color: indigo;
// 	scrollbar-track-color: #eee;
// 	scrollbar-arrow-color: gray;
// 	scrollbar-shadow-color: gray;
// 	scrollbar-darkshadow-color: gray;
// }

div.record-picker::-webkit-scrollbar-track {
  background:#eee;
  box-shadow: 0px 0px 3px #dfdfdf inset;
}

div.record-picker::-webkit-scrollbar-thumb {
  background: indigo;
  border: thin solid gray;
}

div.record-picker::-webkit-scrollbar-thumb:hover {
  background: purple;
}

.record-picker ul {
  margin: 0;
  padding: 0 0 1px 0;
}

.record-picker li {
  border-top: thin solid #ddd;
  border-bottom: 1px solid #ddd;
  display: block;
  padding: 2px 2px 2px 10px;
  margin-bottom: -1px;
  font-size: 0.85em;
  cursor: pointer;
  white-space: nowrap;
  min-height:16px;
  text-align: left;
}

.record-picker li:hover {
  background-color: #93b1a7;
}

.record-picker li.selected {
  background-color: #93b1a7;
}

.record-picker li.selected:hover {
  background-color: #93b1a7;
}

.record-picker li.disabled {
  opacity: 0.5;
  cursor: default;
}

.record-picker li:first-child {
  border-top: none;
}

.record-picker li:last-child {
  border-bottom: none;
}

.record-picker label {
  cursor: pointer;
  font-weight: inherit;
  font-size: 14px;
  padding: 4px;
  margin-bottom: -1px;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.record-picker ul.over {
  background-color:lightgray;
}

.dual-list {
  display: flex;
  flex-direction: row;
  align-content: flex-start;
  justify-content: space-evenly;
  
}

.dual-list .listbox {
  width: 35%;
  margin: 0px;
  flex-grow: 2;
}

.buttonbox {
  margin: 0 10px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.btn-block {
  display: block;
  width: 70px;
  margin: 4px;
}

button {
  color: white;
  font-size: 18px;
  background: #005643;
  padding: 5px 10px;
  // border: solid black 1px;
  text-decoration: none;
  cursor: pointer;
}

button:hover {
  background:  #93b1a7;
  text-decoration: none;
}

button:disabled {
  background: rgba(0, 85, 67, .5);
  cursor: default;
}

p {
  font-family: sans-serif;
  font-weight: 600;
  margin-bottom: 4px;
}

 

duel-list-component.html

<div class="dual-list">
  <div class="listbox">
    <p>{{sourceName}}</p>
    <div class="record-picker">
      <ul [ngStyle]="{'max-height': height, 'min-height': height}" [ngClass]="{over:available.dragOver}" (drop)="drop($event, confirmed)"
        (dragover)="allowDrop($event, available)" (dragleave)="dragLeave()">
        <li *ngFor="let item of available.sift; let idx=index;" (click)="selectItem(available.pick, item); shiftClick($event, idx, available, item)"
          [ngClass]="{selected: isItemSelected(available.pick, item)}" draggable="true" (dragstart)="drag($event, item, available)"
          (dragend)="dragEnd(available)"><label>{{item._name}}</label></li>
      </ul>
    </div>
  </div>

  <div class="buttonbox">
    <button type="button" class="btn btn-primary btn-block" (click)="addItem()" [disabled]="available.pick.length === 0"><i
        class="fa fa-arrow-right"></i></button>

    <button type="button" class="btn btn-primary btn-block" (click)="addAll()" [disabled]="isAllSelected(available)"><i
        class="fa fa-arrow-right"></i><i class="fa fa-arrow-right"></i></button>


    <button type="button" class="btn btn-primary btn-block" (click)="removeItem()" [disabled]="confirmed.pick.length === 0"><i
        class="fa fa-arrow-left"></i></button>

    <button type="button" class="btn btn-primary btn-block" (click)="removeAll()" [disabled]="isAllSelected(confirmed)"><i
        class="fa fa-arrow-left"></i><i class="fa fa-arrow-left"></i></button>
    <!-- <button type="button" class="btn btn-primary btn-block" (click)="moveItem(available, confirmed)" [disabled]="available.pick.length === 0"><i
        class="fa fa-arrow-right"></i></button>

    <button type="button" class="btn btn-primary btn-block" (click)="moveAll()" [disabled]="isAllSelected(available)"><i
        class="fa fa-arrow-right"></i><i class="fa fa-arrow-right"></i></button>


    <button type="button" class="btn btn-primary btn-block" (click)="moveItem(confirmed, available)" [disabled]="confirmed.pick.length === 0"><i
        class="fa fa-arrow-left"></i></button>

    <button type="button" class="btn btn-primary btn-block" (click)="removeAll()" [disabled]="isAllSelected(confirmed)"><i
        class="fa fa-arrow-left"></i><i class="fa fa-arrow-left"></i></button> -->

  </div>

  <div class="listbox">
    <p>{{targetName}}</p>
    <div class="record-picker">
      <ul [ngStyle]="{'max-height': height, 'min-height': height}" [ngClass]="{over:confirmed.dragOver}" (drop)="drop($event, available)"
        (dragover)="allowDrop($event, confirmed)" (dragleave)="dragLeave()">
        <li *ngFor="let item of confirmed.sift; let idx=index;" (click)="selectItem(confirmed.pick, item); shiftClick($event, idx, confirmed, item)"
          [ngClass]="{selected: isItemSelected(confirmed.pick, item)}" draggable="true" (dragstart)="drag($event, item, confirmed)"
          (dragend)="dragEnd(confirmed)"><label>{{item._name}}</label></li>
      </ul>
    </div>
  </div>
</div>

duel-list-component.ts

import { Component, EventEmitter, OnInit, IterableDiffers, Input, Output } from '@angular/core';
import { DualListComponent } from 'angular-dual-listbox';
import { ItemAddRemoveEventArgs } from './item-add-remove-event-args';
import { Role } from 'src/app/security/role';

@Component({
  selector: 'app-duel-list',
  templateUrl: './duel-list.component.html',
  styleUrls: ['./duel-list.component.scss']
})
export class DuelListComponent extends DualListComponent implements OnInit {

  @Input() sourceName = '';
  @Input() targetName = '';

  @Output() selectChange = new EventEmitter();
  @Output() itemAdd = new EventEmitter();
  @Output() itemRemove = new EventEmitter();

  constructor(differs: IterableDiffers) {
    super(differs);
  }
  ngOnInit() {
  }

  addItem() {
    this.moveItem(this.available, this.confirmed);
    const args: ItemAddRemoveEventArgs = ItemAddRemoveEventArgs.createItem(this.confirmed.list);
    this.itemAdd.emit(args);
  }
  removeItem() {
    this.moveItem(this.confirmed, this.available);
    const args: ItemAddRemoveEventArgs = ItemAddRemoveEventArgs.createItem(this.available.list);
    this.itemRemove.emit(args);
  }

  addAll() {
    this.selectAll(this.available);
    this.moveItem(this.available, this.confirmed);
    const args: ItemAddRemoveEventArgs = ItemAddRemoveEventArgs.createItems(this.confirmed);
    this.itemAdd.emit(args);
  }

  removeAll() {
    this.selectAll(this.confirmed);
    this.moveItem(this.confirmed, this.available);
    // this.itemRemove.emit({ item: this.available });
    const args: ItemAddRemoveEventArgs = ItemAddRemoveEventArgs.createItems(this.available);
    this.itemRemove.emit(args);
  }

  // Override function in DualListComponent to add custom selectChange event.
  selectItem(list: Array<any>, item: any) {
    const pk = list.filter((e: any) => {
      return Object.is(e, item);
    });
    if (pk.length > 0) {
      // Already in list, so deselect.
      for (let i = 0, len = pk.length; i < len; i += 1) {
        const idx = list.indexOf(pk[i]);
        if (idx !== -1) {
          list.splice(idx, 1);
          this.selectChange.emit({ key: item._id, selected: false });
        }
      }
    } else {
      list.push(item);
      this.selectChange.emit({ key: item._id, selected: true });
    }
  }
}

使用方法

<app-duel-list [(source)]="roleSource" [(destination)]="currentRoles" [key]="'roleId'" [display]="'roleCode'"
height="350px"
sourceName="Available Roles" targetName="Selected Roles"
[sort]="true" 
(selectChange)="roleSelectChange($event)"
(itemAdd)="roleItemAdd($event)"
(itemRemove)="roleItemRemove($event)"
></app-duel-list>

 

About C.H. Ling 260 Articles
a .net / Java developer from Hong Kong and currently located in United Kingdom. Thanks for Google because it solve many technical problems so I build this blog as return. Besides coding and trying advance technology, hiking and traveling is other favorite to me, so I will write down something what I see and what I feel during it. Happy reading!!!

2 Comments

Leave a Reply

Your email address will not be published.


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.