Detach form components to a separate directory

This commit is contained in:
2022-04-08 15:24:50 +08:00
parent 7556ea983e
commit 47d097e94a
61 changed files with 169 additions and 168 deletions
+60
View File
@@ -0,0 +1,60 @@
@import "../../Component/Theme/Theme.scss";
$line-min-height: 24px;
div.attr-input-root {
display: flex;
justify-content: space-between;
align-items: center;
input {
display: block;
width: 100%;
height: 100%;
border: none;
outline: none;
background-color: transparent;
min-height: $line-min-height;
};
input:focus {
border: none;
background-color: transparent;
}
div.button-left, div.button-right {
min-height: $line-min-height;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
padding: 0 3px;
}
}
div.dark.text-field-root {
input {
background-color: $lt-bg-color-lvl3-dark;
color: $lt-font-color-normal-dark;
}
div.button-left:hover, div.button-right:hover {
background-color: $lt-bg-color-lvl2-dark;
}
}
div.light.text-field-root {
input {
background-color: $lt-bg-color-lvl3-light;
color: $lt-font-color-normal-light;
}
div.button-left:hover, div.button-right:hover {
background-color: $lt-bg-color-lvl2-light;
}
}
+183
View File
@@ -0,0 +1,183 @@
import { Component, ReactNode } from "react";
import { Icon } from "@fluentui/react";
import { AllI18nKeys } from "@Component/Localization/Localization";
import { ObjectID } from "@Model/Renderer";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import "./AttrInput.scss";
interface IAttrInputProps extends ITextFieldProps {
id?: ObjectID;
value?: number | string;
isNumber?: boolean;
maxLength?: number;
minLength?: number;
max?: number;
min?: number;
step?: number;
valueChange?: (value: this["isNumber"] extends true ? number : string) => any;
}
class AttrInput extends Component<IAttrInputProps> {
private value: string = "";
private error?: AllI18nKeys;
private errorOption?: Record<string, string>;
private numberTestReg = [/\.0*$/, /\.\d*[1-9]+0+$/];
private numberTester(value: string) {
return isNaN((value as any) / 1) ||
this.numberTestReg[0].test(value) ||
this.numberTestReg[1].test(value);
}
private check(value: string): AllI18nKeys | undefined {
// 长度校验
const maxLength = this.props.maxLength ?? 32;
if (value.length > maxLength) {
this.error = "Input.Error.Length";
this.errorOption = { num: maxLength.toString() };
return this.error;
}
const minLength = this.props.minLength ?? 1;
if (value.length < minLength) {
this.error = "Input.Error.Length.Less";
this.errorOption = { num: minLength.toString() };
return this.error;
}
if (this.props.isNumber) {
const praseNumber = (value as any) / 1;
// 数字校验
if (this.numberTester(value)) {
this.error = "Input.Error.Not.Number";
return this.error;
}
// 最大值校验
if (this.props.max !== undefined && praseNumber > this.props.max) {
this.error = "Input.Error.Max";
this.errorOption = { num: this.props.max.toString() };
return this.error;
}
// 最小值校验
if (this.props.min !== undefined && praseNumber < this.props.min) {
this.error = "Input.Error.Min";
this.errorOption = { num: this.props.min.toString() };
return this.error;
}
}
return undefined;
}
private handelValueChange = () => {
if (!this.error && this.props.valueChange) {
if (this.props.isNumber) {
let numberVal = (this.value as any) * 10000;
this.value = (Math.round(numberVal) / 10000).toString();
}
this.props.valueChange(this.value);
}
this.forceUpdate();
}
private changeValue = (direction: number) => {
if (this.error) {
return;
} else {
let newVal = (this.value as any / 1) + (this.props.step ?? 1) * direction;
// 最大值校验
if (this.props.max !== undefined && newVal > this.props.max) {
newVal = this.props.max;
}
// 最小值校验
if (this.props.min !== undefined && newVal < this.props.min) {
newVal = this.props.min;
}
this.value = newVal.toString();
this.handelValueChange()
}
}
private renderInput() {
return <>
{
this.props.isNumber ? <div
className="button-left"
onClick={() => this.changeValue(-1)}
>
<Icon iconName="ChevronLeft"></Icon>
</div> : null
}
<input
className="input"
value={this.value}
style={{
padding: this.props.isNumber ? "0 3px" : "0 8px"
}}
onChange={(e) => {
this.value = e.target.value;
this.error = this.check(e.target.value);
this.handelValueChange();
}}
></input>
{
this.props.isNumber ? <div
className="button-right"
onClick={() => this.changeValue(1)}
>
<Icon iconName="ChevronRight"></Icon>
</div> : null
}
</>
}
public shouldComponentUpdate(nextProps: IAttrInputProps) {
// ID 都为空时更新
if (!nextProps.id && !this.props.id) {
this.updateValueFromProps(nextProps.value);
}
// ID 变换时更新 State 到最新的 Props
if (nextProps.id !== this.props.id) {
this.updateValueFromProps(nextProps.value);
}
return true;
}
public constructor(props: IAttrInputProps) {
super(props);
this.updateValueFromProps(props.value);
}
private updateValueFromProps(val: IAttrInputProps["value"]) {
const value = val ?? (this.props.isNumber ? "0" : "");
this.value = value.toString();
this.error = this.check(value.toString());
}
public render(): ReactNode {
return <TextField
{...this.props}
className="attr-input-root"
customHoverStyle
errorI18n={this.error}
errorI18nOption={this.errorOption}
>
{
this.renderInput()
}
</TextField>;
}
}
export { AttrInput };
@@ -0,0 +1,101 @@
@import "../../Component/Theme/Theme.scss";
div.behavior-picker-list {
width: 100%;
max-width: 400px;
padding: 8px 0;
div.behavior-picker-line {
width: 100%;
padding: 0 !important;
display: flex !important;
justify-content: flex-start !important;
div.behavior-picker-line-color-view {
width: 0;
max-width: 0;
height: 100%;
div {
width: 0;
height: 0;
border-bottom: 10px solid transparent;
position: relative;
z-index: 2;
}
}
div.behavior-picker-line-icon-view {
width: 30px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
i.behavior-icon {
display: inline-block;
}
i.view-icon {
display: none;
}
}
div.behavior-picker-line-icon-view:hover {
i.view-icon {
color: $lt-green;
}
}
div.behavior-picker-title {
height: 100%;
width: calc(100% - 60px);
display: flex;
align-items: center;
div {
text-overflow: ellipsis;
overflow: hidden;
}
}
div.behavior-picker-title.is-deleted {
div {
text-decoration: line-through;
opacity: 0.6;
}
}
div.behavior-picker-title.behavior-add-line {
width: calc(100% - 30px);
}
div.behavior-picker-line-delete-view {
width: 30px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
div.behavior-picker-line-delete-view:hover i {
color: $lt-red;
}
}
div.behavior-picker-line:hover {
div.behavior-picker-line-icon-view {
i.behavior-icon {
display: none;
}
i.view-icon {
display: inline-block;
}
}
}
}
@@ -0,0 +1,190 @@
import { Component, ReactNode, createRef } from "react";
import { Icon } from "@fluentui/react";
import { Behavior } from "@Model/Behavior";
import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { DetailsList } from "@Component/DetailsList/DetailsList";
import { Localization } from "@Component/Localization/Localization";
import { PickerList } from "@Input/PickerList/PickerList";
import "./BehaviorPicker.scss";
interface IBehaviorPickerProps {
behavior: Behavior[];
focusBehavior?: Behavior;
click?: (behavior: Behavior) => void;
delete?: (behavior: Behavior) => void;
action?: (behavior: Behavior) => void;
add?: (behavior: Behavior) => void;
}
interface IBehaviorPickerState {
isPickerListOpen: boolean;
}
@useStatusWithEvent("behaviorChange")
@useSettingWithEvent("language")
class BehaviorPicker extends Component<IBehaviorPickerProps & IMixinSettingProps & IMixinStatusProps> {
public state = {
isPickerListOpen: false
}
private isInnerClick: boolean = false;
private clickLineRef = createRef<HTMLDivElement>();
private getData() {
let data: Array<{select: boolean, key: string, behavior: Behavior | undefined}> = [];
for (let i = 0; i < this.props.behavior.length; i++) {
data.push({
key: this.props.behavior[i].id,
behavior: this.props.behavior[i],
select: this.props.behavior[i].id === this.props.focusBehavior?.id
})
}
data.push({
key: "@@AddButton_List_Key",
behavior: undefined,
select: false
})
return data;
}
private renderLine = (behavior?: Behavior): ReactNode => {
if (behavior) {
const titleClassList: string[] = ["behavior-picker-title"];
if (this.props.setting) {
titleClassList.push(this.props.setting.language);
}
if (behavior.isDeleted()) {
titleClassList.push("is-deleted");
}
return <>
<div className="behavior-picker-line-color-view">
<div style={{ borderLeft: `10px solid rgb(${behavior.color.join(",")})` }}/>
</div>
<div
className="behavior-picker-line-icon-view"
onClick={() => {
this.isInnerClick = true;
this.props.action && this.props.action(behavior);
}}
>
{
behavior.isDeleted() ?
<Icon iconName={behavior.iconName}/>:
<>
<Icon iconName={behavior.iconName} className="behavior-icon"/>
<Icon iconName="EditCreate" className="view-icon"/>
</>
}
</div>
<div className={titleClassList.join(" ")}>
<div>{behavior.name}</div>
</div>
<div
className="behavior-picker-line-delete-view"
onClick={() => {
this.isInnerClick = true;
this.props.delete && this.props.delete(behavior);
}}
>
<Icon iconName="Delete"/>
</div>
</>;
} else {
const openPicker = () => {
this.isInnerClick = true;
this.setState({
isPickerListOpen: true
});
}
return <>
<div
className="behavior-picker-line-icon-view"
onClick={openPicker}
>
<Icon iconName="Add" className="add-icon"/>
</div>
<div
className="behavior-picker-title behavior-add-line"
onClick={openPicker}
ref={this.clickLineRef}
>
<Localization i18nKey="Behavior.Picker.Add.Button"/>
</div>
</>;
}
}
private getAllData = (): Behavior[] => {
if (this.props.status) {
let res: Behavior[] = [];
for (let i = 0; i < this.props.status.model.behaviorPool.length; i++) {
let isAdded = false;
for (let j = 0; j < this.props.behavior.length; j++) {
if (this.props.status.model.behaviorPool[i].id === this.props.behavior[j].id) {
isAdded = true;
break;
}
}
if (!isAdded) {
res.push(this.props.status.model.behaviorPool[i]);
}
}
return res;
} else {
return [];
}
}
private renderPickerList(): ReactNode {
return <PickerList
objectList={this.getAllData()}
noData="Behavior.Picker.Add.Nodata"
target={this.clickLineRef}
clickObjectItems={((item) => {
if (item instanceof Behavior && this.props.add) {
this.props.add(item);
}
this.setState({
isPickerListOpen: false
})
})}
dismiss={() => {
this.setState({
isPickerListOpen: false
});
}}
/>
}
public render(): ReactNode {
return <>
<DetailsList
hideCheckBox
className="behavior-picker-list"
items={this.getData()}
clickLine={(item) => {
if (!this.isInnerClick && this.props.click && item.behavior) {
this.props.click(item.behavior);
}
this.isInnerClick = false;
}}
columns={[{
className: "behavior-picker-line",
key: "behavior",
render: this.renderLine
}]}
/>
{this.state.isPickerListOpen ? this.renderPickerList() : null}
</>
}
}
export { BehaviorPicker };
+40
View File
@@ -0,0 +1,40 @@
$line-min-height: 24px;
div.color-input {
div.color-view {
width: $line-min-height;
min-width: $line-min-height;
display: flex;
justify-content: center;
align-items: center;
div.color-box {
width: 12px;
height: 12px;
border-radius: 3px;
}
}
div.value-view {
width: 100%;
height: 100%;
min-height: $line-min-height;
display: flex;
align-items: center;
div.text-box {
max-width: calc( 100% - 24px);
text-overflow: ellipsis;
overflow: hidden;
padding-left: 1px;
white-space: nowrap;
word-break: keep-all;
}
}
}
div.color-picker-root {
width: 300px;
height: 340px;
}
+110
View File
@@ -0,0 +1,110 @@
import { Component, createRef, ReactNode } from "react";
import { Callout, ColorPicker, DirectionalHint } from "@fluentui/react";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import "./ColorInput.scss";
interface IColorInputProps extends ITextFieldProps {
value?: number[];
normal?: boolean;
valueChange?: (color: number[]) => any;
}
interface IColorInputState {
isPickerVisible: boolean;
}
class ColorInput extends Component<IColorInputProps, IColorInputState> {
public constructor(props: IColorInputProps) {
super(props);
this.state = {
isPickerVisible: false
}
}
private pickerTarget = createRef<HTMLDivElement>();
private renderPicker() {
return <Callout
target={this.pickerTarget}
directionalHint={DirectionalHint.topAutoEdge}
onDismiss={() => {
this.setState({
isPickerVisible: false
})
}}
>
<div className="color-picker-root">
<ColorPicker
color={this.getColorString()}
alphaType={"none"}
onChange={(_, color) => {
if (this.props.valueChange) {
if (this.props.normal) {
this.props.valueChange([
color.r / 255,
color.g / 255,
color.b / 255,
])
} else {
this.props.valueChange([
color.r,
color.g,
color.b,
])
}
}
}}
/>
</div>
</Callout>
}
private renderColorInput() {
return <>
<div className="color-view">
<div className="color-box" style={{
backgroundColor: this.getColorString()
}}/>
</div>
<div className="value-view">
<div className="text-box">{this.getColorString(true)}</div>
</div>
</>;
}
private getColorString(display?: boolean) {
let color: number[] = this.props.value?.concat([]) ?? [0, 0, 0];
if (this.props.normal) {
color[0] = Math.round(color[0] * 255);
color[1] = Math.round(color[1] * 255);
color[2] = Math.round(color[2] * 255);
}
if (display) {
return `rgb ( ${color[0] ?? 0}, ${color[1] ?? 0}, ${color[2] ?? 0} )`;
} else {
return `rgb(${color[0] ?? 0},${color[1] ?? 0},${color[2] ?? 0})`;
}
}
public render(): ReactNode {
return <>
<TextField
{...this.props}
className="color-input"
keyI18n={this.props.keyI18n}
targetRef={this.pickerTarget}
onClick={() => {
this.setState({
isPickerVisible: !this.props.disableI18n
})
}}
>
{ this.renderColorInput() }
</TextField>
{this.state.isPickerVisible ? this.renderPicker(): null}
</>;
}
}
export { ColorInput };
+38
View File
@@ -0,0 +1,38 @@
@import "../../Component/Theme/Theme.scss";
$line-min-height: 24px;
div.combo-input {
div.value-view {
width: 100%;
height: 100%;
min-height: $line-min-height;
max-width: calc( 100% - 32px );
display: flex;
align-items: center;
padding-left: 8px;
white-space: nowrap;
word-break: keep-all;
text-overflow: ellipsis;
overflow: hidden;
span {
word-break: keep-all;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
max-width: 100%;
}
}
div.list-button {
width: $line-min-height;
height: $line-min-height;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
+81
View File
@@ -0,0 +1,81 @@
import { Component, createRef, ReactNode } from "react";
import { Icon } from "@fluentui/react";
import { PickerList, IDisplayItem } from "@Input/PickerList/PickerList";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import { Localization } from "@Component/Localization/Localization";
import "./ComboInput.scss";
interface IComboInputProps extends ITextFieldProps {
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 <>
<TextField
{...this.props}
targetRef={this.pickerTarget}
className="combo-input"
keyI18n={this.props.keyI18n}
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>
</TextField>
{this.state.isPickerVisible ? this.renderPicker(): null}
</>
}
}
export { ComboInput, IDisplayItem };
@@ -0,0 +1,7 @@
@import "../../Component/Theme/Theme.scss";
$line-min-height: 26px;
div.label-picker {
min-height: $line-min-height;
}
+95
View File
@@ -0,0 +1,95 @@
import { Component, ReactNode, createRef } from "react";
import { Label } from "@Model/Label";
import { PickerList } from "@Input/PickerList/PickerList";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { LabelList } from "@Component/LabelList/LabelList";
import "./LabelPicker.scss"
interface ILabelPickerProps extends ITextFieldProps {
labels: Label[];
labelAdd?: (label: Label) => any;
labelDelete?: (label: Label) => any;
}
interface ILabelPickerState {
isPickerVisible: boolean;
}
@useStatusWithEvent("labelAttrChange", "labelChange")
class LabelPicker extends Component<ILabelPickerProps & IMixinStatusProps, ILabelPickerState> {
public constructor(props: ILabelPickerProps) {
super(props);
this.state = {
isPickerVisible: false
}
}
private addButtonRef = createRef<HTMLDivElement>();
private getOtherLabel() {
let res: Label[] = [];
let nowLabel: Label[] = this.props.labels ?? [];
if (this.props.status) {
this.props.status.model.labelPool.forEach((aLabel) => {
let isHas = false;
nowLabel.forEach((nLabel) => {
if (aLabel.equal(nLabel)) isHas = true;
})
if (!isHas) {
res.push(aLabel);
}
})
}
return res;
}
private renderPicker() {
return <PickerList
noData="Common.Attr.Key.Label.Picker.Nodata"
objectList={this.getOtherLabel()}
dismiss={() => {
this.setState({
isPickerVisible: false
});
}}
clickObjectItems={(label) => {
if (label instanceof Label && this.props.labelAdd) {
this.props.labelAdd(label)
}
this.setState({
isPickerVisible: false
});
}}
target={this.addButtonRef}
/>;
}
public render(): ReactNode {
return <TextField
{...this.props}
className="label-picker"
customHoverStyle
customStyle
keyI18n={this.props.keyI18n}
>
<LabelList
addRef={this.addButtonRef}
labels={this.props.labels}
minHeight={26}
deleteLabel={(label) => {
this.props.labelDelete ? this.props.labelDelete(label) : 0;
}}
addLabel={() => {
this.setState({
isPickerVisible: true
});
}}
/>
{this.state.isPickerVisible ? this.renderPicker(): null}
</TextField>;
}
}
export { LabelPicker }
+20
View File
@@ -0,0 +1,20 @@
div.panel-error-message {
transition: none !important;
padding-top: 5px;
padding-bottom: 5px;
min-height: 26px;
display: flex;
align-items: center;
span {
display: block;
}
}
div.panel-error-message.title {
padding: 15px 0 5px 0;
}
div.panel-error-message.title.first {
padding: 5px 0 5px 0;
}
+51
View File
@@ -0,0 +1,51 @@
import { FunctionComponent } from "react";
import { AllI18nKeys, I18N } from "@Component/Localization/Localization";
import { useSettingWithEvent, IMixinSettingProps, Themes, Language } from "@Context/Setting";
import "./Message.scss";
interface IMessageProps {
i18nKey?: AllI18nKeys;
text?: string;
options?: Record<string, string>;
className?: string;
isTitle?: boolean;
first?: boolean;
}
const MessageView: FunctionComponent<IMessageProps & IMixinSettingProps> = (props) => {
let theme = props.setting ? props.setting.themes : Themes.dark;
let language: Language = props.setting ? props.setting.language : "EN_US";
let classList: string[] = [
"panel-error-message",
theme === Themes.dark ? "dark" : "light",
];
if (props.first) {
classList.push("first");
}
if (props.isTitle) {
classList.push("title");
classList.push("font-lvl3");
}
if (props.className) {
classList.push(props.className);
}
return <div className={classList.join(" ")}>
{
props.text ?
<span className={language}>{props.text}</span> :
props.i18nKey ?
<span className={language}>{
I18N(language, props.i18nKey, props.options)
}</span> :
null
}
</div>
}
const Message = useSettingWithEvent("language", "themes")(MessageView);
export { Message };
@@ -0,0 +1,58 @@
@import "../../Component/Theme/Theme.scss";
@import "../PickerList/RainbowBg.scss";
$line-min-height: 24px;
div.object-picker {
div.list-color {
width: 3px;
height: $line-min-height;
border-radius: 1000px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
div.value-view {
flex-shrink: 1;
width: 100%;
height: 100%;
max-width: calc( 100% - 51px );
min-height: $line-min-height;
display: flex;
align-items: center;
span {
word-break: keep-all;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
max-width: 100%;
}
}
div.list-button {
width: $line-min-height;
height: $line-min-height;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
div.list-empty {
width: $line-min-height;
height: $line-min-height;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
div.list-empty:hover {
color: $lt-red;
}
}
+169
View File
@@ -0,0 +1,169 @@
import { Component, createRef, ReactNode } from "react";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { Label } from "@Model/Label";
import { Group } from "@Model/Group";
import { Range } from "@Model/Range";
import { CtrlObject } from "@Model/CtrlObject";
import { Behavior } from "@Model/Behavior";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import { PickerList, IDisplayItem, getObjectDisplayInfo, IDisplayInfo } from "@Input/PickerList/PickerList";
import { Localization } from "@Component/Localization/Localization";
import { Icon } from "@fluentui/react";
import "./ObjectPicker.scss";
type IObjectType = Label | Group | Range | CtrlObject;
interface IObjectPickerProps extends ITextFieldProps {
type: string;
value?: IObjectType;
valueChange?: (value: IObjectType) => any;
cleanValue?: () => any;
}
interface IObjectPickerState {
isPickerVisible: boolean;
}
@useStatusWithEvent("objectChange", "labelChange")
class ObjectPicker extends Component<IObjectPickerProps & IMixinStatusProps, IObjectPickerState> {
private getAllOption() {
let option: Array<IObjectType> = [];
if (!this.props.status) return option;
if (this.props.type.includes("L")) {
for (let j = 0; j < this.props.status.model.labelPool.length; j++) {
option.push(this.props.status.model.labelPool[j]);
}
if (this.props.type.includes("R")) {
option.push(this.props.status.model.allRangeLabel);
}
if (this.props.type.includes("G")) {
option.push(this.props.status.model.allGroupLabel);
}
}
if (this.props.type.includes("R")) {
for (let j = 0; j < this.props.status.model.objectPool.length; j++) {
if (this.props.status.model.objectPool[j] instanceof Range) {
option.push(this.props.status.model.objectPool[j]);
}
}
}
if (this.props.type.includes("G")) {
for (let j = 0; j < this.props.status.model.objectPool.length; j++) {
if (this.props.status.model.objectPool[j] instanceof Group) {
option.push(this.props.status.model.objectPool[j]);
}
}
}
return option;
}
private isClean: boolean = false;
public constructor(props: IObjectPickerProps) {
super(props);
this.state = {
isPickerVisible: false
}
}
private pickerTarget = createRef<HTMLDivElement>();
private renderPicker() {
return <PickerList
noData="Object.Picker.List.No.Data"
target={this.pickerTarget}
objectList={this.getAllOption()}
clickObjectItems={((item) => {
if (item instanceof Behavior) return;
if (this.props.valueChange) {
this.props.valueChange(item);
}
this.setState({
isPickerVisible: false
})
})}
dismiss={() => {
this.setState({
isPickerVisible: false
})
}}
/>
}
public render(): ReactNode {
let disPlayInfo: IDisplayInfo = getObjectDisplayInfo(this.props.value);
let isDelete: boolean = !!this.props.value?.isDeleted();
return <>
<TextField
{...this.props}
className="object-picker"
keyI18n={this.props.keyI18n}
targetRef={this.pickerTarget}
onClick={() => {
if (this.isClean) {
this.isClean = false;
} else {
this.setState({
isPickerVisible: true
})
}
}}
>
<div
className={
"list-color" + (
disPlayInfo.allLabel ? " rainbow-back-ground-color" : ""
)
}
style={{
backgroundColor: disPlayInfo.color
}}
/>
<div className="list-button">
<Icon iconName={disPlayInfo.icon}/>
</div>
<div
className="value-view"
style={{
textDecoration: isDelete ? "line-through" : undefined,
opacity: isDelete ? ".6" : undefined
}}
>
{
disPlayInfo.internal ?
<Localization i18nKey={disPlayInfo.name as any}/> :
<span>{disPlayInfo.name}</span>
}
</div>
<div
className="list-empty"
onClick={() => {
if (this.props.cleanValue && disPlayInfo.name) {
this.isClean = true;
this.props.cleanValue();
}
}}
>
{
disPlayInfo.name ?
<Icon iconName="delete"/> :
null
}
</div>
</TextField>
{this.state.isPickerVisible ? this.renderPicker(): null}
</>
}
}
export { ObjectPicker, IDisplayItem };
+202
View File
@@ -0,0 +1,202 @@
import { Component, Fragment, ReactNode } from "react";
import { useSettingWithEvent, IMixinSettingProps, Language } from "@Context/Setting";
import { AttrInput } from "@Input/AttrInput/AttrInput";
import { ObjectID } from "@Model/Renderer";
import { TogglesInput } from "@Input/TogglesInput/TogglesInput";
import { ObjectPicker } from "@Input/ObjectPicker/ObjectPicker";
import { AllI18nKeys } from "@Component/Localization/Localization";
import { Message } from "@Input/Message/Message";
import {
IParameter, IParameterOption, IParameterOptionItem,
IParameterValue, IParamValue, isObjectType, isVectorType
} from "@Model/Parameter";
import "./Parameter.scss";
interface IParameterProps<P extends IParameter = {}> {
option: IParameterOption<P>;
value: IParameterValue<P>;
key: ObjectID;
change: <K extends keyof P>(key: K, val: IParamValue<P[K]>) => any;
i18n: <K extends keyof P>(option: IParameterOptionItem<P[K]>, language: Language) => string;
title?: AllI18nKeys;
titleOption?: Record<string, string>;
isFirst?: boolean;
}
@useSettingWithEvent("language")
class Parameter<P extends IParameter> extends Component<IParameterProps<P> & IMixinSettingProps> {
private renderParameter<K extends keyof P>
(key: K, option: IParameterOptionItem<P[K]>, value: IParamValue<P[K]>): ReactNode {
const indexKey = `${this.props.key}-${key}`;
const type = option.type;
const i18nString = this.props.i18n(option, this.props.setting?.language ?? "EN_US");
if (type === "number") {
return <AttrInput
key={indexKey}
id={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key"
keyI18nOption={{ key: i18nString }}
isNumber={true}
step={option.numberStep}
maxLength={option.maxLength}
max={option.numberMax}
min={option.numberMin}
value={value as IParamValue<"number"> ?? 0}
valueChange={(val) => {
this.props.change(key, parseFloat(val) as IParamValue<P[K]>);
}}
/>;
}
else if (type === "string") {
return <AttrInput
key={indexKey}
id={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key"
keyI18nOption={{ key: i18nString }}
maxLength={option.maxLength}
value={value as IParamValue<"string"> ?? ""}
valueChange={(val) => {
this.props.change(key, val as IParamValue<P[K]>);
}}
/>;
}
else if (type === "boolean") {
return <TogglesInput
key={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key"
keyI18nOption={{ key: i18nString }}
onIconName={option.iconName}
value={value as IParamValue<"boolean"> ?? false}
valueChange={(val) => {
this.props.change(key, val as IParamValue<P[K]>);
}}
/>
}
else if (isObjectType(type)) {
type IObjectParamValue = IParamValue<"G" | "R" | "LG" | "LR">;
const typedValue = value as IObjectParamValue;
return <ObjectPicker
key={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key"
keyI18nOption={{ key: i18nString }}
type={type}
value={typedValue.picker}
valueChange={(obj) => {
typedValue.picker = obj as IObjectParamValue["picker"];
this.props.change(key, typedValue as IParamValue<P[K]>);
}}
cleanValue={() => {
typedValue.picker = undefined as IObjectParamValue["picker"];
this.props.change(key, typedValue as IParamValue<P[K]>);
}}
/>
}
else if (isVectorType(type)) {
type IObjectParamValue = IParamValue<"vec">;
const typedValue = value as IObjectParamValue;
return <Fragment key={indexKey}>
<AttrInput
key={`${indexKey}-X`}
id={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key.Vec.X"
keyI18nOption={{ key: i18nString }}
isNumber={true}
step={option.numberStep}
maxLength={option.maxLength}
max={option.numberMax}
min={option.numberMin}
value={typedValue[0] ?? 0}
valueChange={(val) => {
typedValue[0] = parseFloat(val);
this.props.change(key, typedValue as IParamValue<P[K]>);
}}
/>
<AttrInput
key={`${indexKey}-Y`}
id={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key.Vec.Y"
keyI18nOption={{ key: i18nString }}
isNumber={true}
step={option.numberStep}
maxLength={option.maxLength}
max={option.numberMax}
min={option.numberMin}
value={typedValue[1] ?? 0}
valueChange={(val) => {
typedValue[1] = parseFloat(val);
this.props.change(key, typedValue as IParamValue<P[K]>);
}}
/>
<AttrInput
key={`${indexKey}-Z`}
id={indexKey}
keyI18n="Panel.Info.Behavior.Details.Parameter.Key.Vec.Z"
keyI18nOption={{ key: i18nString }}
isNumber={true}
step={option.numberStep}
maxLength={option.maxLength}
max={option.numberMax}
min={option.numberMin}
value={typedValue[2] ?? 0}
valueChange={(val) => {
typedValue[2] = parseFloat(val);
this.props.change(key, typedValue as IParamValue<P[K]>);
}}
/>
</Fragment>
}
else {
return <Fragment key={indexKey}/>
}
}
private renderAllParameter(key: Array<keyof P>) {
return key.map((key) => {
return this.renderParameter(
key,
this.props.option[key],
this.props.value[key],
);
});
}
public render(): ReactNode {
const allOptionKeys: Array<keyof P> = Object.getOwnPropertyNames(this.props.option);
return <>
{
allOptionKeys.length <= 0 && this.props.title ?
<Message
isTitle
first={this.props.isFirst}
i18nKey={this.props.title}
options={this.props.titleOption}
/> : null
}
{
this.renderAllParameter(allOptionKeys)
}
</>
}
}
export { Parameter }
+78
View File
@@ -0,0 +1,78 @@
@import "./RainbowBg.scss";
div.picker-list-root {
min-width: 200px;
height: 100%;
padding: 0px;
margin: 0px;
overflow-y: auto;
div.picker-list-item {
min-width: 200px;
padding-right: 10px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
border-radius: 3px;
overflow: hidden;
div.list-item-color {
width: 3px;
height: calc( 100% - 6px );
margin: 3px 0 3px 3px;
flex-shrink: 0;
border-radius: 1000px;
overflow: hidden;
background-color: black;
}
div.list-item-icon {
width: 30px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
div.list-item-name {
width: 100%;
white-space: nowrap;
box-sizing: border-box;
}
}
div.picker-list-item:hover {
background-color: rgba($color: #000000, $alpha: .1);
}
span.picker-list-nodata {
display: inline-block;
padding: 8px;
}
}
div.picker-list-root::-webkit-scrollbar {
width : 0; /*高宽分别对应横竖滚动条的尺寸*/
height: 0;
}
div.picker-list-root::-webkit-scrollbar {
width : 8px; /*高宽分别对应横竖滚动条的尺寸*/
height: 0;
}
div.picker-list-root::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 8px;
background-color: rgba($color: #000000, $alpha: .2);
}
div.picker-list-root::-webkit-scrollbar-track {
/*滚动条里面轨道*/
border-radius: 8px;
background-color: rgba($color: #000000, $alpha: 0);
}
+197
View File
@@ -0,0 +1,197 @@
import { AllI18nKeys, Localization } from "@Component/Localization/Localization";
import { Callout, DirectionalHint, Icon } from "@fluentui/react";
import { Behavior } from "@Model/Behavior";
import { CtrlObject } from "@Model/CtrlObject";
import { Group } from "@Model/Group";
import { Label } from "@Model/Label";
import { Range } from "@Model/Range";
import { Component, ReactNode, RefObject } from "react";
import "./PickerList.scss";
type IPickerListItem = CtrlObject | Label | Range | Group | Behavior;
interface IDisplayInfo {
color: string;
icon: string;
name: string;
internal: boolean;
allLabel: boolean;
};
interface IDisplayItem {
nameKey: AllI18nKeys;
key: string;
mark?: boolean;
}
function getObjectDisplayInfo(item?: IPickerListItem): IDisplayInfo {
if (!item) {
return {
color: "transparent",
icon: "Label",
name: "Input.Error.Select",
internal: true,
allLabel: false
}
}
let color: number[] | string = [];
let icon: string = "tag";
let name: string = "";
let internal: boolean = false;
let allLabel: boolean = false;
if (item instanceof Range) {
icon = "CubeShape"
}
if (item instanceof Group) {
icon = "WebAppBuilderFragment"
}
if (item instanceof CtrlObject) {
color = [];
color[0] = Math.round(item.color[0] * 255);
color[1] = Math.round(item.color[1] * 255);
color[2] = Math.round(item.color[2] * 255);
name = item.displayName;
}
if (item instanceof Label) {
if (item.isBuildIn) {
internal = true;
allLabel = true;
color = "transparent";
if (item.id === "AllRange") {
icon = "ProductList";
name = "Build.In.Label.Name.All.Range";
} else if (item.id === "AllGroup") {
icon = "SizeLegacy";
name = "Build.In.Label.Name.All.Group";
}
}
else {
icon = "tag";
color = item.color.concat([]);
name = item.name;
}
}
if (item instanceof Behavior) {
color = item.color;
icon = item.iconName;
name = item.name;
internal = false;
allLabel = false;
}
if (Array.isArray(color)) {
color = `rgb(${color[0]},${color[1]},${color[2]})`;
}
return {
color: color,
icon: icon,
name: name,
internal: internal,
allLabel: allLabel
}
}
interface IPickerListProps {
displayItems?: IDisplayItem[];
objectList?: IPickerListItem[];
target?: RefObject<any>;
noData?: AllI18nKeys;
dismiss?: () => any;
clickObjectItems?: (item: IPickerListItem) => any;
clickDisplayItems?: (item: IDisplayItem) => any;
}
class PickerList extends Component<IPickerListProps> {
private renderItem(item: IPickerListItem) {
const displayInfo = getObjectDisplayInfo(item);
return <div
className="picker-list-item"
key={item.id}
onClick={() => {
if (this.props.clickObjectItems) {
this.props.clickObjectItems(item)
}
}}
>
<div className={
"list-item-color" + (
displayInfo.allLabel ? " rainbow-back-ground-color" : ""
)
}
style={{
backgroundColor: displayInfo.color
}}
></div>
<div className="list-item-icon">
<Icon iconName={displayInfo.icon}/>
</div>
<div className="list-item-name">
{
displayInfo.internal ?
<Localization i18nKey={displayInfo.name}/> :
displayInfo.name
}
</div>
</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}
target={this.props.target}
directionalHint={DirectionalHint.topCenter}
>
<div className="picker-list-root">
{this.props.objectList ? this.props.objectList.map((item) => {
return this.renderItem(item);
}) : null}
{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>
</Callout>
}
}
export { PickerList, IDisplayItem, IDisplayInfo, getObjectDisplayInfo }
+13
View File
@@ -0,0 +1,13 @@
div.rainbow-back-ground-color {
background-image: linear-gradient(
to top,
orangered,
orange,
gold,
lightgreen,
cyan,
dodgerblue,
mediumpurple,
hotpink,
orangered);
}
+95
View File
@@ -0,0 +1,95 @@
@import "../../Component/Theme/Theme.scss";
$search-box-height: 26px;
div.search-box-root {
min-height: $search-box-height;
max-width: 280px;
width: 100%;
border-radius: 3px;
display: flex;
cursor: pointer;
overflow: hidden;
div.search-icon {
min-width: $search-box-height;
height: $search-box-height;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
div.input-box {
width: calc(100% - 26px);
height: $search-box-height;
input {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: 0;
outline: none;
background-color: transparent;
vertical-align: middle;
}
}
div.clean-box {
height: $search-box-height;
width: 0;
display: flex;
align-items: center;
div.clean-box-view {
flex-shrink: 0;
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
right: 24px;
border-radius: 3px;
user-select: none;
}
}
}
div.dark.search-box-root {
div.clean-box {
div.clean-box-view:hover {
background-color: $lt-bg-color-lvl2-dark;
}
div.clean-box-view {
background-color: $lt-bg-color-lvl3-dark;
}
}
div.input-box input {
color: $lt-font-color-normal-dark;
}
}
div.light.search-box-root {
div.clean-box {
div.clean-box-view:hover {
background-color: $lt-bg-color-lvl3-light;
}
div.clean-box-view {
background-color: $lt-bg-color-lvl2-light;
}
}
div.input-box input {
color: $lt-font-color-normal-light;
}
}
+60
View File
@@ -0,0 +1,60 @@
import { Component, ReactNode } from "react";
import { Icon } from "@fluentui/react";
import { AllI18nKeys, I18N } from "@Component/Localization/Localization";
import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme";
import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting";
import "./SearchBox.scss";
interface ISearchBoxProps {
value?: string;
valueChange?: (value: string) => void;
placeholderI18N?: AllI18nKeys;
className?: string;
}
@useSettingWithEvent("language")
class SearchBox extends Component<ISearchBoxProps & IMixinSettingProps> {
private renderCleanBox() {
return <div className="clean-box">
<div
className="clean-box-view"
onClick={() => {
if (this.props.valueChange) {
this.props.valueChange("")
}
}}
>
<Icon iconName="CalculatorMultiply"/>
</div>
</div>;
}
public render(): ReactNode {
return <Theme
className={"search-box-root" + (this.props.className ? ` ${this.props.className}` : "")}
backgroundLevel={BackgroundLevel.Level3}
fontLevel={FontLevel.normal}
>
<div className="search-icon">
<Icon iconName="search"/>
</div>
<div className="input-box">
<input
value={this.props.value}
placeholder={
I18N(this.props, this.props.placeholderI18N ?? "Common.Search.Placeholder")
}
onInput={(e) => {
if (e.target instanceof HTMLInputElement && this.props.valueChange) {
this.props.valueChange(e.target.value)
}
}}
/>
</div>
{this.props.value ? this.renderCleanBox() : null}
</Theme>
}
}
export { SearchBox };
+84
View File
@@ -0,0 +1,84 @@
@import "../../Component/Theme/Theme.scss";
$line-min-height: 26px;
div.text-field-root {
min-height: $line-min-height;
width: 100%;
display: flex;
padding: 5px 0;
div.text-field-intro {
min-height: $line-min-height;
width: 50%;
height: 100%;
max-width: 220px;
display: flex;
align-items: center;
padding-right: 5px;
box-sizing: border-box;
}
div.text-field-container {
min-height: $line-min-height;
width: 50%;
height: 100%;
max-width: 180px;
div.text-field-content {
min-height: $line-min-height;
width: 100%;
height: 100%;
}
div.text-field-content-styled {
box-sizing: border-box;
border: 1px solid transparent;
border-radius: 3px;
overflow: hidden;
display: flex;
}
div.text-field-content-disable {
align-items: center;
span {
display: block;
padding-left: 8px;
}
}
div.text-field-content-error {
border: 1px solid $lt-red;
}
div.text-field-error-message {
padding-top: 5px;
color: $lt-red;
}
}
}
div.dark.text-field-root {
div.text-field-content-styled {
background-color: $lt-bg-color-lvl3-dark;
color: $lt-font-color-normal-dark;
}
div.text-field-content-hover-styled:hover {
background-color: $lt-bg-color-lvl2-dark;
}
}
div.light.text-field-root {
div.text-field-content-styled {
background-color: $lt-bg-color-lvl3-light;
color: $lt-font-color-normal-light;
}
div.text-field-content-hover-styled:hover {
background-color: $lt-bg-color-lvl2-light;
}
}
+105
View File
@@ -0,0 +1,105 @@
import { Component, ReactNode, RefObject } from "react";
import { FontLevel, Theme } from "@Component/Theme/Theme";
import { AllI18nKeys, Localization } from "@Component/Localization/Localization";
import "./TextField.scss";
interface ITextFieldProps {
className?: string;
keyI18n: AllI18nKeys;
keyI18nOption?: Record<string, string>;
infoI18n?: AllI18nKeys;
disableI18n?: AllI18nKeys;
disableI18nOption?: Record<string, string>;
errorI18n?: AllI18nKeys;
errorI18nOption?: Record<string, string>;
targetRef?: RefObject<HTMLDivElement>;
customStyle?: boolean;
customHoverStyle?: boolean;
onClick?: () => any;
}
class TextField extends Component<ITextFieldProps> {
private renderInput() {
const classList: string[] = ["text-field-content"];
if (this.props.className) {
classList.push(this.props.className);
}
if (!this.props.customStyle) {
classList.push("text-field-content-styled");
}
if (!this.props.customHoverStyle) {
classList.push("text-field-content-hover-styled");
}
if (this.props.errorI18n) {
classList.push("text-field-content-error");
}
return <div
className={classList.join(" ")}
ref={this.props.targetRef}
style={{ cursor: "pointer" }}
onClick={this.props.onClick}
>
{ this.props.children }
</div>
}
private renderDisable() {
return <div
className={
`${
"text-field-content"
} ${
"text-field-content-styled"
} ${
"text-field-content-hover-styled"
} ${
"text-field-content-disable"
}`
}
ref={this.props.targetRef}
style={{ cursor: "not-allowed" }}
onClick={this.props.onClick}
>
<Localization
i18nKey={this.props.disableI18n ?? "Common.No.Unknown.Error"}
options={this.props.disableI18nOption}
/>
</div>
}
private renderError() {
return <div className="text-field-error-message">
<Localization
i18nKey={this.props.errorI18n ?? "Common.No.Unknown.Error"}
options={this.props.errorI18nOption}
/>
</div>
}
public render(): ReactNode {
return <>
<Theme className="text-field-root" fontLevel={FontLevel.normal}>
<div className="text-field-intro">
<Localization i18nKey={this.props.keyI18n} options={this.props.keyI18nOption}/>
</div>
<div className="text-field-container">
{
this.props.disableI18n ?
this.renderDisable() :
this.renderInput()
}
{
this.props.errorI18n ?
this.renderError() :
undefined
}
</div>
</Theme>
</>
}
}
export { TextField, ITextFieldProps };
@@ -0,0 +1,46 @@
@import "../../Component/Theme/Theme.scss";
$line-min-height: 26px;
div.toggles-input {
div.checkbox {
width: $line-min-height;
height: $line-min-height;
overflow: hidden;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
}
div.checkbox.red:hover {
color: $lt-red !important;
}
}
div.dark.text-field-root {
div.toggles-input div.checkbox {
background-color: $lt-bg-color-lvl3-dark;
color: $lt-font-color-normal-dark;
}
div.toggles-input div.checkbox:hover {
background-color: $lt-bg-color-lvl2-dark;
}
}
div.light.text-field-root {
div.toggles-input div.checkbox {
background-color: $lt-bg-color-lvl3-light;
color: $lt-font-color-normal-light;
}
div.toggles-input div.checkbox:hover {
background-color: $lt-bg-color-lvl2-light;
}
}
@@ -0,0 +1,53 @@
import { Component, ReactNode } from "react";
import { Icon } from "@fluentui/react";
import { TextField, ITextFieldProps } from "@Input/TextField/TextField";
import "./TogglesInput.scss";
interface ITogglesInputProps extends ITextFieldProps {
value?: boolean;
onIconName?: string;
offIconName?: string;
valueChange?: (value: boolean) => any;
red?: boolean;
}
class TogglesInput extends Component<ITogglesInputProps> {
public render(): ReactNode {
return <TextField
{...this.props}
className="toggles-input"
keyI18n={this.props.keyI18n}
customHoverStyle
customStyle
>
<div
className={"checkbox" + (this.props.red ? " red" : "")}
style={{
cursor: this.props.disableI18n ? "not-allowed" : "pointer"
}}
onClick={(() => {
if (this.props.disableI18n) {
return;
}
if (this.props.valueChange) {
this.props.valueChange(!this.props.value);
}
})}
>
<Icon
iconName={
this.props.value ?
this.props.onIconName ?? "CheckMark" :
this.props.offIconName ?? undefined
}
style={{
display: this.props.value ? "inline-block" :
this.props.offIconName ? "inline-block" : "none"
}}
/>
</div>
</TextField>;
}
}
export { TogglesInput };