Merge pull request 'Add combo input component' (#21) from dev-mrkbear into master
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/21
This commit is contained in:
		
						commit
						2c329e9d17
					
				| @ -25,7 +25,7 @@ class AttrInput extends Component<IAttrInputProps> { | ||||
| 
 | ||||
|     private value: string = ""; | ||||
|     private error: ReactNode; | ||||
|     private numberTestReg = [/\.0*$/, /[1-9]+0+$/]; | ||||
|     private numberTestReg = [/\.0*$/, /\.\d*[1-9]+0+$/]; | ||||
| 
 | ||||
|     private numberTester(value: string) { | ||||
|         return isNaN((value as any) / 1) || | ||||
|  | ||||
							
								
								
									
										87
									
								
								source/Component/ComboInput/ComboInput.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								source/Component/ComboInput/ComboInput.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| @import "../Theme/Theme.scss"; | ||||
| 
 | ||||
| $line-min-height: 26px; | ||||
| 
 | ||||
| div.combo-input-root { | ||||
| 	width: 100%; | ||||
| 	display: flex; | ||||
| 	min-height: $line-min-height; | ||||
| 	padding: 5px 0; | ||||
| 
 | ||||
|     div.input-intro { | ||||
| 		width: 50%; | ||||
| 		height: 100%; | ||||
| 		max-width: 220px; | ||||
|         min-height: $line-min-height; | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
|         padding-right: 5px; | ||||
|         box-sizing: border-box; | ||||
| 	} | ||||
| 
 | ||||
|     div.root-content { | ||||
|         width: 50%; | ||||
|         height: 100%; | ||||
|         max-width: 180px; | ||||
|         min-height: $line-min-height; | ||||
|         border-radius: 3px; | ||||
|         overflow: hidden; | ||||
|         display: flex; | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|         div.value-view { | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             min-height: $line-min-height; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             padding-left: 8px; | ||||
|             white-space: nowrap; | ||||
|             word-break: keep-all; | ||||
|             text-overflow: ellipsis; | ||||
|             overflow: hidden; | ||||
| 
 | ||||
|             span { | ||||
|                 display: block; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         div.list-button { | ||||
|             width: $line-min-height; | ||||
|             height: $line-min-height; | ||||
|             flex-shrink: 0; | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             align-items: center; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| div.dark.combo-input-root { | ||||
| 
 | ||||
| 	div.root-content { | ||||
| 		background-color: $lt-bg-color-lvl3-dark; | ||||
| 		color: $lt-font-color-normal-dark; | ||||
| 	} | ||||
| 
 | ||||
| 	div.root-content:hover { | ||||
| 		background-color: $lt-bg-color-lvl2-dark; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| div.light.combo-input-root { | ||||
| 
 | ||||
| 	div.root-content { | ||||
| 		background-color: $lt-bg-color-lvl3-light; | ||||
| 		color: $lt-font-color-normal-light; | ||||
| 	} | ||||
| 
 | ||||
| 	div.root-content:hover { | ||||
| 		background-color: $lt-bg-color-lvl2-light; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| div.combo-picker-root { | ||||
|     width: 300px; | ||||
|     height: 340px; | ||||
| } | ||||
							
								
								
									
										87
									
								
								source/Component/ComboInput/ComboInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								source/Component/ComboInput/ComboInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| import { Component, createRef, ReactNode } from "react"; | ||||
| import { FontLevel, Theme } from "@Component/Theme/Theme"; | ||||
| import { PickerList, IDisplayItem } from "../PickerList/PickerList"; | ||||
| import { AllI18nKeys, Localization } from "@Component/Localization/Localization"; | ||||
| import { Icon } from "@fluentui/react"; | ||||
| import "./ComboInput.scss"; | ||||
| 
 | ||||
| interface IComboInputProps { | ||||
|     keyI18n: AllI18nKeys; | ||||
|     infoI18n?: AllI18nKeys; | ||||
|     allOption?: IDisplayItem[]; | ||||
|     value?: IDisplayItem; | ||||
|     valueChange?: (value: IDisplayItem) => any; | ||||
| } | ||||
| 
 | ||||
| interface IComboInputState { | ||||
|     isPickerVisible: boolean; | ||||
| } | ||||
| 
 | ||||
| class ComboInput extends Component<IComboInputProps, IComboInputState> { | ||||
| 
 | ||||
|     public constructor(props: IComboInputProps) { | ||||
|         super(props); | ||||
|         this.state = { | ||||
|             isPickerVisible: false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private pickerTarget = createRef<HTMLDivElement>(); | ||||
| 
 | ||||
|     private renderPicker() { | ||||
|         return <PickerList | ||||
|             target={this.pickerTarget} | ||||
|             displayItems={(this.props.allOption ?? []).map((item) => { | ||||
|                 return item.key === this.props.value?.key ?  | ||||
|                     {...item, mark: true} : item; | ||||
|             })} | ||||
|             clickDisplayItems={((item) => { | ||||
|                 if (this.props.valueChange) { | ||||
|                     this.props.valueChange(item); | ||||
|                 } | ||||
|                 this.setState({ | ||||
|                     isPickerVisible: false | ||||
|                 }) | ||||
|             })} | ||||
|             dismiss={() => { | ||||
|                 this.setState({ | ||||
|                     isPickerVisible: false | ||||
|                 }) | ||||
|             }} | ||||
|         /> | ||||
|     } | ||||
| 
 | ||||
|     public render(): ReactNode { | ||||
|         return <> | ||||
|             <Theme className="combo-input-root" fontLevel={FontLevel.normal}> | ||||
|                 <div className="input-intro"> | ||||
|                     <Localization i18nKey={this.props.keyI18n}/> | ||||
|                 </div> | ||||
|                 <div | ||||
|                     className="root-content" | ||||
|                     ref={this.pickerTarget} | ||||
|                     onClick={() => { | ||||
|                         this.setState({ | ||||
|                             isPickerVisible: true | ||||
|                         }) | ||||
|                     }} | ||||
|                 > | ||||
|                     <div className="value-view"> | ||||
|                         { | ||||
|                             this.props.value ?  | ||||
|                             <Localization i18nKey={this.props.value.nameKey}/> : | ||||
|                             null | ||||
|                         } | ||||
|                     </div> | ||||
|                     <div className="list-button"> | ||||
|                         <Icon iconName="ChevronDownMed"/> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </Theme> | ||||
| 
 | ||||
|             {this.state.isPickerVisible ?  this.renderPicker(): null} | ||||
|         </> | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export { ComboInput, IDisplayItem }; | ||||
| @ -69,13 +69,14 @@ class LabelPicker extends Component<ILabelPickerProps & IMixinStatusProps, ILabe | ||||
| 					}} | ||||
| 				/> | ||||
| 				{this.state.isPickerVisible ? <PickerList | ||||
|                     noData="Common.Attr.Key.Label.Picker.Nodata" | ||||
| 					objectList={this.getOtherLabel()} | ||||
| 					dismiss={() => { | ||||
| 						this.setState({ | ||||
| 							isPickerVisible: false | ||||
| 						}); | ||||
| 					}} | ||||
| 					click={(label) => { | ||||
| 					clickObjectItems={(label) => { | ||||
| 						if (label instanceof Label && this.props.labelAdd) { | ||||
| 							this.props.labelAdd(label) | ||||
| 						} | ||||
|  | ||||
| @ -37,6 +37,7 @@ div.picker-list-root { | ||||
| 
 | ||||
| 		div.list-item-name { | ||||
| 			width: 100%; | ||||
|             box-sizing: border-box; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Localization } from "@Component/Localization/Localization"; | ||||
| import { AllI18nKeys, Localization } from "@Component/Localization/Localization"; | ||||
| import { Callout, DirectionalHint, Icon } from "@fluentui/react"; | ||||
| import { CtrlObject } from "@Model/CtrlObject"; | ||||
| import { Group } from "@Model/Group"; | ||||
| @ -8,12 +8,20 @@ import { Component, ReactNode, RefObject } from "react"; | ||||
| import "./PickerList.scss"; | ||||
| 
 | ||||
| type IPickerListItem = CtrlObject | Label; | ||||
| type IDisplayItem = { | ||||
|     nameKey: AllI18nKeys; | ||||
|     key: string; | ||||
| 	mark?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface IPickerListProps { | ||||
|     displayItems?: IDisplayItem[]; | ||||
| 	objectList?: IPickerListItem[]; | ||||
| 	target?: RefObject<any>; | ||||
|     noData?: AllI18nKeys; | ||||
| 	dismiss?: () => any; | ||||
| 	click?: (item: IPickerListItem) => any; | ||||
| 	clickObjectItems?: (item: IPickerListItem) => any; | ||||
|     clickDisplayItems?: (item: IDisplayItem) => any; | ||||
| } | ||||
| 
 | ||||
| class PickerList extends Component<IPickerListProps> { | ||||
| @ -46,8 +54,8 @@ class PickerList extends Component<IPickerListProps> { | ||||
| 			className="picker-list-item" | ||||
| 			key={item.id} | ||||
| 			onClick={() => { | ||||
| 				if (this.props.click) { | ||||
| 					this.props.click(item) | ||||
| 				if (this.props.clickObjectItems) { | ||||
| 					this.props.clickObjectItems(item) | ||||
| 				} | ||||
| 			}} | ||||
| 		> | ||||
| @ -65,6 +73,27 @@ class PickerList extends Component<IPickerListProps> { | ||||
| 		</div>; | ||||
| 	} | ||||
| 
 | ||||
|     private renderString(item: IDisplayItem) { | ||||
|         return <div | ||||
| 			className="picker-list-item" | ||||
| 			key={item.key} | ||||
| 			onClick={() => { | ||||
| 				if (this.props.clickDisplayItems) { | ||||
| 					this.props.clickDisplayItems(item) | ||||
| 				} | ||||
| 			}} | ||||
| 		> | ||||
| 			<div className="list-item-icon"> | ||||
| 				<Icon iconName="CheckMark" style={{ | ||||
| 					display: item.mark ? "block" : "none" | ||||
| 				}}/> | ||||
| 			</div> | ||||
| 			<div className="list-item-name"> | ||||
| 				<Localization i18nKey={item.nameKey}/> | ||||
| 			</div> | ||||
| 		</div>; | ||||
|     } | ||||
| 
 | ||||
| 	public render(): ReactNode { | ||||
| 		return <Callout | ||||
| 			onDismiss={this.props.dismiss} | ||||
| @ -75,8 +104,19 @@ class PickerList extends Component<IPickerListProps> { | ||||
| 				{this.props.objectList ? this.props.objectList.map((item) => { | ||||
| 					return this.renderItem(item); | ||||
| 				}) : null} | ||||
| 				{!this.props.objectList || (this.props.objectList && this.props.objectList.length <= 0) ? | ||||
| 					<Localization className="picker-list-nodata" i18nKey="Common.Attr.Key.Label.Picker.Nodata"/> | ||||
|                 {this.props.displayItems ? this.props.displayItems.map((item) => { | ||||
| 					return this.renderString(item); | ||||
| 				}) : null} | ||||
| 				{ | ||||
|                     !(this.props.objectList || this.props.displayItems) ||  | ||||
|                     !( | ||||
|                         this.props.objectList && this.props.objectList.length > 0 ||  | ||||
|                         this.props.displayItems && this.props.displayItems.length > 0 | ||||
|                     ) ? | ||||
|                         <Localization | ||||
|                             className="picker-list-nodata" | ||||
|                             i18nKey={this.props.noData ?? "Common.No.Data"} | ||||
|                         /> | ||||
|                         : null | ||||
| 				} | ||||
| 			</div> | ||||
| @ -84,4 +124,4 @@ class PickerList extends Component<IPickerListProps> { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { PickerList } | ||||
| export { PickerList, IDisplayItem } | ||||
| @ -42,6 +42,7 @@ const EN_US = { | ||||
|     "Panel.Info.Label.Details.View": "Edit view label attributes", | ||||
|     "Panel.Title.Group.Details.View": "Group", | ||||
|     "Panel.Info.Group.Details.View": "Edit view group attributes", | ||||
|     "Common.No.Data": "No Data", | ||||
|     "Common.Attr.Title.Basic": "Basic properties", | ||||
|     "Common.Attr.Title.Spatial": "Spatial property", | ||||
|     "Common.Attr.Title.Individual.Generation": "Individual generation", | ||||
| @ -57,9 +58,13 @@ const EN_US = { | ||||
|     "Common.Attr.Key.Update": "Update", | ||||
|     "Common.Attr.Key.Delete": "Delete", | ||||
|     "Common.Attr.Key.Label": "Label", | ||||
|     "Common.Attr.Key.Size": "Size", | ||||
|     "Common.Attr.Key.Error.Multiple": "Multiple values", | ||||
|     "Common.Attr.Key.Label.Picker.Nodata": "No tags can be added", | ||||
|     "Common.Attr.Key.Generation": "Generation", | ||||
|     "Common.Attr.Key.Generation.Mod": "Generation model", | ||||
|     "Common.Attr.Key.Generation.Mod.Point": "Point model", | ||||
|     "Common.Attr.Key.Generation.Mod.Range": "Range model", | ||||
|     "Panel.Info.Range.Details.Attr.Error.Not.Range": "Object is not a Range", | ||||
|     "Panel.Info.Range.Details.Attr.Error.Unspecified": "Unspecified range object", | ||||
|     "Panel.Info.Group.Details.Attr.Error.Not.Group": "Object is not a Group", | ||||
|  | ||||
| @ -42,6 +42,7 @@ const ZH_CN = { | ||||
|     "Panel.Info.Label.Details.View": "编辑查看标签属性", | ||||
|     "Panel.Title.Group.Details.View": "群", | ||||
|     "Panel.Info.Group.Details.View": "编辑查看群属性", | ||||
|     "Common.No.Data": "暂无数据", | ||||
|     "Common.Attr.Title.Basic": "基础属性", | ||||
|     "Common.Attr.Title.Spatial": "空间属性", | ||||
|     "Common.Attr.Title.Individual.Generation": "个体生成", | ||||
| @ -57,9 +58,13 @@ const ZH_CN = { | ||||
|     "Common.Attr.Key.Update": "更新", | ||||
|     "Common.Attr.Key.Delete": "删除", | ||||
|     "Common.Attr.Key.Label": "标签", | ||||
|     "Common.Attr.Key.Size": "大小", | ||||
|     "Common.Attr.Key.Error.Multiple": "多重数值", | ||||
|     "Common.Attr.Key.Label.Picker.Nodata": "没有可以被添加的标签", | ||||
|     "Common.Attr.Key.Generation": "生成", | ||||
|     "Common.Attr.Key.Generation.Mod": "生成模式", | ||||
|     "Common.Attr.Key.Generation.Mod.Point": "点生成", | ||||
|     "Common.Attr.Key.Generation.Mod.Range": "范围生成", | ||||
|     "Panel.Info.Range.Details.Attr.Error.Not.Range": "对象不是一个范围", | ||||
|     "Panel.Info.Range.Details.Attr.Error.Unspecified": "未指定范围对象", | ||||
|     "Panel.Info.Group.Details.Attr.Error.Not.Group": "对象不是一个群", | ||||
|  | ||||
| @ -3,6 +3,11 @@ import { CtrlObject } from "./CtrlObject"; | ||||
| import type { Behavior } from "./Behavior";  | ||||
| import type { Model } from "./Model"; | ||||
| 
 | ||||
| enum GenMod { | ||||
|     Point = "p", | ||||
|     Range = "R" | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 群体类型 | ||||
|  */ | ||||
| @ -13,6 +18,11 @@ class Group extends CtrlObject { | ||||
| 	 */ | ||||
| 	public individuals: Set<Individual> = new Set(); | ||||
| 
 | ||||
|     /** | ||||
|      * 个体生成方式 | ||||
|      */ | ||||
|     public genMethod: GenMod = GenMod.Point; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 创建个体 | ||||
|      * @param count 创建数量 | ||||
| @ -143,4 +153,4 @@ class Group extends CtrlObject { | ||||
| } | ||||
| 
 | ||||
| export default Group; | ||||
| export { Group }; | ||||
| export { Group, GenMod }; | ||||
| @ -6,12 +6,22 @@ import { ObjectID } from "@Model/Renderer"; | ||||
| import { ColorInput } from "@Component/ColorInput/ColorInput"; | ||||
| import { TogglesInput } from "@Component/TogglesInput/TogglesInput"; | ||||
| import { LabelPicker } from "@Component/LabelPicker/LabelPicker"; | ||||
| import { Group } from "@Model/Group"; | ||||
| import { Group, GenMod } from "@Model/Group"; | ||||
| import { AllI18nKeys } from "@Component/Localization/Localization"; | ||||
| import { ComboInput, IDisplayItem } from "@Component/ComboInput/ComboInput"; | ||||
| import "./GroupDetails.scss"; | ||||
| 
 | ||||
| interface IGroupDetailsProps {} | ||||
| 
 | ||||
| const mapGenModToI18nKey = new Map<GenMod, AllI18nKeys>(); | ||||
| mapGenModToI18nKey.set(GenMod.Point, "Common.Attr.Key.Generation.Mod.Point"); | ||||
| mapGenModToI18nKey.set(GenMod.Range, "Common.Attr.Key.Generation.Mod.Range"); | ||||
| 
 | ||||
| const allOption: IDisplayItem[] = [ | ||||
|     {nameKey: "Common.Attr.Key.Generation.Mod.Point", key: GenMod.Point}, | ||||
|     {nameKey: "Common.Attr.Key.Generation.Mod.Range", key: GenMod.Range} | ||||
| ]; | ||||
| 
 | ||||
| @useStatusWithEvent("groupAttrChange", "groupLabelChange", "focusObjectChange") | ||||
| class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> { | ||||
| 
 | ||||
| @ -61,6 +71,13 @@ class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> { | ||||
|                 }} | ||||
|             /> | ||||
| 
 | ||||
|             {this.renderAttrInput( | ||||
|                 group.id, "Common.Attr.Key.Size", group.size, | ||||
|                 (val, status) => { | ||||
|                     status.changeGroupAttrib(group.id, "size", (val as any) / 1); | ||||
|                 }, 10, undefined, 0 | ||||
|             )} | ||||
| 
 | ||||
|             <LabelPicker | ||||
|                 keyI18n="Common.Attr.Key.Label" | ||||
|                 labels={group.allLabels()} | ||||
| @ -104,6 +121,22 @@ class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> { | ||||
| 					} | ||||
| 				}} | ||||
| 			/> | ||||
| 
 | ||||
|             <Message i18nKey="Common.Attr.Title.Individual.Generation" isTitle/> | ||||
| 
 | ||||
|             <ComboInput | ||||
|                 keyI18n="Common.Attr.Key.Generation.Mod" | ||||
|                 value={{ | ||||
|                     nameKey: mapGenModToI18nKey.get(group.genMethod) ?? "Common.No.Data", | ||||
|                     key: group.genMethod | ||||
|                 }} | ||||
|                 allOption={allOption} | ||||
|                 valueChange={(value) => { | ||||
|                     if (this.props.status) { | ||||
|                         this.props.status.changeGroupAttrib(group.id, "genMethod", value.key as any); | ||||
|                     } | ||||
|                 }} | ||||
|             /> | ||||
| 		</> | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user