diff --git a/src/app/app.interceptor.ts b/src/app/app.interceptor.ts index 4970ea51d3..db1ae7a949 100644 --- a/src/app/app.interceptor.ts +++ b/src/app/app.interceptor.ts @@ -8,7 +8,7 @@ import {PagingInfo} from "../jigsaw/core/data/component-data"; @Injectable() export class AjaxInterceptor implements HttpInterceptor { - private static _usingRealRDK: boolean = false; + private static _usingRealRDK: boolean = true; private static _processors: any[] = []; public static registerProcessor(urlPattern: RegExp | string, processor: (req: HttpRequest) => any, context?: any) { @@ -49,9 +49,12 @@ export class AjaxInterceptor implements HttpInterceptor { dealServerSidePagingRequest(req: HttpRequest): Observable> { const params = req.method.toLowerCase() == 'post' ? 'body' : 'params'; const service = this.getParamValue(req, params, 'service'); - const paging = this.getParamValue(req, params, 'paging') ? JSON.parse(this.getParamValue(req, params, 'paging')) : null; - const filter = this.getParamValue(req, params, 'filter') ? JSON.parse(this.getParamValue(req, params, 'filter')) : null; - const sort = this.getParamValue(req, params, 'sort') ? JSON.parse(this.getParamValue(req, params, 'sort')) : null; + let paging = this.getParamValue(req, params, 'paging') ? this.getParamValue(req, params, 'paging') : null; + paging = typeof paging === 'string' ? JSON.stringify(paging) : paging; + let filter = this.getParamValue(req, params, 'filter') ? this.getParamValue(req, params, 'filter') : null; + filter = typeof filter === 'string' ? JSON.stringify(filter) : filter; + let sort = this.getParamValue(req, params, 'sort') ? this.getParamValue(req, params, 'sort') : null; + sort = typeof sort === 'string' ? JSON.stringify(sort) : sort; return PageableData.get({service, paging, filter, sort}); } @@ -176,7 +179,7 @@ class PageableData { } private static _filter(dataTable, filter) { - return filter.hasOwnProperty('rawFunction') ? + return filter.hasOwnProperty('rawFunction') && !!filter.rawFunction ? this._filterWithFunction(dataTable.data, filter.rawFunction, filter.context) : this._filterWithKeyword(dataTable.data, filter.key, filter.field, dataTable.field); } @@ -222,7 +225,7 @@ class PageableData { private static _filterWithFunction(data, rawFunction, context) { let func; try { - func = eval('(' + rawFunction.replace(/\b_this\b/g, 'this') + ')'); + func = eval('(' + rawFunction + ')'); } catch (e) { console.error('eval raw filter function error, detail: ' + e.message); return data; diff --git a/src/app/demo-list.component.ts b/src/app/demo-list.component.ts index b56e10d2ef..8f3df66809 100644 --- a/src/app/demo-list.component.ts +++ b/src/app/demo-list.component.ts @@ -47,6 +47,7 @@ import {routerConfig as treeConfig} from "./demo/tree/demo-set.module"; import {routerConfig as trustedHtmlConfig} from "./demo/trusted-html/demo-set.module"; import {routerConfig as uploadConfig} from "./demo/upload/demo-set.module"; import {routerConfig as iconConfig} from "./demo/icon/demo-set.module"; +import {routerConfig as transferConfig} from "./demo/transfer/demo-set.module"; import {routerConfig} from "./router-config"; @Component({ @@ -140,6 +141,7 @@ export class DemoListManager { this._addRouterConfig(routerConfig, 'trusted-html', trustedHtmlConfig); this._addRouterConfig(routerConfig, 'upload', uploadConfig); this._addRouterConfig(routerConfig, 'icon', iconConfig); + this._addRouterConfig(routerConfig, 'transfer', transferConfig); } private static _addRouterConfig(routerConfig: any[], path: string, childConfig: any[]) { diff --git a/src/app/demo/transfer/basic/demo.component.html b/src/app/demo/transfer/basic/demo.component.html new file mode 100644 index 0000000000..8b4576f8ec --- /dev/null +++ b/src/app/demo/transfer/basic/demo.component.html @@ -0,0 +1,20 @@ + + + + + + + +
+

Transfer

+
+

普通数据

+ +

选择的地区:{{selectedCountriesStr}}

+
+
+ + + diff --git a/src/app/demo/transfer/basic/demo.component.ts b/src/app/demo/transfer/basic/demo.component.ts new file mode 100644 index 0000000000..d25c4544e5 --- /dev/null +++ b/src/app/demo/transfer/basic/demo.component.ts @@ -0,0 +1,37 @@ +import {Component} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {ArrayCollection, LocalPageableArray} from "jigsaw/core/data/array-collection"; +import {TableData} from "jigsaw/core/data/table-data"; + +@Component({ + templateUrl: './demo.component.html' +}) +export class TransferArrayDemoComponent { + constructor(private _http: HttpClient) { + this.data = new ArrayCollection(); + this.data.http = _http; + this.data.fromAjax('mock-data/countries'); + this.data.dataReviser = (td: TableData) => TableData.toArray(td); + + this.selectedCountries = new ArrayCollection(); + this.selectedCountries.http = _http; + this.selectedCountries.fromAjax('mock-data/countries'); + this.selectedCountries.dataReviser = (td: TableData) => TableData.toArray(td).slice(0,5); + } + + data: ArrayCollection; + selectedCountries: ArrayCollection; + selectedCountriesStr: string; + + handleSelectChange($event) { + this.selectedCountriesStr = $event.map(item => item.zhName).join(','); + } + + // ==================================================================== + // ignore the following lines, they are not important to this demo + // ==================================================================== + summary: string = ''; + description: string = ''; + +} + diff --git a/src/app/demo/transfer/basic/demo.module.ts b/src/app/demo/transfer/basic/demo.module.ts new file mode 100644 index 0000000000..5cc8490a61 --- /dev/null +++ b/src/app/demo/transfer/basic/demo.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from "@angular/core"; +import {JigsawDemoDescriptionModule} from "app/demo-description/demo-description"; +import {TransferArrayDemoComponent} from "./demo.component"; +import {JigsawTransferModule} from "jigsaw/component/transfer/transfer"; + +@NgModule({ + declarations: [TransferArrayDemoComponent], + exports: [ TransferArrayDemoComponent ], + imports: [JigsawDemoDescriptionModule, JigsawTransferModule] +}) +export class TransferArrayDemoModule{ + +} diff --git a/src/app/demo/transfer/demo-set.module.ts b/src/app/demo/transfer/demo-set.module.ts new file mode 100644 index 0000000000..9c021dd77a --- /dev/null +++ b/src/app/demo/transfer/demo-set.module.ts @@ -0,0 +1,37 @@ +import {NgModule} from "@angular/core"; +import {RouterModule} from "@angular/router"; +import {TransferArrayDemoComponent} from "./basic/demo.component"; +import {TransferArrayDemoModule} from "./basic/demo.module"; +import {TransferLocalPageableArrayComponent} from "./local-pageable-array/demo.component"; +import {TransferLocalPageableArrayDemoModule} from "./local-pageable-array/demo.module"; +import {TransferPageableArrayComponent} from "./pageable-array/demo.component"; +import {TransferPageableArrayDemoModule} from "./pageable-array/demo.module"; +import {TransferItemDisabledDemoComponent} from "./item-disabled/demo.component"; +import {TransferItemDisabledDemoModule} from "./item-disabled/demo.module"; + +export const routerConfig = [ + { + path: 'basic', component: TransferArrayDemoComponent + }, + { + path: 'local-pageable-array', component: TransferLocalPageableArrayComponent + }, + { + path: 'pageable-array', component: TransferPageableArrayComponent + }, + { + path: 'item-disabled', component: TransferItemDisabledDemoComponent + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routerConfig), + TransferArrayDemoModule, + TransferLocalPageableArrayDemoModule, + TransferPageableArrayDemoModule, + TransferItemDisabledDemoModule + ] +}) +export class TransferDemoModule { +} diff --git a/src/app/demo/transfer/item-disabled/demo.component.html b/src/app/demo/transfer/item-disabled/demo.component.html new file mode 100644 index 0000000000..05fa7d1278 --- /dev/null +++ b/src/app/demo/transfer/item-disabled/demo.component.html @@ -0,0 +1,17 @@ + + + + + + + +
+

Transfer

+
+

随机设置不可操作条目

+ +
+
+ + + diff --git a/src/app/demo/transfer/item-disabled/demo.component.ts b/src/app/demo/transfer/item-disabled/demo.component.ts new file mode 100644 index 0000000000..db148b2490 --- /dev/null +++ b/src/app/demo/transfer/item-disabled/demo.component.ts @@ -0,0 +1,31 @@ +import {Component} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {ArrayCollection} from "jigsaw/core/data/array-collection"; +import {TableData} from "jigsaw/core/data/table-data"; + +@Component({ + templateUrl: './demo.component.html' +}) +export class TransferItemDisabledDemoComponent { + constructor(private _http: HttpClient) { + this.data = new ArrayCollection(); + this.data.http = _http; + this.data.fromAjax('mock-data/countries'); + this.data.dataReviser = (td: TableData) => { + return TableData.toArray(td).map(item => { + item.disabled = Math.random() > 0.8; + return item; + }) + } + } + + data: ArrayCollection; + + // ==================================================================== + // ignore the following lines, they are not important to this demo + // ==================================================================== + summary: string = ''; + description: string = ''; + +} + diff --git a/src/app/demo/transfer/item-disabled/demo.module.ts b/src/app/demo/transfer/item-disabled/demo.module.ts new file mode 100644 index 0000000000..ac0a5126a1 --- /dev/null +++ b/src/app/demo/transfer/item-disabled/demo.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from "@angular/core"; +import {JigsawTransferModule} from "jigsaw/component/transfer/transfer"; +import {JigsawDemoDescriptionModule} from "app/demo-description/demo-description"; +import {TransferItemDisabledDemoComponent} from "./demo.component"; + +@NgModule({ + declarations: [TransferItemDisabledDemoComponent], + exports: [ TransferItemDisabledDemoComponent ], + imports: [JigsawDemoDescriptionModule, JigsawTransferModule] +}) +export class TransferItemDisabledDemoModule{ + +} diff --git a/src/app/demo/transfer/local-pageable-array/demo.component.html b/src/app/demo/transfer/local-pageable-array/demo.component.html new file mode 100644 index 0000000000..6fd343b862 --- /dev/null +++ b/src/app/demo/transfer/local-pageable-array/demo.component.html @@ -0,0 +1,20 @@ + + + + + + + +
+

Transfer

+
+

本地分页数据

+ +

选择的地区:{{selectedCountriesStr}}

+
+
+ + + diff --git a/src/app/demo/transfer/local-pageable-array/demo.component.ts b/src/app/demo/transfer/local-pageable-array/demo.component.ts new file mode 100644 index 0000000000..32b7e9951d --- /dev/null +++ b/src/app/demo/transfer/local-pageable-array/demo.component.ts @@ -0,0 +1,38 @@ +import {Component} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {ArrayCollection, LocalPageableArray} from "jigsaw/core/data/array-collection"; +import {TableData} from "jigsaw/core/data/table-data"; + +@Component({ + templateUrl: './demo.component.html' +}) +export class TransferLocalPageableArrayComponent { + constructor(private _http: HttpClient) { + this.data = new LocalPageableArray(); + this.data.http = _http; + this.data.pagingInfo.pageSize = 15; + this.data.fromAjax('mock-data/countries'); + this.data.dataReviser = (td: TableData) => TableData.toArray(td); + + this.selectedCountries = new ArrayCollection(); + this.selectedCountries.http = _http; + this.selectedCountries.fromAjax('mock-data/countries'); + this.selectedCountries.dataReviser = (td: TableData) => TableData.toArray(td).slice(0,5); + } + + data: LocalPageableArray; + selectedCountries: ArrayCollection; + selectedCountriesStr: string; + + handleSelectChange($event) { + this.selectedCountriesStr = $event.map(item => item.zhName).join(','); + } + + // ==================================================================== + // ignore the following lines, they are not important to this demo + // ==================================================================== + summary: string = ''; + description: string = ''; + +} + diff --git a/src/app/demo/transfer/local-pageable-array/demo.module.ts b/src/app/demo/transfer/local-pageable-array/demo.module.ts new file mode 100644 index 0000000000..021a69e5c6 --- /dev/null +++ b/src/app/demo/transfer/local-pageable-array/demo.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from "@angular/core"; +import {JigsawDemoDescriptionModule} from "app/demo-description/demo-description"; +import {TransferLocalPageableArrayComponent} from "./demo.component"; +import {JigsawTransferModule} from "jigsaw/component/transfer/transfer"; + +@NgModule({ + declarations: [TransferLocalPageableArrayComponent], + exports: [ TransferLocalPageableArrayComponent ], + imports: [JigsawDemoDescriptionModule, JigsawTransferModule] +}) +export class TransferLocalPageableArrayDemoModule{ + +} diff --git a/src/app/demo/transfer/pageable-array/demo.component.html b/src/app/demo/transfer/pageable-array/demo.component.html new file mode 100644 index 0000000000..c433798715 --- /dev/null +++ b/src/app/demo/transfer/pageable-array/demo.component.html @@ -0,0 +1,20 @@ + + + + + + + +
+

Transfer

+
+

服务端分页数据

+ +

选择的地区:{{selectedCountriesStr}}

+
+
+ + + diff --git a/src/app/demo/transfer/pageable-array/demo.component.ts b/src/app/demo/transfer/pageable-array/demo.component.ts new file mode 100644 index 0000000000..79cca204b0 --- /dev/null +++ b/src/app/demo/transfer/pageable-array/demo.component.ts @@ -0,0 +1,38 @@ +import {Component} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {ArrayCollection, PageableArray} from "jigsaw/core/data/array-collection"; +import {TableData} from "jigsaw/core/data/table-data"; + +@Component({ + templateUrl: './demo.component.html' +}) +export class TransferPageableArrayComponent { + constructor(private _http: HttpClient) { + this.data = new PageableArray(_http, { + url: '/rdk/service/app/example/server/my_service', + method: 'post' + }); + this.data.fromAjax(); + + this.selectedCountries = new ArrayCollection(); + this.selectedCountries.http = _http; + this.selectedCountries.fromAjax('/rdk/service/app/example/server/my_service'); + this.selectedCountries.dataReviser = (td: TableData) => TableData.toArray(td).slice(10,15); + } + + data: PageableArray; + selectedCountries: ArrayCollection; + selectedCountriesStr: string; + + handleSelectChange($event) { + this.selectedCountriesStr = $event.map(item => item.zhName).join(','); + } + + // ==================================================================== + // ignore the following lines, they are not important to this demo + // ==================================================================== + summary: string = ''; + description: string = ''; + +} + diff --git a/src/app/demo/transfer/pageable-array/demo.module.ts b/src/app/demo/transfer/pageable-array/demo.module.ts new file mode 100644 index 0000000000..754cc4fdff --- /dev/null +++ b/src/app/demo/transfer/pageable-array/demo.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from "@angular/core"; +import {JigsawDemoDescriptionModule} from "app/demo-description/demo-description"; +import {TransferPageableArrayComponent} from "./demo.component"; +import {JigsawTransferModule} from "jigsaw/component/transfer/transfer"; + +@NgModule({ + declarations: [TransferPageableArrayComponent], + exports: [ TransferPageableArrayComponent ], + imports: [JigsawDemoDescriptionModule, JigsawTransferModule] +}) +export class TransferPageableArrayDemoModule{ + +} diff --git a/src/app/router-config.ts b/src/app/router-config.ts index d3a6a2480c..065f6bbd78 100644 --- a/src/app/router-config.ts +++ b/src/app/router-config.ts @@ -192,5 +192,9 @@ export const routerConfig = [ { path: "icon", loadChildren: "./demo/icon/demo-set.module#IconDemoModule", + }, + { + path: "transfer", + loadChildren: "./demo/transfer/demo-set.module#TransferDemoModule", } ]; diff --git a/src/jigsaw/component/list-and-tile/group-lite-common.ts b/src/jigsaw/component/list-and-tile/group-lite-common.ts index 3e49e3b894..f2821d0345 100644 --- a/src/jigsaw/component/list-and-tile/group-lite-common.ts +++ b/src/jigsaw/component/list-and-tile/group-lite-common.ts @@ -22,7 +22,7 @@ export class AbstractJigsawGroupLiteComponent extends AbstractJigsawComponent im if (this.data && (typeof this.data[0] == 'string' || typeof this.data[0] == 'number')) { this._trackItemBy = null; } else if (CommonUtils.isUndefined(this._trackItemBy) && this.data && typeof this.data[0] !== 'string') { - this._trackItemBy = this.labelField; + this._trackItemBy = [this.labelField]; } return this._trackItemBy; } diff --git a/src/jigsaw/component/transfer/transfer-list.html b/src/jigsaw/component/transfer/transfer-list.html new file mode 100644 index 0000000000..abbbace9ad --- /dev/null +++ b/src/jigsaw/component/transfer/transfer-list.html @@ -0,0 +1,36 @@ +
+ + + {{_$currentPageSelectedItems ? _$currentPageSelectedItems.length : 0}} / {{data ? data.length : 0}} 项 + + 共 {{selectedItems ? selectedItems.length : 0}} / {{data && data.pagingInfo ? data.pagingInfo.totalRecord : 0}} 项 + +
+
+
+ + + +
+
+ + +
+ + {{item && item[labelField] ? item[labelField] : item}} +
+ {{item && item[subLabelField] ? item[subLabelField] : ''}} +
+
+
+
+
+ +
diff --git a/src/jigsaw/component/transfer/transfer.html b/src/jigsaw/component/transfer/transfer.html new file mode 100644 index 0000000000..4d9a76f090 --- /dev/null +++ b/src/jigsaw/component/transfer/transfer.html @@ -0,0 +1,25 @@ +
+
+ +
+ +
+ +
+
+ diff --git a/src/jigsaw/component/transfer/transfer.scss b/src/jigsaw/component/transfer/transfer.scss new file mode 100644 index 0000000000..72b47f0fc8 --- /dev/null +++ b/src/jigsaw/component/transfer/transfer.scss @@ -0,0 +1,98 @@ +@import "../../assets/scss/core/base"; + +$transfer-prefix-cls: #{$jigsaw-prefix}-transfer; +$list-prefix-cls: #{$jigsaw-prefix}-list; + +.#{$transfer-prefix-cls} { + display: inline-block; + width: 400px; + height: 200px; + &-wrapper { + display: flex; + flex: 1 1 0; + width: 100%; + height: 100%; + } + &-box { + border: 1px solid $border-color-base; + border-radius: $border-radius-base; + flex: 1 1 0; + } + &-option-box { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + &-option-bar { + border: 1px solid $primary-color; + background: $primary-color; + color: $text-color-of-bg; + border-radius: $border-radius-base; + margin: 0 8px 4px; + padding: 2px 12px; + font-size: 12px; + &:hover { + background: $primary-7; + color: $text-color-of-bg; + } + &:active { + background: $primary-8; + color: $text-color-of-bg; + } + } + &-option-disabled { + cursor: not-allowed; + @include compatible(color, $disabled-color, $disabled-color-ie11); + border: 1px solid $border-color-base; + background: $disabled-bg; + color: $text-color-secondary; + &:hover, &:active { + background: $disabled-bg; + color: $text-color-secondary; + } + } + &-head { + position: relative; + height: 32px; + padding: 0 4px; + border-bottom: 1px solid $border-color-base; + &:before { + content: ''; + display: inline-block; + vertical-align: middle; + height: 100%; + } + } + &-list-wrapper { + .#{$list-prefix-cls} { + border: none; + } + } + &-search-wrapper { + padding: 4px; + } + &-middle-title { + vertical-align: middle; + margin-left: 4px; + } + &-right-title { + position: absolute; + top: 8px; + right: 8px; + } + &-list-frame { + display: block; + height: 100%; + } + &-foot { + height: 28px; + text-align: center; + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + } + } +} diff --git a/src/jigsaw/component/transfer/transfer.ts b/src/jigsaw/component/transfer/transfer.ts new file mode 100644 index 0000000000..82ba51f31e --- /dev/null +++ b/src/jigsaw/component/transfer/transfer.ts @@ -0,0 +1,471 @@ +import {Component, Input, NgModule, OnDestroy, Optional} from "@angular/core"; +import {JigsawListModule} from "../list-and-tile/list"; +import {JigsawCheckBoxModule} from "../checkbox/index"; +import {ArrayCollection, LocalPageableArray, PageableArray} from "../../core/data/array-collection"; +import {PerfectScrollbarModule} from "ngx-perfect-scrollbar"; +import {JigsawInputModule} from "../input/input"; +import {GroupOptionValue} from "../list-and-tile/group-common"; +import {AbstractJigsawGroupLiteComponent} from "../list-and-tile/group-lite-common"; +import {CallbackRemoval, CommonUtils} from "../../core/utils/common-utils"; +import {JigsawPaginationModule} from "../pagination/pagination"; +import {InternalUtils} from "../../core/utils/internal-utils"; +import {Subscriber} from "rxjs/Subscriber"; +import {CommonModule} from "@angular/common"; + +// 此处不能使用箭头函数 +const transferFilterFunction = function (item) { + let listResult = true; + let keyResult = true; + if (this.selectedItems) { + if (this.selectedItems.some(si => CommonUtils.compareWithKeyProperty(item, si, this.trackItemBy))) { + listResult = false; + } + } + if (this.keyword !== null && this.keyword !== undefined) { + keyResult = LocalPageableArray.filterItemByKeyword(item, this.keyword, this.fields); + } + return listResult && keyResult; +}; + +const transferServerFilterFunction = function (item) { + function compareWithKeyProperty(item1: any, item2: any, trackItemBy: string[]): boolean { + if (trackItemBy && trackItemBy.length > 0) { + for (let i = 0; i < trackItemBy.length; i++) { + if (!item1 || !item2) { + // 过滤掉 typeof null == 'object' + return false; + } else if (typeof item1 === 'object' && typeof item2 === 'object') { + if (item1[trackItemBy[i]] != item2[trackItemBy[i]]) { + return false; + } + } else if (typeof item1 !== 'object' && typeof item2 === 'object') { + if (item1 != item2[trackItemBy[i]]) { + return false; + } + } else if (typeof item1 === 'object' && typeof item2 !== 'object') { + if (item1[trackItemBy[i]] != item2) { + return false; + } + } + } + return true; + } else { + return item1 == item2; + } + } + + let listResult = true; + let keyResult = true; + if (this.selectedItems && this.selectedItems.length && typeof this.selectedItems[0] == 'object') { + const itemJson = Object.create(null); + Object.keys(this.selectedItems[0]).forEach((k, i) => { + itemJson[k] = item[i]; + }); + if (this.selectedItems.some(si => compareWithKeyProperty(itemJson, si, this.trackItemBy))) { + listResult = false; + } + } + if (this.keyword !== null && this.keyword !== undefined) { + if (typeof item == 'string') { + keyResult = item.toLowerCase().includes(this.keyword.toLowerCase()) + } else if (this.fields) { + keyResult = (this.fields).find(field => { + const value: string = !item || item[field] === undefined || item[field] === null ? '' : item[field].toString(); + return value.toLowerCase().includes(this.keyword.toLowerCase()) + }) + } else { + keyResult = false + } + } + return listResult && keyResult; +}; + +@Component({ + selector: 'jigsaw-transfer, j-transfer', + templateUrl: './transfer.html', + host: { + '[class.jigsaw-transfer]': 'true', + '[style.width]': 'width', + '[style.height]': 'height' + } +}) +export class JigsawTransfer extends AbstractJigsawGroupLiteComponent implements OnDestroy { + private _removePageableCallbackListener: CallbackRemoval; + private _removeArrayCallbackListener: CallbackRemoval; + private _removeSelectedArrayCallbackListener: CallbackRemoval; + private _filterFunction: (item: any) => boolean; + + private _data: LocalPageableArray | PageableArray; + + @Input() + public get data() { + return this._data; + } + + public set data(value: any[] | ArrayCollection | LocalPageableArray | PageableArray) { + if (!value || value == this.data) return; + if ((value instanceof LocalPageableArray || value instanceof PageableArray) && value.pagingInfo) { + this._data = value; + this._filterFunction = value instanceof LocalPageableArray ? transferFilterFunction : transferServerFilterFunction; + this.callLater(() => { + // 等待输入属性初始化 + this._filterDataBySelectedItems(); + }); + if (value instanceof LocalPageableArray) { + if (this._removePageableCallbackListener) { + this._removePageableCallbackListener(); + } + this._removePageableCallbackListener = value.onAjaxComplete(() => { + this._filterDataBySelectedItems(); + }) + } + } else if (value instanceof Array || value instanceof ArrayCollection) { + this._data = new LocalPageableArray(); + this._data.pagingInfo.pageSize = Infinity; + this._data.fromArray(value); + this._filterFunction = transferFilterFunction; + this.callLater(() => { + // 等待输入属性初始化 + this._filterDataBySelectedItems(); + }); + if (value instanceof ArrayCollection) { + if (this._removeArrayCallbackListener) { + this._removeArrayCallbackListener(); + } + this._removeArrayCallbackListener = value.onAjaxSuccess(res => { + (>this._data).fromArray(res); + this._filterDataBySelectedItems(); + }) + } + } else { + console.error('data type error, data support Array, ArrayCollection, LocalPageableArray and PageableArray.') + } + } + + private _selectedItems: ArrayCollection | any[] = []; + + @Input() + public get selectedItems() { + return this._selectedItems; + } + + public set selectedItems(value: ArrayCollection | any[]) { + if (!value || this._selectedItems == value) return; + if (!(value instanceof Array) && !(value instanceof ArrayCollection)) { + console.error('selectedItems type error, selectedItems support Array and ArrayCollection'); + return; + } + this._selectedItems = value; + if (value instanceof ArrayCollection) { + if (this._removeSelectedArrayCallbackListener) { + this._removeSelectedArrayCallbackListener(); + } + this._removeSelectedArrayCallbackListener = value.onAjaxComplete(this._filterDataBySelectedItems, this); + } + } + + @Input() + public subLabelField: string; + + @Input() + public searchable: boolean; + + /** + * @internal + */ + public _$sourceSelectedItems: ArrayCollection | GroupOptionValue[]; + /** + * @internal + */ + public _$targetSelectedItems: ArrayCollection | GroupOptionValue[]; + + private _filterDataBySelectedItems() { + if(this._data.busy) { + const removeAjaxCallback = this._data.onAjaxComplete(() => { + removeAjaxCallback(); + this._filterData(); + }) + } else { + this._filterData(); + } + } + + private _filterData() { + this._data.filter(this._filterFunction, {selectedItems: [].concat(...this.selectedItems), trackItemBy: this.trackItemBy}); + } + + /** + * @Internal + * + * data和selectedItems不和list里数据双绑,list里面要做一些转换 + * + * @param {string} from + * @private + */ + public _$transferTo(from: string) { + if (from == 'target') { + if (!this._$sourceSelectedItems || !this._$sourceSelectedItems.length) return; + this.selectedItems = this.selectedItems ? this.selectedItems : []; + this.selectedItems.push(...this._$sourceSelectedItems); + this.selectedItems = this.selectedItems.concat(); + if ((this.data instanceof LocalPageableArray || this.data instanceof PageableArray) && this.data.pagingInfo) { + this._filterDataBySelectedItems(); + } + this._$sourceSelectedItems = []; + } + if (from == 'source') { + if (!this._$targetSelectedItems || !this._$targetSelectedItems.length) return; + this.selectedItems = this.selectedItems.filter(item => + !this._$targetSelectedItems.some(i => CommonUtils.compareWithKeyProperty(item, i, this.trackItemBy))); + if ((this.data instanceof LocalPageableArray || this.data instanceof PageableArray) && this.data.pagingInfo) { + this._filterDataBySelectedItems(); + } + this._$targetSelectedItems = []; + } + this.selectedItemsChange.emit(this.selectedItems); + } + + ngOnDestroy() { + super.ngOnDestroy(); + if (this._removePageableCallbackListener) { + this._removePageableCallbackListener(); + this._removePageableCallbackListener = null; + } + if (this._removeArrayCallbackListener) { + this._removeArrayCallbackListener(); + this._removeArrayCallbackListener = null; + } + if (this._removeSelectedArrayCallbackListener) { + this._removeSelectedArrayCallbackListener(); + this._removeSelectedArrayCallbackListener = null; + } + if (this.data) { + (>this.data).destroy(); + } + if (this.selectedItems instanceof ArrayCollection) { + this.selectedItems.destroy(); + } + } +} + +@Component({ + selector: 'jigsaw-transfer-list, j-transfer-list', + templateUrl: './transfer-list.html', + host: { + '[class.jigsaw-transfer-list-frame]': 'true' + } +}) +export class JigsawTransferInternalList extends AbstractJigsawGroupLiteComponent implements OnDestroy { + constructor(@Optional() private _transfer: JigsawTransfer) { + super(); + this._removeHostSubscribe = _transfer.selectedItemsChange.subscribe(() => { + this._$searchKey = ''; + }) + } + + @Input() + public isTarget: boolean; + + private _data: LocalPageableArray | PageableArray; + + @Input() + public get data(): LocalPageableArray | PageableArray { + return this._data; + } + + public set data(value: LocalPageableArray | PageableArray) { + if (!value || this._data == value) return; + if ((value instanceof LocalPageableArray || value instanceof PageableArray) && value.pagingInfo) { + this._data = value; + (>this._data).onRefresh(() => { + if (this.selectedItems) { + this.selectedItems = this.selectedItems.concat(); + this._$updateCurrentPageSelectedItems(); + } + }); + this._filterFunction = value instanceof LocalPageableArray ? transferFilterFunction : transferServerFilterFunction; + } else if (value instanceof Array || value instanceof ArrayCollection) { + this._filterFunction = transferFilterFunction; + this._updateData(value); + if (value instanceof ArrayCollection) { + if (this._removeArrayCallbackListener) { + this._removeArrayCallbackListener(); + } + this._removeArrayCallbackListener = value.onAjaxSuccess(this._updateData, this); + } + } + } + + @Input() + public subLabelField: string; + + @Input() + public trackItemBy: string | string[]; + + @Input() + public searchable: boolean; + + private _removeHostSubscribe: Subscriber; + private _filterFunction: (item: any) => boolean; + private _removeArrayCallbackListener: CallbackRemoval; + + /** + * @internal + */ + public _$searchKey: string; + + /** + * @internal + */ + public _$currentPageSelectedItems: any[] | ArrayCollection; + + /** + * @internal + */ + public _$infinity = Infinity; + + /** + * @internal + */ + public get _$trackByFn() { + return InternalUtils.trackByFn(this.trackItemBy); + }; + + /** + * 这边把transfer过来的数组转成分页数据,中间变量data主要用于消除数据闪动 + * @param {GroupOptionValue[] | ArrayCollection} value + * @private + */ + private _updateData(value: GroupOptionValue[] | ArrayCollection) { + if (!(value instanceof Array) && !(value instanceof ArrayCollection)) return; + const data = new LocalPageableArray(); + if (this.isTarget && this._transfer.data && (>this._transfer.data).pagingInfo) { + // target列同步用户给的data的pageSize + data.pagingInfo.pageSize = (>this._transfer.data).pagingInfo.pageSize; + } else { + data.pagingInfo.pageSize = Infinity; + } + data.fromArray(value); + const removeDataOnRefresh = data.onRefresh(() => { + removeDataOnRefresh(); + this._data = data; + // 用于刷新分页 + this._data.onRefresh(() => { + if (this.selectedItems) { + this.selectedItems = this.selectedItems.concat(); + this._$updateCurrentPageSelectedItems(); + } + }); + this._data.refresh(); + }) + } + + /** + * @internal + */ + public _$handleSearching(filterKey?: string) { + filterKey = filterKey ? filterKey.trim() : ''; + let field: string | number = this.labelField; + if (this._data instanceof PageableArray && this._data.length && typeof this._data[0] == 'object') { + field = Object.keys(this._data[0]).findIndex(k => k === this.labelField); + } + if(this._data.busy) { + const removeAjaxCallback = this._data.onAjaxComplete(() => { + removeAjaxCallback(); + this._filterData(filterKey, field); + }) + } else { + this._filterData(filterKey, field); + } + } + + private _filterData(filterKey: string, field: string | number) { + this.callLater(() => { + // 触发变更检查 + this._data.filter(this._filterFunction, { + selectedItems: this.isTarget ? null : [].concat(...this._transfer.selectedItems), + trackItemBy: this._transfer.trackItemBy, + keyword: filterKey, + fields: [field] + }); + }) + } + + /** + * @internal + * + * 这里有几个需要注意的地方: + * - this.data.concat() 不仅为了浅拷贝数据,当this.data是分页数据的时候,还有一个目的是为了取出this.data的当前页数据 + * - 在过滤选中的条目是直接用了`item.disabled`,在item的类型是字符串时,也没问题,因为字符串的时候,`item.disabled`必然是false + */ + public _$handleHeadSelect(checked) { + this._$currentPageSelectedItems = checked ? this.data.concat().filter(item => !item.disabled) : []; + this.selectedItems = this.selectedItems ? this.selectedItems : []; + if (checked) { + this.selectedItems.push(...this.data.concat().filter(item => + !item.disabled && !this.selectedItems.some(it => CommonUtils.compareWithKeyProperty(it, item, this.trackItemBy)))); + this.selectedItems = this.selectedItems.concat(); + } else { + this.selectedItems = this.selectedItems.filter(item => + !item.disabled && !(this.data).some(it => CommonUtils.compareWithKeyProperty(it, item, this.trackItemBy))) + } + this.selectedItemsChange.emit(this.selectedItems); + } + + /** + * @internal + */ + public _$updateCurrentPageSelectedItems() { + this.callLater(() => { + // 初始化时触发变更检查 + this.selectedItems = this.selectedItems ? this.selectedItems : []; + if (this.data && this.data.pagingInfo && this.data.pagingInfo.pageSize != Infinity) { + this._$currentPageSelectedItems = this.selectedItems.filter(item => (this.data).some(it => + CommonUtils.compareWithKeyProperty(it, item, this.trackItemBy))); + } else { + this._$currentPageSelectedItems = this.selectedItems; + } + }); + } + + /** + * @internal + */ + public _$updateSelectedItemsByCurrent() { + this._$currentPageSelectedItems = this._$currentPageSelectedItems ? this._$currentPageSelectedItems : []; + this.selectedItems = this.selectedItems ? this.selectedItems : []; + this.selectedItems.push(...this._$currentPageSelectedItems.filter(item => + !this.selectedItems.some(it => CommonUtils.compareWithKeyProperty(item, it, this.trackItemBy)))); + const currentUnselectedItems = this.data.concat().filter(item => + !this._$currentPageSelectedItems.some(it => CommonUtils.compareWithKeyProperty(item, it, this.trackItemBy))); + this.selectedItems = this.selectedItems.filter(item => + !currentUnselectedItems.some(it => CommonUtils.compareWithKeyProperty(item, it, this.trackItemBy))); + this.selectedItemsChange.emit(this.selectedItems); + } + + ngOnDestroy() { + super.ngOnDestroy(); + if (this._removeHostSubscribe) { + this._removeHostSubscribe.unsubscribe(); + this._removeHostSubscribe = null; + } + if (this._removeArrayCallbackListener) { + this._removeArrayCallbackListener(); + this._removeArrayCallbackListener = null; + } + if (this.data) { + this.data.destroy(); + } + if (this.selectedItems instanceof ArrayCollection) { + this.selectedItems.destroy(); + } + } +} + +@NgModule({ + imports: [JigsawListModule, JigsawCheckBoxModule, PerfectScrollbarModule, JigsawInputModule, JigsawPaginationModule, CommonModule], + declarations: [JigsawTransfer, JigsawTransferInternalList], + exports: [JigsawTransfer] +}) +export class JigsawTransferModule { +} diff --git a/src/jigsaw/core/data/array-collection.ts b/src/jigsaw/core/data/array-collection.ts index b665c265f2..84fd1818a0 100644 --- a/src/jigsaw/core/data/array-collection.ts +++ b/src/jigsaw/core/data/array-collection.ts @@ -20,7 +20,8 @@ import { PagingInfo, PreparedHttpClientOptions, SortAs, - SortOrder + SortOrder, + serializeFilterFunction } from "./component-data"; import {TableData} from "./table-data"; @@ -582,10 +583,10 @@ export class ArrayCollection extends JigsawArray implements IAjaxComponent console.log('destroying ArrayCollection....'); this.splice(0, this.length); - this.componentDataHelper.clearCallbacks(); + this.componentDataHelper && this.componentDataHelper.clearCallbacks(); this.componentDataHelper = null; this.dataReviser = null; - this._emitter.unsubscribe(); + this._emitter && this._emitter.unsubscribe(); this._emitter = null; } @@ -634,6 +635,9 @@ export class PageableArray extends ArrayCollection implements IServerSidePa } this.pagingInfo = new PagingInfo(); + this.pagingInfo.subscribe(() => { + this._ajax(); + }); this.sourceRequestOptions = typeof requestOptionsOrUrl === 'string' ? {url: requestOptionsOrUrl} : requestOptionsOrUrl; this._initSubjects(); @@ -738,7 +742,6 @@ export class PageableArray extends ArrayCollection implements IServerSidePa return; } const paging = data.paging; - this.pagingInfo.currentPage = paging.hasOwnProperty('currentPage') ? paging.currentPage : this.pagingInfo.currentPage; this.pagingInfo.totalRecord = paging.hasOwnProperty('totalRecord') ? paging.totalRecord : this.pagingInfo.totalRecord; } @@ -760,7 +763,7 @@ export class PageableArray extends ArrayCollection implements IServerSidePa pfi = term; } else if (term instanceof Function) { // 这里的fields相当于thisArg,即函数执行的上下文对象 - pfi = new DataFilterInfo(undefined, undefined, term.toString(), fields); + pfi = new DataFilterInfo(undefined, undefined, serializeFilterFunction(term), fields); } else { pfi = new DataFilterInfo(term, fields); } @@ -829,12 +832,13 @@ export class PageableArray extends ArrayCollection implements IServerSidePa this.http = null; this.sourceRequestOptions = null; + this.pagingInfo && this.pagingInfo.unsubscribe(); this.pagingInfo = null; this.filterInfo = null; this.sortInfo = null; - this._filterSubject.unsubscribe(); + this._filterSubject && this._filterSubject.unsubscribe(); this._filterSubject = null; - this._sortSubject.unsubscribe(); + this._sortSubject && this._sortSubject.unsubscribe(); this._sortSubject = null; } } @@ -874,7 +878,7 @@ export class LocalPageableArray extends ArrayCollection implements IPageab public set filteredData(value: T[]) { this._filteredData = value; - if (this._filteredData instanceof Array) { + if (this._filteredData instanceof Array || this._filteredData instanceof ArrayCollection) { this.pagingInfo.totalRecord = this._filteredData.length; } } @@ -900,20 +904,29 @@ export class LocalPageableArray extends ArrayCollection implements IPageab return this; } + /** + * @internal + * @param item + * @param {string} keyword + * @param {any[]} fields + * @returns {boolean} + */ + public static filterItemByKeyword(item: any, keyword: string, fields: any[]): boolean { + if (typeof item == 'string') { + return item.toLowerCase().includes(keyword.toLowerCase()) + } else if (fields) { + return fields.find(field => { + const value: string = !item || item[field] === undefined || item[field] === null ? '' : item[field].toString(); + return value.toLowerCase().includes(keyword.toLowerCase()) + }) + } else { + return false + } + } + private _initSubjects(): void { this._filterSubject.debounceTime(300).subscribe(filter => { - this.filteredData = this._bakData.filter(item => { - if (typeof item == 'string') { - return item.toLowerCase().includes(filter.key.toLowerCase()) - } else if (filter.field) { - return (filter.field).find(field => { - const value: string = !item || item[field] === undefined || item[field] === null ? '' : item[field].toString(); - return value.toLowerCase().includes(filter.key.toLowerCase()) - }) - } else { - return false - } - }); + this.filteredData = this._bakData.filter(item => LocalPageableArray.filterItemByKeyword(item, filter.key, filter.field)); this.firstPage(); }); @@ -928,19 +941,21 @@ export class LocalPageableArray extends ArrayCollection implements IPageab }) } - public filter(callbackfn: (value: any, index: number, array: any[]) => any, thisArg?: any): any; + public filter(callbackfn: (value: any, index: number, array: any[]) => any, context?: any): any; public filter(term: string, fields?: string[] | number[]): void; public filter(term: DataFilterInfo): void; /** * @internal */ public filter(term, fields?: string[] | number[]): void { + if(!this._bakData) return; if (term instanceof Function) { - this.filteredData = this._bakData.filter(term); + this.filteredData = this._bakData.filter(term.bind(fields)); this.firstPage(); + } else { + const pfi = term instanceof DataFilterInfo ? term : new DataFilterInfo(term, fields); + this._filterSubject.next(pfi); } - const pfi = term instanceof DataFilterInfo ? term : new DataFilterInfo(term, fields); - this._filterSubject.next(pfi); } public sort(compareFn?: (a: any, b: any) => number): any; @@ -1016,11 +1031,13 @@ export class LocalPageableArray extends ArrayCollection implements IPageab public destroy() { super.destroy(); - this._filterSubject.unsubscribe(); - this._sortSubject.unsubscribe(); - this.pagingInfo.unsubscribe(); + this._filterSubject && this._filterSubject.unsubscribe(); + this._sortSubject && this._sortSubject.unsubscribe(); + this.pagingInfo && this.pagingInfo.unsubscribe(); this._bakData = null; this.filteredData = null; this.pagingInfo = null; + this._filterSubject = null; + this._sortSubject = null; } } diff --git a/src/jigsaw/core/data/component-data.ts b/src/jigsaw/core/data/component-data.ts index 5e146efee7..b4f142ad51 100644 --- a/src/jigsaw/core/data/component-data.ts +++ b/src/jigsaw/core/data/component-data.ts @@ -787,3 +787,15 @@ export interface IEmittable { */ unsubscribe(); } + +export function serializeFilterFunction(filter: Function): string { + if (!filter) { + return undefined; + } + let funcString = filter.toString(); + if (!funcString.match(/(const|var|let)\s+_this\s*=\s*this\b/)) { + // 在函数的开头添加一行 `var _this = this;` + funcString = funcString.replace(/{/, '{\nvar _this = this;\n'); + } + return funcString; +} diff --git a/src/jigsaw/core/data/table-data.ts b/src/jigsaw/core/data/table-data.ts index ca0bf2cfb7..37c12af1ae 100644 --- a/src/jigsaw/core/data/table-data.ts +++ b/src/jigsaw/core/data/table-data.ts @@ -17,6 +17,7 @@ import { PreparedHttpClientOptions, SortAs, SortOrder, + serializeFilterFunction, ViewportData } from "./component-data"; import {CommonUtils} from "../utils/common-utils"; @@ -519,7 +520,7 @@ export class PageableTableData extends TableData implements IServerSidePageable, pfi = term; } else if (term instanceof Function) { // 这里的fields相当于thisArg,即函数执行的上下文对象 - pfi = new DataFilterInfo(undefined, undefined, term.toString(), fields); + pfi = new DataFilterInfo(undefined, undefined, serializeFilterFunction(term), fields); } else { pfi = new DataFilterInfo(term, fields); } diff --git a/src/jigsaw/core/theming/all-theme.scss b/src/jigsaw/core/theming/all-theme.scss index 225aadabf4..12b5617e2c 100644 --- a/src/jigsaw/core/theming/all-theme.scss +++ b/src/jigsaw/core/theming/all-theme.scss @@ -48,4 +48,5 @@ @import "../../component/input/auto-complete-input"; @import "../../component/upload/upload"; @import "../../component/icon/icon"; +@import "../../component/transfer/transfer"; @import "../../component/textarea/textarea"; diff --git a/src/jigsaw/module.ts b/src/jigsaw/module.ts index 13921f1372..84e9c1b578 100644 --- a/src/jigsaw/module.ts +++ b/src/jigsaw/module.ts @@ -47,6 +47,7 @@ import {JigsawRadioLiteModule} from "./component/radio/radio-lite"; import {JigsawButtonBarModule} from "./component/list-and-tile/button-bar"; import {JigsawIconModule} from "./component/icon/icon"; import {JigsawUploadModule} from "./component/upload/index"; +import {JigsawTransferModule} from "./component/transfer/transfer"; import {JigsawTextareaModule} from "./component/textarea/index"; const JIGSAW_MODULE = [ @@ -98,6 +99,7 @@ const JIGSAW_MODULE = [ JigsawViewportModule, JigsawIconModule, JigsawUploadModule, + JigsawTransferModule, JigsawTextareaModule ]; diff --git a/src/jigsaw/public_api.ts b/src/jigsaw/public_api.ts index 666b8efd4a..7311501125 100644 --- a/src/jigsaw/public_api.ts +++ b/src/jigsaw/public_api.ts @@ -54,6 +54,7 @@ export * from "./component/tree/ztree-types"; export * from "./component/viewport/viewport"; export * from "./component/icon/icon"; export * from "./component/upload/index"; +export * from "./component/transfer/transfer"; export * from "./core/data/array-collection"; export * from "./core/data/component-data"; export * from "./core/data/echart-types";