377 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { h, Component, render } from '@/components/preact'
 | |
| import { isFunction, isArray, safety, deepMerge, mergeArr, IEVersion } from '@/components/common/util'
 | |
| 
 | |
| /**
 | |
|  * 普通的多选渲染
 | |
|  */
 | |
| class General extends Component{
 | |
| 
 | |
| 	constructor(options){
 | |
| 		super(options);
 | |
| 
 | |
| 		this.setState({
 | |
| 			filterValue: '',
 | |
| 			remote: true,
 | |
| 			loading: false,
 | |
| 			pageIndex: 1,
 | |
| 			pageSize: 10,
 | |
| 		});
 | |
| 
 | |
|         this.searchCid = 0;
 | |
|         this.inputOver = true;
 | |
|         this.__value = '';
 | |
| 	}
 | |
| 
 | |
| 	optionClick(item, selected, disabled, e){
 | |
| 		this.props.ck(item, selected, disabled);
 | |
| 		//阻止父组件上的事件冒泡
 | |
| 		this.blockClick(e);
 | |
| 	}
 | |
| 
 | |
|     groupClick(item, e){
 | |
|         const { click, children, disabled } = this.props.prop;
 | |
|         let m = item[click], arr = item[children].filter(opt => !opt[disabled]);
 | |
| 
 | |
|         if(m === 'SELECT'){
 | |
|             this.props.onReset(arr, 'append');
 | |
|         }else if(m === 'CLEAR'){
 | |
|             this.props.onReset(arr, 'delete');
 | |
|         }else if(m === 'AUTO'){
 | |
|             this.props.onReset(arr, 'auto');
 | |
|         }else if(isFunction(m)){
 | |
|             m(item);
 | |
|         }
 | |
|         //阻止父组件上的事件冒泡
 | |
|         this.blockClick(e);
 | |
|     }
 | |
| 
 | |
| 	blockClick(e){
 | |
| 		e.stopPropagation();
 | |
| 	}
 | |
| 
 | |
| 	pagePrevClick(e){
 | |
| 		let index = this.state.pageIndex;
 | |
| 		if(index <= 1){
 | |
| 			return ;
 | |
| 		}
 | |
| 		this.changePageIndex(index - 1);
 | |
| 	}
 | |
| 	pageNextClick(e, size){
 | |
| 		let index = this.state.pageIndex;
 | |
| 		if(index >= size){
 | |
| 			return ;
 | |
| 		}
 | |
| 		this.changePageIndex(index + 1);
 | |
| 	}
 | |
| 
 | |
| 	changePageIndex(index){
 | |
| 		this.setState({
 | |
| 			pageIndex: index
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	searchInput(e){
 | |
| 		let v = e.target.value;
 | |
| 
 | |
|         if(v === this.__value){
 | |
|             return ;
 | |
|         }
 | |
| 
 | |
|         clearTimeout(this.searchCid);
 | |
|         if(this.inputOver){
 | |
|             //保证输入框内的值是实时的
 | |
|             this.__value = v;
 | |
| 
 | |
|             //让搜索变成异步的
 | |
|             this.searchCid = setTimeout(() => {
 | |
|                 this.callback = true;
 | |
|                 this.setState({ filterValue: this.__value, remote: true })
 | |
|             }, this.props.delay);
 | |
|         }
 | |
| 	}
 | |
| 
 | |
| 	focus(){
 | |
| 		this.searchInputRef && this.searchInputRef.focus();
 | |
| 	}
 | |
| 
 | |
| 	blur(){
 | |
| 		this.searchInputRef && this.searchInputRef.blur();
 | |
| 	}
 | |
| 
 | |
| 	handleComposition(e){
 | |
| 		let type = e.type;
 | |
| 
 | |
| 		if(type === 'compositionstart'){
 | |
| 			this.inputOver = false;
 | |
|             clearTimeout(this.searchCid);
 | |
| 		}else if(type === 'compositionend'){
 | |
| 			this.inputOver = true;
 | |
|             this.searchInput(e);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	componentWillReceiveProps(props){
 | |
| 		if(this.props.show != props.show){
 | |
| 			if(!props.show){
 | |
| 				//清空输入框的值
 | |
| 				this.setState({ filterValue: '' });
 | |
|                 this.__value = '';
 | |
|                 this.searchInputRef && (this.searchInputRef.value = '');
 | |
| 			}else{
 | |
| 				//聚焦输入框
 | |
| 				setTimeout(() => this.focus(), 0);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
|     componentDidUpdate(){
 | |
|         if(this.callback){
 | |
|             this.callback = false;
 | |
| 
 | |
|             let done = this.props.filterDone;
 | |
|             if(isFunction(done)){
 | |
|                 done(this.state.filterValue, this.tempData || []);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
| 	render(config) {
 | |
| 
 | |
| 		let { data, prop, template, theme, radio, sels, empty, filterable, filterMethod, remoteSearch, remoteMethod, delay, searchTips } = config
 | |
| 
 | |
| 		const { name, value, disabled, children, optgroup } = prop;
 | |
| 
 | |
| 		let arr = deepMerge([], data);
 | |
| 		//是否开启了搜索
 | |
| 		if(filterable){
 | |
| 			if(remoteSearch){//是否进行远程搜索
 | |
| 				if(this.state.remote){
 | |
|                     this.callback = false;
 | |
| 					this.setState({ loading: true, remote: false });
 | |
| 					//让输入框失去焦点
 | |
| 					this.blur();
 | |
| 					remoteMethod(this.state.filterValue, result => {
 | |
| 						//回调后可以重新聚焦
 | |
| 						this.focus();
 | |
| 
 | |
|                         this.callback = true;
 | |
| 						this.setState({ loading: false });
 | |
| 						this.props.onReset(result, 'data');
 | |
| 					}, this.props.show);
 | |
| 				}
 | |
| 			}else{
 | |
|                 const filterData = (item, index) => {
 | |
|                     const isGroup = item[optgroup];
 | |
|                     if(isGroup){
 | |
|                         delete item.__del;
 | |
|                         return true;
 | |
|                     }
 | |
|                     return filterMethod(this.state.filterValue, item, index, prop);
 | |
|                 }
 | |
| 				arr = arr.filter(filterData);
 | |
| 
 | |
|                 for(let i = 0; i < arr.length - 1; i++){
 | |
|                     let a = arr[i];
 | |
|                     let b = arr[i + 1];
 | |
|                     if(a[optgroup] && b[optgroup]){
 | |
|                         arr[i].__del = true;
 | |
|                     }
 | |
|                 }
 | |
|                 if(arr.length && arr[arr.length - 1][optgroup]){
 | |
|                     arr[arr.length - 1].__del = true;
 | |
|                 }
 | |
|                 arr = arr.filter(item => !item.__del);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		const search = (
 | |
| 			<div class='xm-search'>
 | |
| 				<i class="xm-iconfont xm-icon-sousuo"></i>
 | |
| 				<input type="text" class="xm-input xm-search-input" placeholder={ searchTips }
 | |
| 					ref={ input => this.searchInputRef = input }
 | |
| 					onClick={ this.blockClick.bind(this) }
 | |
|                     onInput={ this.searchInput.bind(this) }
 | |
| 					onCompositionStart={ this.handleComposition.bind(this) }
 | |
| 					onCompositionUpdate={ this.handleComposition.bind(this) }
 | |
| 					onCompositionEnd={ this.handleComposition.bind(this) }
 | |
| 				/>
 | |
| 			</div>
 | |
| 		);
 | |
| 
 | |
|         //如果是分组模式, 要分页先去除分组, 然后在计算分页, 最后再加上分组
 | |
|         let groups = [], groupInfo = {};
 | |
|         groups = arr.filter(item => item[optgroup]).forEach(g => {
 | |
|             g[children].forEach(item => groupInfo[item[value]] = g);
 | |
|         });
 | |
|         arr = arr.filter(item => !item[optgroup]);
 | |
| 
 | |
| 		let paging = '';
 | |
| 		if(config.paging){
 | |
| 			//计算当前分页的总页码
 | |
| 			const size = Math.floor((arr.length - 1) / config.pageSize) + 1;
 | |
| 
 | |
| 			//如果当前页码大于总页码, 重置一下
 | |
| 			if(this.state.pageIndex > size){
 | |
| 				this.changePageIndex(size);
 | |
| 			}
 | |
| 
 | |
| 			//如有总页码>1, 但是因为搜索造成的页码=0的情况
 | |
| 			if(size > 0 && this.state.pageIndex <= 0){
 | |
| 				this.changePageIndex(1);
 | |
| 			}
 | |
| 
 | |
| 			//实现简单的物理分页
 | |
| 			let start = (this.state.pageIndex - 1) * config.pageSize;
 | |
| 			let end = start + config.pageSize;
 | |
| 			arr = arr.slice(start, end);
 | |
| 
 | |
| 			const disabledStyle = {cursor: 'no-drop', color: '#d2d2d2'};
 | |
| 
 | |
| 			let prevStyle = {}, nextStyle = {};
 | |
| 			this.state.pageIndex <= 1 && (prevStyle = disabledStyle);
 | |
| 			this.state.pageIndex == size && (nextStyle = disabledStyle);
 | |
| 
 | |
| 			// const defaultCurrClass = {
 | |
| 			// 	position: 'relative',
 | |
| 			// 	borderRadius: '1px',
 | |
| 			// }
 | |
| 			// {
 | |
| 			// 	''.padEnd(size, ' ').split('').map((s, i) => (
 | |
| 			// 		<span style={
 | |
| 			// 			this.state.pageIndex == i + 1 ? {
 | |
| 			// 				...defaultCurrClass,
 | |
| 			// 				backgroundColor: theme.color,
 | |
| 			// 				borderColor: theme.color,
 | |
| 			// 				color: '#FFF',
 | |
| 			// 			} : defaultCurrClass
 | |
| 			// 		}>{ i + 1 }</span>
 | |
| 			// 	))
 | |
| 			// }
 | |
| 			paging = (
 | |
| 				<div class='xm-paging'>
 | |
| 					<span style={ prevStyle } onClick={ this.pagePrevClick.bind(this) }>上一页</span>
 | |
| 					<span>{ this.state.pageIndex } / { size }</span>
 | |
| 					<span style={ nextStyle } onClick={ e => this.pageNextClick.bind(this, e, size)() }>下一页</span>
 | |
| 				</div>
 | |
| 			)
 | |
| 
 | |
| 		}else{
 | |
|             //检查是否设置了显示数量上限
 | |
|             if(config.showCount > 0){
 | |
|                 arr = arr.slice(0, config.showCount);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let newArr = [], group;
 | |
|         arr.forEach(item => {
 | |
|             let g = groupInfo[item[value]];
 | |
|             if(g != group){
 | |
|                 group = g;
 | |
|                 newArr.push(group);
 | |
|             }
 | |
|             newArr.push(item);
 | |
|         });
 | |
|         arr = newArr;
 | |
| 
 | |
|         let safetyArr = deepMerge([], arr);
 | |
|         this.tempData = safetyArr;
 | |
| 
 | |
|         //工具条操作
 | |
|         const toolbar = (
 | |
|             <div class='xm-toolbar'>
 | |
|                 { config.toolbar.list.map(tool => {
 | |
|                     const toolClass = 'toolbar-tag';
 | |
| 
 | |
|                     let info;
 | |
|                     if(tool === 'ALL'){
 | |
|                         info = { icon: 'xm-iconfont xm-icon-quanxuan', name: '全选', method: (pageData) => {
 | |
|                             const { optgroup, disabled } = prop;
 | |
|                             const list = pageData.filter(item => !item[optgroup]).filter(item => !item[disabled])
 | |
|                             this.props.onReset(mergeArr(list, sels, prop), 'sels');
 | |
|                         } };
 | |
|                     }else if(tool === 'CLEAR'){
 | |
|                         info = { icon: 'xm-iconfont xm-icon-qingkong', name: '清空', method: (pageData) => {
 | |
|                             this.props.onReset(sels.filter(item => item[prop.disabled]), 'sels');
 | |
|                         } };
 | |
|                     }else {
 | |
|                         info = tool
 | |
|                     }
 | |
| 
 | |
|                     const hoverChange = e => {
 | |
|                         if(e.type === 'mouseenter') e.target.style.color = theme.color;
 | |
|                         if(e.type === 'mouseleave') e.target.style.color = '';
 | |
|                     }
 | |
| 
 | |
|                     return (<div class={ toolClass } onClick={ () => {
 | |
|                         isFunction(info.method) && info.method(safetyArr)
 | |
|                     } } onMouseEnter={ hoverChange } onMouseLeave={ hoverChange }>
 | |
|                         { config.toolbar.showIcon && <i class={ info.icon }></i> }
 | |
|                         <span>{ info.name }</span>
 | |
|                     </div>)
 | |
|                 }).filter(a => a) }
 | |
|             </div>
 | |
|         )
 | |
| 
 | |
|         const showIcon = config.model.icon != 'hidden';
 | |
|         const renderItem = item => {
 | |
|             const selected = !!sels.find(sel => sel[value] == item[value])
 | |
|             const iconStyle = selected ? {
 | |
|             	color: theme.color,
 | |
|             	border: 'none'
 | |
|             } : {
 | |
|             	borderColor: theme.color,
 | |
|             };
 | |
|             const itemStyle = {}
 | |
|             if(!showIcon && selected){
 | |
|                 itemStyle.backgroundColor = theme.color;
 | |
|                 item[disabled] && (itemStyle.backgroundColor = '#C2C2C2');
 | |
|             }
 | |
|             const className = ['xm-option', (item[disabled] ? ' disabled' : ''), (selected ? ' selected' : ''), (showIcon ? 'show-icon' : 'hide-icon') ].join(' ');
 | |
|             const iconClass = ['xm-option-icon xm-iconfont', radio ? 'xm-icon-danx' : 'xm-icon-duox'].join(' ');
 | |
| 
 | |
|             return (
 | |
|             	<div class={ className } style={ itemStyle } value={ item[value] } onClick={ this.optionClick.bind(this, item, selected, item[disabled]) }>
 | |
|             		{ showIcon && <i class={ iconClass } style={ iconStyle }></i> }
 | |
|             		<div class='xm-option-content' dangerouslySetInnerHTML={{ __html: template({ data, item, arr: sels, name: item[name], value: item[value] }) }}></div>
 | |
|             	</div>
 | |
|             )
 | |
|         }
 | |
| 
 | |
|         const renderGroup = item => {
 | |
|             const isGroup = item[optgroup];
 | |
|             if(isGroup){//分组模式
 | |
|                 return (
 | |
|                     <div class="xm-group">
 | |
|                         <div class="xm-group-item" onClick={ this.groupClick.bind(this, item) }>{ item[name] }</div>
 | |
|                     </div>
 | |
|                 )
 | |
|             }
 | |
|             return renderItem(item);
 | |
|         }
 | |
| 
 | |
| 		arr = arr.map(renderGroup);
 | |
| 
 | |
| 		if(!arr.length){
 | |
| 			arr.push(
 | |
| 				<div class="xm-select-empty">{ empty }</div>
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		return (
 | |
| 			<div onClick={ this.blockClick }>
 | |
| 				<div>
 | |
| 					{ config.toolbar.show && toolbar }
 | |
| 					{ filterable && search }
 | |
| 					<div class="scroll-body" style={ {maxHeight: config.height} }>{ arr }</div>
 | |
| 					{ config.paging && paging }
 | |
| 				</div>
 | |
| 				{ this.state.loading && <div class="loading" >
 | |
| 					<span class="loader"></span>
 | |
| 				</div> }
 | |
| 			</div>
 | |
| 		)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| export default General;
 |