From 84895cb1435b524a46a3af6c0c5eac5222022460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=89=B2?= Date: Fri, 17 Nov 2017 14:25:31 +0800 Subject: [PATCH] feat(module:transfer): add transfer component (#578) export transfer component. fix code review issues. related #132 --- src/components/locale/locales/en-US.ts | 8 + src/components/locale/locales/zh-CN.ts | 8 + src/components/locale/nz-locale.class.ts | 8 + src/components/ng-zorro-antd.module.ts | 6 +- src/components/transfer/item.ts | 8 + .../transfer/nz-transfer-list.component.ts | 178 ++++++++++++++++ .../transfer/nz-transfer-search.component.ts | 35 ++++ .../transfer/nz-transfer.component.ts | 176 ++++++++++++++++ src/components/transfer/nz-transfer.module.ts | 18 ++ src/components/transfer/nz-transfer.spec.ts | 195 ++++++++++++++++++ src/components/transfer/style/index.less | 166 +++++++++++++++ src/components/transfer/style/patch.less | 18 ++ .../nz-demo-transfer-advanced.component.ts | 56 +++++ .../nz-demo-transfer-basic.component.ts | 36 ++++ .../nz-demo-transfer-custom-item.component.ts | 47 +++++ .../nz-demo-transfer-search.component.ts | 45 ++++ .../nz-demo-transfer.component.ts | 20 ++ .../nz-demo-transfer/nz-demo-transfer.html | 192 +++++++++++++++++ .../nz-demo-transfer.module.ts | 21 ++ .../nz-demo-transfer.routing.module.ts | 12 ++ src/showcase/router.ts | 10 + 21 files changed, 1262 insertions(+), 1 deletion(-) create mode 100644 src/components/transfer/item.ts create mode 100644 src/components/transfer/nz-transfer-list.component.ts create mode 100644 src/components/transfer/nz-transfer-search.component.ts create mode 100644 src/components/transfer/nz-transfer.component.ts create mode 100644 src/components/transfer/nz-transfer.module.ts create mode 100644 src/components/transfer/nz-transfer.spec.ts create mode 100644 src/components/transfer/style/index.less create mode 100644 src/components/transfer/style/patch.less create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer-advanced.component.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer-basic.component.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer-custom-item.component.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer-search.component.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer.component.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer.html create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer.module.ts create mode 100644 src/showcase/nz-demo-transfer/nz-demo-transfer.routing.module.ts diff --git a/src/components/locale/locales/en-US.ts b/src/components/locale/locales/en-US.ts index e17c5952b1f..04cff08cafd 100644 --- a/src/components/locale/locales/en-US.ts +++ b/src/components/locale/locales/en-US.ts @@ -52,4 +52,12 @@ export const enUS: NzLocale = { Select: { notFoundContent: 'Not Found', }, + + Transfer: { + titles: ',', + notFoundContent: 'Not Found', + searchPlaceholder: 'Search here', + itemUnit: 'item', + itemsUnit: 'items', + } }; diff --git a/src/components/locale/locales/zh-CN.ts b/src/components/locale/locales/zh-CN.ts index 437d8708696..9984753a68e 100644 --- a/src/components/locale/locales/zh-CN.ts +++ b/src/components/locale/locales/zh-CN.ts @@ -52,4 +52,12 @@ export const zhCN: NzLocale = { Select: { notFoundContent: '无法找到', }, + + Transfer: { + titles: ',', + notFoundContent: '无匹配结果', + searchPlaceholder: '请输入', + itemUnit: '项目', + itemsUnit: '项目', + } }; diff --git a/src/components/locale/nz-locale.class.ts b/src/components/locale/nz-locale.class.ts index 62d01254975..dee9fca5ce8 100644 --- a/src/components/locale/nz-locale.class.ts +++ b/src/components/locale/nz-locale.class.ts @@ -50,4 +50,12 @@ export class NzLocale { Select: { notFoundContent: string; }; + + Transfer: { + titles: string, + notFoundContent: string, + searchPlaceholder: string, + itemUnit: string, + itemsUnit: string, + }; } diff --git a/src/components/ng-zorro-antd.module.ts b/src/components/ng-zorro-antd.module.ts index 8bee90d1484..b694324664c 100644 --- a/src/components/ng-zorro-antd.module.ts +++ b/src/components/ng-zorro-antd.module.ts @@ -53,6 +53,7 @@ import { NzBackTopModule } from './back-top/nz-back-top.module'; import { NzAffixModule } from './affix/nz-affix.module'; import { NzAnchorModule } from './anchor/nz-anchor.module'; import { NzAvatarModule } from './avatar/nz-avatar.module'; +import { NzTransferModule } from './transfer/nz-transfer.module'; // Services import { NzNotificationService } from './notification/nz-notification.service'; @@ -118,6 +119,7 @@ export { NzBackTopModule } from './back-top/nz-back-top.module'; export { NzAffixModule } from './affix/nz-affix.module'; export { NzAnchorModule } from './anchor/nz-anchor.module'; export { NzAvatarModule } from './avatar/nz-avatar.module'; +export { NzTransferModule } from './transfer/nz-transfer.module'; // Components export { NzRowComponent } from './grid/nz-row.component'; @@ -222,6 +224,7 @@ export { NzAffixComponent } from './affix/nz-affix.component'; export { NzAnchorLinkComponent } from './anchor/nz-anchor-link.component'; export { NzAnchorComponent } from './anchor/nz-anchor.component'; export { NzAvatarComponent } from './avatar/nz-avatar.component'; +export { NzTransferComponent } from './transfer/nz-transfer.component'; // Services export { NzNotificationService } from './notification/nz-notification.service'; @@ -286,7 +289,8 @@ export { NZ_ROOT_CONFIG, NzRootConfig } from './root/nz-root-config'; NzBackTopModule, NzAffixModule, NzAnchorModule, - NzAvatarModule + NzAvatarModule, + NzTransferModule ] }) export class NgZorroAntdModule { diff --git a/src/components/transfer/item.ts b/src/components/transfer/item.ts new file mode 100644 index 00000000000..76a505aa4d2 --- /dev/null +++ b/src/components/transfer/item.ts @@ -0,0 +1,8 @@ +export interface TransferItem { + title: string; + direction?: 'left' | 'right'; + disabled?: boolean; + checked?: boolean; + _hiden?: boolean; + [key: string]: any; +} diff --git a/src/components/transfer/nz-transfer-list.component.ts b/src/components/transfer/nz-transfer-list.component.ts new file mode 100644 index 00000000000..d96a811fd32 --- /dev/null +++ b/src/components/transfer/nz-transfer-list.component.ts @@ -0,0 +1,178 @@ +// tslint:disable:member-ordering +import { Component, Input, ContentChild, TemplateRef, Renderer2, ElementRef, OnChanges, SimpleChanges, Output, EventEmitter, OnInit, DoCheck, IterableDiffers, IterableDiffer } from '@angular/core'; +import { TransferItem } from './item'; + +@Component({ + selector: 'nz-transfer-list', + template: ` +
+ + {{ (stat.checkCount > 0 ? stat.checkCount + '/' : '') + stat.shownCount}} {{_list.length > 1 ? itemsUnit : itemUnit}} + {{titleText}} + +
+
+
+ +
+ +
{{notFoundContent}}
+
+ + ` +}) +export class NzTransferListComponent implements OnChanges, OnInit, DoCheck { + + // private + _list: TransferItem[] = []; + + // region: fields + + @Input() direction = ''; + @Input() titleText = ''; + + @Input() + set dataSource(list: TransferItem[]) { + this._list = list; + this.updateCheckStatus(); + } + + @Input() itemUnit = ''; + @Input() itemsUnit = ''; + @Input() filter = ''; + // search + @Input() showSearch: boolean; + @Input() searchPlaceholder: string; + @Input() notFoundContent: string; + @Input() filterOption: (inputValue: any, item: any) => boolean; + + @Input() render: TemplateRef; + @Input() footer: TemplateRef; + + // events + @Output() handleSelectAll: EventEmitter = new EventEmitter(); + @Output() handleSelect: EventEmitter = new EventEmitter(); + @Output() filterChange: EventEmitter = new EventEmitter(); + + // endregion + + // region: styles + + _prefixCls = 'ant-transfer-list'; + _classList: string[] = []; + + _setClassMap() { + this._classList.forEach(cls => this._renderer.removeClass(this._el.nativeElement, cls)); + + this._classList = [ + this._prefixCls, + !!this.footer && `${this._prefixCls}-with-footer` + ].filter(item => !!item); + + this._classList.forEach(cls => this._renderer.addClass(this._el.nativeElement, cls)); + } + + // endregion + + // region: select all + stat = { + checkAll: false, + checkHalf: false, + checkCount: 0, + shownCount: 0 + }; + + onHandleSelectAll() { + this._list.forEach(item => { + if (!item.disabled) { + item.checked = this.stat.checkAll; + } + }); + this.updateCheckStatus(); + + this.handleSelectAll.emit(this.stat.checkAll); + } + + private updateCheckStatus() { + const validCount = this._list.filter(w => !w.disabled).length; + this.stat.checkCount = this._list.filter(w => w.checked && !w.disabled).length; + this.stat.shownCount = this._list.filter(w => !w._hiden).length; + this.stat.checkAll = validCount > 0 && validCount === this.stat.checkCount; + this.stat.checkHalf = this.stat.checkCount > 0 && !this.stat.checkAll; + } + + // endregion + + // region: search + + handleFilter(value: string) { + this._list.forEach(item => { + item._hiden = value.length > 0 && !this.matchFilter(value, item); + }); + this.stat.shownCount = this._list.filter(w => !w._hiden).length; + this.filterChange.emit({ direction: this.direction, value }); + } + + handleClear() { + this.handleFilter(''); + } + + private matchFilter(text: string, item: TransferItem) { + if (this.filterOption) { + return this.filterOption(text, item); + } + return item.title.includes(text); + } + + // endregion + + _listDiffer: IterableDiffer<{}>; + constructor(private _el: ElementRef, private _renderer: Renderer2, differs: IterableDiffers) { + this._listDiffer = differs.find([]).create(null); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('footer' in changes) { + this._setClassMap(); + } + } + + ngOnInit() { + this._setClassMap(); + } + + ngDoCheck(): void { + const change = this._listDiffer.diff(this._list); + if (change) { + this.updateCheckStatus(); + } + } + + _handleSelect(item: TransferItem) { + if (item.disabled) { + return; + } + item.checked = !item.checked; + this.updateCheckStatus(); + this.handleSelect.emit(item); + } +} diff --git a/src/components/transfer/nz-transfer-search.component.ts b/src/components/transfer/nz-transfer-search.component.ts new file mode 100644 index 00000000000..7f029b0c79a --- /dev/null +++ b/src/components/transfer/nz-transfer-search.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'nz-transfer-search', + template: ` + + + + + + ` +}) +export class NzTransferSearchComponent { + + // region: fields + + @Input() placeholder: string; + @Input() value: string; + + @Output() valueChanged = new EventEmitter(); + @Output() valueClear = new EventEmitter(); + + // endregion + + _handle() { + this.valueChanged.emit(this.value); + } + + _clear() { + this.value = ''; + this.valueClear.emit(); + } + +} diff --git a/src/components/transfer/nz-transfer.component.ts b/src/components/transfer/nz-transfer.component.ts new file mode 100644 index 00000000000..c5b0f0c5ba0 --- /dev/null +++ b/src/components/transfer/nz-transfer.component.ts @@ -0,0 +1,176 @@ +// tslint:disable:member-ordering +import { Component, ViewEncapsulation, Input, Output, ContentChild, TemplateRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { NzLocaleService } from '../locale/index'; +import { TransferItem } from './item'; + +@Component({ + selector: 'nz-transfer', + template: ` + +
+ + +
+ + `, + encapsulation: ViewEncapsulation.None, + styleUrls: [ + './style/index.less', + './style/patch.less' + ], + // tslint:disable-next-line:use-host-property-decorator + host: { + '[class.ant-transfer]': 'true' + } +}) +export class NzTransferComponent implements OnChanges { + + leftFilter = ''; + rightFilter = ''; + + // region: fields + + @Input() nzDataSource: TransferItem[] = []; + @Input() nzTitles: string[] = this._locale.translate('Transfer.titles').split(','); + @Input() nzOperations: string[] = []; + @Input() nzListStyle: Object; + @Input() nzItemUnit = this._locale.translate('Transfer.itemUnit'); + @Input() nzItemsUnit = this._locale.translate('Transfer.itemsUnit'); + @ContentChild('render') render: TemplateRef; + @ContentChild('footer') footer: TemplateRef; + + // search + @Input() nzShowSearch = false; + @Input() nzFilterOption: (inputValue: any, item: any) => boolean; + @Input() nzSearchPlaceholder = this._locale.translate('Transfer.searchPlaceholder'); + @Input() nzNotFoundContent = this._locale.translate('Transfer.notFoundContent'); + + // events + @Output() nzChange: EventEmitter = new EventEmitter(); + @Output() nzSearchChange: EventEmitter = new EventEmitter(); + @Output() nzSelectChange: EventEmitter = new EventEmitter(); + + // endregion + + // region: process data + + // left + leftDataSource: TransferItem[] = []; + + // right + rightDataSource: TransferItem[] = []; + + private splitDataSource() { + this.leftDataSource = []; + this.rightDataSource = []; + this.nzDataSource.forEach(record => { + if (record.direction === 'right') { + this.rightDataSource.push(record); + } else { + this.leftDataSource.push(record); + } + }); + } + + private getCheckedData(direction: string): TransferItem[] { + return this[direction === 'left' ? 'leftDataSource' : 'rightDataSource'].filter(w => w.checked); + } + + handleLeftSelectAll = (checked: boolean) => this.handleSelect('left', checked); + handleRightSelectAll = (checked: boolean) => this.handleSelect('right', checked); + + handleLeftSelect = (item: TransferItem) => this.handleSelect('left', item.checked, item); + handleRightSelect = (item: TransferItem) => this.handleSelect('right', item.checked, item); + + handleSelect(direction: 'left' | 'right', checked: boolean, item?: TransferItem) { + const list = this.getCheckedData(direction); + this.updateOperationStatus(direction, list.length); + this.nzSelectChange.emit({ direction, checked, list, item }); + } + + handleFilterChange(ret: any) { + this.nzSearchChange.emit(ret); + } + + // endregion + + // region: operation + + leftActive = false; + rightActive = false; + + private updateOperationStatus(direction: string, count: number) { + this[direction === 'right' ? 'leftActive' : 'rightActive'] = count > 0; + } + + moveToLeft = () => this.moveTo('left'); + moveToRight = () => this.moveTo('right'); + + moveTo(direction: string) { + const oppositeDirection = direction === 'left' ? 'right' : 'left'; + const datasource = direction === 'left' ? this.rightDataSource : this.leftDataSource; + const targetDatasource = direction === 'left' ? this.leftDataSource : this.rightDataSource; + const moveList: TransferItem[] = []; + for (let i = 0; i < datasource.length; i++) { + const item = datasource[i]; + if (item.checked === true && !item.disabled) { + item.checked = false; + moveList.push(item); + targetDatasource.push(item); + datasource.splice(i, 1); + --i; + } + } + this.updateOperationStatus(oppositeDirection, 0); + this.nzChange.emit({ + from: oppositeDirection, + to: direction, + list: moveList + }); + // this.nzSelectChange.emit({ direction: oppositeDirection, list: [] }); + } + + // endregion + + constructor(private _locale: NzLocaleService) {} + + ngOnChanges(changes: SimpleChanges): void { + if ('nzDataSource' in changes || 'nzTargetKeys' in changes) { + this.splitDataSource(); + this.updateOperationStatus('left', this.leftDataSource.filter(w => w.checked && !w.disabled).length) + this.updateOperationStatus('right', this.rightDataSource.filter(w => w.checked && !w.disabled).length) + } + } +} diff --git a/src/components/transfer/nz-transfer.module.ts b/src/components/transfer/nz-transfer.module.ts new file mode 100644 index 00000000000..5c860c8a2da --- /dev/null +++ b/src/components/transfer/nz-transfer.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NzLocaleModule } from '../locale/index'; +import { NzButtonModule } from '../button/nz-button.module'; +import { NzInputModule } from '../input/nz-input.module'; +import { NzCheckboxModule } from '../checkbox/nz-checkbox.module'; + +import { NzTransferComponent } from './nz-transfer.component'; +import { NzTransferListComponent } from './nz-transfer-list.component'; +import { NzTransferSearchComponent } from './nz-transfer-search.component'; + +@NgModule({ + imports: [CommonModule, FormsModule, NzCheckboxModule, NzButtonModule, NzInputModule, NzLocaleModule], + declarations: [NzTransferComponent, NzTransferListComponent, NzTransferSearchComponent], + exports: [NzTransferComponent] +}) +export class NzTransferModule { } diff --git a/src/components/transfer/nz-transfer.spec.ts b/src/components/transfer/nz-transfer.spec.ts new file mode 100644 index 00000000000..20e56f70b0e --- /dev/null +++ b/src/components/transfer/nz-transfer.spec.ts @@ -0,0 +1,195 @@ +// tslint:disable +import { Component, ViewChild, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed, ComponentFixtureAutoDetect, async, fakeAsync, tick } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; +import { NzTransferComponent } from './nz-transfer.component'; +import { NzTransferModule } from '../ng-zorro-antd.module'; +import { NzButtonModule } from '../button/nz-button.module'; +import { By } from '@angular/platform-browser'; + +const DEFAULT = ` + + +`; + +const RENDER = ` + + + [OK]{{item.title}}-{{item.description}} + + + + + +`; + +const DATA = []; +for (let i = 0; i < 20; i++) { + DATA.push({ + key: i.toString(), + title: `content${i + 1}`, + description: `description of content${i + 1}`, + direction: i % 2 === 0 ? 'right' : '' + }); +} + +function getListItemElemens(el: HTMLElement, count: number = 2, direction: 'left' | 'right' = 'left'): HTMLLIElement[] { + const find = el.querySelectorAll(`[data-direction="${direction}"] .ant-transfer-list-content-item`); + const ret: HTMLLIElement[] = []; + for (let i = 0, len = find.length; i < len; i++) { + if (count > i) + ret.push(find[i] as HTMLLIElement); + else + break; + } + return ret; +} + +describe('NzTransferModule', () => { + let fixture: ComponentFixture; + let context: TestTransferComponent; + let dl: DebugElement; + let el: HTMLElement; + + function createTestModule(html) { + TestBed.configureTestingModule({ + declarations: [TestTransferComponent], + imports: [NzTransferModule, FormsModule, NzButtonModule, NoopAnimationsModule], + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ] + }); + TestBed.overrideComponent(TestTransferComponent, { set: { template: html } }); + fixture = TestBed.createComponent(TestTransferComponent); + context = fixture.componentInstance; + spyOn(context, 'select'); + spyOn(context, 'change'); + spyOn(context, 'search'); + spyOn(context, 'reload'); + spyOn(context, 'nzFilterOption'); + dl = fixture.debugElement; + el = fixture.nativeElement; + fixture.detectChanges(); + } + + describe('[default]', () => { + beforeEach(() => { + createTestModule(DEFAULT); + }); + + it('should be inited', () => { + expect(context).not.toBeNull(); + // [nzTitles]="['source', 'target']" + const titleEl = dl.query(By.css('.ant-transfer-list-header-title')); + expect(titleEl.nativeElement.textContent).toContain(`source`, `the left title must be [source]`); + // [nzOperations]="['to right', 'to left']" + const operationEl = dl.query(By.css('.ant-transfer-operation .ant-btn')); + expect(operationEl.nativeElement.textContent).toContain(`to left`, `the from right to left button must be [to left]`); + // [nzListStyle]="{'width.px': 250, 'height.px': 300}" + const listEl = dl.query(By.css('.ant-transfer-list')); + expect(listEl.styles.width).toBe(`250px`, `the body width style must be 250px`); + expect(listEl.styles.height).toBe(`300px`, `the body height style must be 300px`); + // [nzItemUnit]="'项目单数'" + // [nzItemsUnit]="'项目复数'" + const unitEl = dl.query(By.css('.ant-transfer-list-header .ant-transfer-list-header-selected span:first-child')); + expect(unitEl.nativeElement.textContent).toContain(`项目复数`, `the item unit must be 项目复数`); + }); + + it('should be selected via click', () => { + (dl.query(By.css('.ant-transfer-list-content-item')).nativeElement as HTMLLIElement).click(); + fixture.detectChanges(); + expect(context.select).toHaveBeenCalled(); + }); + + it('should be changed from left to right', () => { + context.list = [ ...DATA ].map((item, idx) => { + if (idx <= 3) item.checked = true; + return item; + }); + fixture.detectChanges(); + (el.querySelectorAll(`.ant-transfer-operation .ant-btn`)[1] as HTMLButtonElement).click(); + fixture.detectChanges(); + expect(context.change).toHaveBeenCalled(); + }); + + it('should be changed from right to left', () => { + context.list = [ ...DATA ].map((item, idx) => { + if (idx <= 3) item.checked = true; + return item; + }); + fixture.detectChanges(); + (el.querySelectorAll(`.ant-transfer-operation .ant-btn`)[0] as HTMLButtonElement).click(); + fixture.detectChanges(); + expect(context.change).toHaveBeenCalled(); + }); + + it('should be inited search', () => { + // [nzShowSearch]="nzShowSearch" + context.nzShowSearch = true; + fixture.detectChanges(); + expect(dl.query(By.css('.ant-transfer-list-search'))).not.toBeNull(`the search element initialization failed`); + + // [nzSearchPlaceholder]="nzSearchPlaceholder" + const iptEl = dl.query(By.css('.ant-transfer-list-search input')); + expect(iptEl.nativeElement.getAttribute('placeholder')).toBe(`nzSearchPlaceholder`, `the input placeholder text must be 'nzSearchPlaceholder'`); + // [nzNotFoundContent]="nzNotFoundContent" + const notFoundEl = dl.query(By.css('.ant-transfer-list-body-not-found')); + expect(notFoundEl.nativeElement.textContent).toContain(`nzNotFoundContent`, `the not found text must be nzNotFoundContent`); + }); + + }); + + describe('#template', () => { + beforeEach(() => { + createTestModule(RENDER); + }); + + it('should be custom render', () => { + const els = getListItemElemens(el, 1); + expect(els.length).toBe(1); + expect(els[0].textContent).toContain(`[OK]`); + }); + + it('should be custom footer', () => { + const footerEl = dl.query(By.css('.ant-transfer-list-footer button')); + expect(footerEl).toBeDefined(); + expect(footerEl.nativeElement.textContent).toContain(`reload`); + footerEl.nativeElement.click(); + fixture.detectChanges(); + expect(context.reload).toHaveBeenCalled(); + }); + }); + +}); + +@Component({ template: '' }) +class TestTransferComponent { + @ViewChild(NzTransferComponent) comp: NzTransferComponent; + list = [ ...DATA ]; + nzShowSearch = false; + nzSearchPlaceholder = 'nzSearchPlaceholder'; + nzNotFoundContent = 'nzNotFoundContent'; + nzFilterOption(inputValue, option) { + console.log(inputValue, option); + return option.description.indexOf(inputValue) > -1; + } + + select() {} + change() {} + search() {} + reload() {} +} diff --git a/src/components/transfer/style/index.less b/src/components/transfer/style/index.less new file mode 100644 index 00000000000..ceb5ddfef27 --- /dev/null +++ b/src/components/transfer/style/index.less @@ -0,0 +1,166 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../checkbox/style/mixin"; + +@transfer-prefix-cls: ~"@{ant-prefix}-transfer"; + +.@{transfer-prefix-cls} { + position: relative; + line-height: @line-height-base; + + &-list { + font-size: @font-size-base; + border: @border-width-base @border-style-base @border-color-base; + display: inline-block; + border-radius: @border-radius-base; + vertical-align: middle; + position: relative; + width: 180px; + height: 200px; + padding-top: 33px; + + &-with-footer { + padding-bottom: 33px; + } + + &-search { + &-action { + color: @disabled-color; + position: absolute; + top: 4px; + right: 4px; + bottom: 4px; + width: 28px; + line-height: 26px; + text-align: center; + font-size: @font-size-lg; + .@{iconfont-css-prefix} { + transition: all .3s; + font-size: @font-size-base; + color: @disabled-color; + &:hover { + color: @text-color-secondary; + } + } + span& { + pointer-events: none; + } + } + } + + &-header { + padding: 7px 15px; + border-radius: @border-radius-base @border-radius-base 0 0; + background: @component-background; + color: @text-color; + border-bottom: @border-width-base @border-style-base @border-color-split; + overflow: hidden; + position: absolute; + top: 0; + left: 0; + width: 100%; + + &-title { + position: absolute; + right: 15px; + } + } + + &-body { + font-size: @font-size-base; + position: relative; + height: 100%; + + &-search-wrapper { + position: absolute; + top: 0; + left: 0; + padding: 4px; + width: 100%; + } + } + + &-body-with-search { + padding-top: 34px; + } + + &-content { + height: 100%; + overflow: auto; + > .LazyLoad { + animation: transferHighlightIn 1s; + } + + &-item { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 7px 15px; + min-height: 32px; + transition: all .3s; + } + + &-item:not(&-item-disabled):hover { + cursor: pointer; + background-color: @item-hover-bg; + } + + &-item-disabled { + cursor: not-allowed; + color: @btn-disable-color; + } + } + + &-body-not-found { + padding-top: 0; + color: @disabled-color; + text-align: center; + display: none; + position: absolute; + top: 50%; + width: 100%; + margin-top: -10px; + } + + &-content:empty + &-body-not-found { + display: block; + } + + &-footer { + border-top: @border-width-base @border-style-base @border-color-split; + border-radius: 0 0 @border-radius-base @border-radius-base; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + } + } + + &-operation { + display: inline-block; + overflow: hidden; + margin: 0 8px; + vertical-align: middle; + + .@{ant-prefix}-btn { + display: block; + + &:first-child { + margin-bottom: 4px; + } + + .@{iconfont-css-prefix} { + .iconfont-size-under-12px(10px); + } + } + } +} + +@keyframes transferHighlightIn { + 0% { + background: @primary-2; + } + 100% { + background: transparent; + } +} diff --git a/src/components/transfer/style/patch.less b/src/components/transfer/style/patch.less new file mode 100644 index 00000000000..63eb94e008e --- /dev/null +++ b/src/components/transfer/style/patch.less @@ -0,0 +1,18 @@ +nz-transfer { + display: block; +} + +nz-transfer-list { + .ant-checkbox-wrapper { + margin-right: 0 !important; + } +} + +.ant-transfer__nodata { + .ant-transfer-list-content { + display: none; + } + .ant-transfer-list-body-not-found { + display: block; + } +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer-advanced.component.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer-advanced.component.ts new file mode 100644 index 00000000000..dceb35eb447 --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer-advanced.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { NzMessageService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-transfer-advanced', + template: ` + + + {{item.title}}-{{item.description}} + + + + + + ` +}) +export class NzDemoTransferAdvancedComponent implements OnInit { + list: any[] = []; + ngOnInit() { + this.getData(); + } + + getData() { + const ret = []; + for (let i = 0; i < 20; i++) { + ret.push({ + key: i.toString(), + title: `content${i + 1}`, + description: `description of content${i + 1}`, + direction: Math.random() * 2 > 1 ? 'right' : '' + }); + } + this.list = ret; + } + + reload(direction: string) { + this.getData(); + this.msg.success(`your clicked ${direction}!`); + } + + select(ret: any) { + console.log('nzSelectChange', ret); + } + + change(ret: any) { + console.log('nzChange', ret); + } + + constructor(public msg: NzMessageService) {} +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer-basic.component.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer-basic.component.ts new file mode 100644 index 00000000000..5c535a4c920 --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer-basic.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { NzMessageService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-transfer-basic', + template: ` + + + ` +}) +export class NzDemoTransferBasicComponent implements OnInit { + list: any[] = []; + ngOnInit() { + for (let i = 0; i < 20; i++) { + this.list.push({ + key: i.toString(), + title: `content${i + 1}`, + disabled: i % 3 < 1, + }); + } + + [ 2, 3 ].forEach(idx => this.list[idx].direction = 'right'); + } + + select(ret: any) { + console.log('nzSelectChange', ret); + } + + change(ret: any) { + console.log('nzChange', ret); + } +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer-custom-item.component.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer-custom-item.component.ts new file mode 100644 index 00000000000..52e2d37dcdc --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer-custom-item.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import { NzMessageService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-transfer-custom-item', + template: ` + + + {{item.title}} + + + ` +}) +export class NzDemoTransferCustomItemComponent implements OnInit { + list: any[] = []; + ngOnInit() { + this.getData(); + } + + getData() { + const ret = []; + for (let i = 0; i < 20; i++) { + ret.push({ + key: i.toString(), + title: `content${i + 1}`, + description: `description of content${i + 1}`, + direction: Math.random() * 2 > 1 ? 'right' : '', + icon: `frown-o` + }); + } + this.list = ret; + } + + select(ret: any) { + console.log('nzSelectChange', ret); + } + + change(ret: any) { + console.log('nzChange', ret); + } + + constructor(public msg: NzMessageService) {} +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer-search.component.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer-search.component.ts new file mode 100644 index 00000000000..7ba8ab7057e --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer-search.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { NzMessageService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-transfer-search', + template: ` + + + ` +}) +export class NzDemoTransferSearchComponent implements OnInit { + list: any[] = []; + ngOnInit() { + for (let i = 0; i < 20; i++) { + this.list.push({ + key: i.toString(), + title: `content${i + 1}`, + description: `description of content${i + 1}`, + direction: Math.random() * 2 > 1 ? 'right' : '' + }); + } + } + + filterOption(inputValue, option) { + return option.description.indexOf(inputValue) > -1; + } + + search(ret: any) { + console.log('nzSearchChange', ret); + } + + select(ret: any) { + console.log('nzSelectChange', ret); + } + + change(ret: any) { + console.log('nzChange', ret); + } +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer.component.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer.component.ts new file mode 100644 index 00000000000..101e5bfeb26 --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector : 'nz-demo-transfer', + encapsulation: ViewEncapsulation.None, + templateUrl : './nz-demo-transfer.html' +}) +export class NzDemoTransferComponent implements OnInit { + NzDemoTransferBasicCode = require('!!raw-loader!./nz-demo-transfer-basic.component'); + NzDemoTransferSearchCode = require('!!raw-loader!./nz-demo-transfer-search.component'); + NzDemoTransferAdvancedCode = require('!!raw-loader!./nz-demo-transfer-advanced.component'); + NzDemoTransferCustomItemCode = require('!!raw-loader!./nz-demo-transfer-custom-item.component'); + + constructor() { + } + + ngOnInit() { + } +} + diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer.html b/src/showcase/nz-demo-transfer/nz-demo-transfer.html new file mode 100644 index 00000000000..d152e8084d9 --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer.html @@ -0,0 +1,192 @@ +
+

Transfer 穿梭框

+

双栏穿梭选择框。

+

何时使用 + # +

+

用直观的方式在两栏中移动元素,完成选择行为。

+

选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。 其中,左边一栏为source,右边一栏为target,API 的设计也反映了这两个概念。

+
+

代码演示

+
+
+
+ + +
+

最基本的用法,展示了nzDataSource、每行的渲染函数 render 以及回调函数nzSelectChangenzChange的用法。

+
+
+
+
+ + +
+

带搜索框的穿梭框,可以自定义搜索函数。

+
+
+
+
+ + +
+

穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。

+
+
+
+
+ + +
+

自定义渲染每一个 Transfer Item,可用于渲染复杂数据。

+
+
+
+
+
+

API + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数说明类型默认值
nzDataSource数据源,其中若数据属性direction: 'right'将会被渲染到右边一栏中TransferItem[][]
nzTitles标题集合,顺序从左至右string[]['', '']
nzOperations操作文案集合,顺序从下至上string[]'>', '<'
nzListStyle两个穿梭框的自定义样式,以ngStyle写法标题object
nzItemUnit单数单位string项目
nzItemsUnit复数单位string项目
#render每行数据渲染模板,见示例TemplateRef
#footer底部渲染模板,见示例TemplateRef
nzShowSearch是否显示搜索框booleanfalse
nzFilterOption接收inputValueoption 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false
nzSearchPlaceholder搜索框的默认值请输入搜索内容
nzNotFoundContent当列表为空时显示的内容列表为空
(nzChange)选项在两栏之间转移时的回调函数
(nzSearchChange)搜索框内容时改变时的回调函数
(nzSelectChange)选中项发生改变时的回调函数
+

TransferItem + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数说明类型默认值
title标题,用于显示及搜索关键字判断string-
direction指定数据方向,若指定right为右栏,其他情况为左栏leftright-
disabled指定checkbox为不可用状态booleanfalse
checked指定checkbox为选中状态booleanfalse
+
+
diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer.module.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer.module.ts new file mode 100644 index 00000000000..d27669a76cd --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgZorroAntdModule } from '../../../index.showcase'; +import { NzCodeBoxModule } from '../share/nz-codebox/nz-codebox.module'; + +import { NzDemoTransferComponent } from './nz-demo-transfer.component'; +import { NzDemoTransferRoutingModule } from './nz-demo-transfer.routing.module'; +import { NzDemoTransferBasicComponent } from './nz-demo-transfer-basic.component'; +import { NzDemoTransferSearchComponent } from './nz-demo-transfer-search.component'; +import { NzDemoTransferAdvancedComponent } from './nz-demo-transfer-advanced.component'; +import { NzDemoTransferCustomItemComponent } from './nz-demo-transfer-custom-item.component'; + +@NgModule({ + imports : [ NzDemoTransferRoutingModule, CommonModule, NzCodeBoxModule, NgZorroAntdModule, FormsModule ], + declarations: [ NzDemoTransferComponent, NzDemoTransferBasicComponent, NzDemoTransferSearchComponent, NzDemoTransferAdvancedComponent, NzDemoTransferCustomItemComponent ] +}) + +export class NzDemoTransferModule { + +} diff --git a/src/showcase/nz-demo-transfer/nz-demo-transfer.routing.module.ts b/src/showcase/nz-demo-transfer/nz-demo-transfer.routing.module.ts new file mode 100644 index 00000000000..c376ed60c70 --- /dev/null +++ b/src/showcase/nz-demo-transfer/nz-demo-transfer.routing.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { NzDemoTransferComponent } from './nz-demo-transfer.component'; + +@NgModule({ + imports: [ RouterModule.forChild([ + { path: '', component: NzDemoTransferComponent } + ]) ], + exports: [ RouterModule ] +}) +export class NzDemoTransferRoutingModule { +} diff --git a/src/showcase/router.ts b/src/showcase/router.ts index 44ff72bc393..d54f00f2edd 100644 --- a/src/showcase/router.ts +++ b/src/showcase/router.ts @@ -171,6 +171,12 @@ export const ROUTER_LIST = { 'path' : 'components/time-picker', // 'loadChildren': './nz-demo-timepicker/nz-demo-timepicker.module#NzDemoTimePickerModule', 'zh' : '时间选择框' + }, + { + 'label' : 'Transfer', + 'path' : 'components/transfer', + // 'loadChildren': './nz-demo-timepicker/nz-demo-timepicker.module#NzDemoTimePickerModule', + 'zh' : '穿梭框' } ] }, @@ -427,6 +433,10 @@ export const DEMO_ROUTES = [ 'path' : 'components/time-picker', 'loadChildren': './nz-demo-timepicker/nz-demo-timepicker.module#NzDemoTimePickerModule' }, + { + 'path' : 'components/transfer', + 'loadChildren': './nz-demo-transfer/nz-demo-transfer.module#NzDemoTransferModule' + }, { 'path' : 'components/badge', 'loadChildren': './nz-demo-badge/nz-demo-badge.module#NzDemoBadgeModule'