diff --git a/source/Component/AttrInput/AttrInput.tsx b/source/Component/AttrInput/AttrInput.tsx index 03bbc77..148d3fe 100644 --- a/source/Component/AttrInput/AttrInput.tsx +++ b/source/Component/AttrInput/AttrInput.tsx @@ -12,6 +12,7 @@ interface IAttrInputProps { value?: number | string; isNumber?: boolean; maxLength?: number; + minLength?: number; max?: number; min?: number; step?: number; @@ -40,6 +41,11 @@ class AttrInput extends Component { return } + const minLength = this.props.minLength ?? 1; + if (value.length < minLength) { + return + } + if (this.props.isNumber) { const praseNumber = (value as any) / 1; diff --git a/source/Component/ErrorMessage/ErrorMessage.tsx b/source/Component/ErrorMessage/ErrorMessage.tsx index d82ef9c..375dc24 100644 --- a/source/Component/ErrorMessage/ErrorMessage.tsx +++ b/source/Component/ErrorMessage/ErrorMessage.tsx @@ -5,10 +5,11 @@ import "./ErrorMessage.scss"; interface IErrorMessageProps { i18nKey: AllI18nKeys; options?: Record; + className?: string; } const ErrorMessage: FunctionComponent = (props) => { - return
+ return
!!c).join(" ")}>
} diff --git a/source/Component/LabelList/LabelList.scss b/source/Component/LabelList/LabelList.scss index fb58a26..b0af602 100644 --- a/source/Component/LabelList/LabelList.scss +++ b/source/Component/LabelList/LabelList.scss @@ -1,8 +1,14 @@ @import "../Theme/Theme.scss"; +div.label-list-root { + margin: -5px; +} + div.label { width: auto; height: auto; + min-height: 24px; + box-sizing: border-box; display: inline-flex; margin: 5px 5px; justify-content: center; @@ -23,11 +29,14 @@ div.label { div.label-name { padding: 2px 3px; + display: flex; + align-items: center; text-overflow: ellipsis; overflow: hidden; } div.delete-button { + flex-shrink: 0; padding: 2px 3px; border-radius: 3px; display: flex; @@ -37,6 +46,28 @@ div.label { } } +div.label.one-line div.label-name { + max-width: 100%; + word-break: keep-all; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +div.label.add-button { + width: 24px; + align-items: center; + justify-content: center; +} + +div.dark.label.add-button { + border: .5px dashed $lt-bg-color-lvl3-dark; +} + +div.light.label.add-button { + border: .5px dashed $lt-bg-color-lvl3-light; +} + div.dark.label { transition: none; background-color: $lt-bg-color-lvl3-dark; @@ -63,14 +94,22 @@ div.light.label { } } -div.dark.label:hover, -div.dark.label.focus { +div.dark.label:hover { color: $lt-font-color-lvl2-dark; background-color: $lt-bg-color-lvl2-dark; } -div.light.label:hover, -div.light.label.focus { +div.dark.label.focus { + color: $lt-font-color-lvl1-dark; + background-color: $lt-bg-color-lvl1-dark; +} + +div.light.label:hover { color: $lt-font-color-lvl2-light; background-color: $lt-bg-color-lvl2-light; +} + +div.light.label.focus { + color: $lt-font-color-lvl1-light; + background-color: $lt-bg-color-lvl1-light; } \ No newline at end of file diff --git a/source/Component/LabelList/LabelList.tsx b/source/Component/LabelList/LabelList.tsx index 59eba50..eae20cd 100644 --- a/source/Component/LabelList/LabelList.tsx +++ b/source/Component/LabelList/LabelList.tsx @@ -1,4 +1,4 @@ -import { Component } from "react"; +import { Component, RefObject } from "react"; import { Label } from "@Model/Label"; import { Icon } from "@fluentui/react"; import { useSetting, IMixinSettingProps, Themes } from "@Context/Setting"; @@ -6,11 +6,15 @@ import { ErrorMessage } from "@Component/ErrorMessage/ErrorMessage"; import "./LabelList.scss"; interface ILabelListProps { + minHeight?: number; + maxWidth?: number; + width?: number; labels: Label[]; - canDelete?: boolean; + addRef?: RefObject; focusLabel?: Label; clickLabel?: (label: Label) => any; deleteLabel?: (label: Label) => any; + addLabel?: () => any; } @useSetting @@ -21,15 +25,23 @@ class LabelList extends Component { private renderLabel(label: Label) { const theme = this.props.setting?.themes ?? Themes.dark; - const classList:string[] = ["label"]; + const classList: string[] = ["label"]; classList.push( theme === Themes.dark ? "dark" : "light" ); const isFocus = this.props.focusLabel && this.props.focusLabel.equal(label); if (isFocus) { classList.push("focus"); } + if (this.props.maxWidth) { + classList.push("one-line"); + } const colorCss = `rgb(${label.color.join(",")})`; + const isDelete = label.isDeleted(); return
{ @@ -38,19 +50,22 @@ class LabelList extends Component { } this.isDeleteClick = false; }} - style={{ - borderColor: isFocus ? colorCss : undefined - }} >
-
- {label.name} +
+
{label.name}
{ - this.props.canDelete ? + this.props.deleteLabel ?
{ @@ -66,18 +81,37 @@ class LabelList extends Component {
} - private renderAllLabels(labels: Label[]) { + private renderAllLabels() { return this.props.labels.map((label) => { return this.renderLabel(label); }); } + + private renderAddButton() { + const theme = this.props.setting?.themes ?? Themes.dark; + const classList: string[] = ["label", "add-button"]; + classList.push( theme === Themes.dark ? "dark" : "light" ); + + return
{ + this.props.addLabel ? this.props.addLabel() : null + }} + > + +
; + } public render() { - if (this.props.labels.length > 0) { - return this.renderAllLabels(this.props.labels); - } else { - return - } + return
+ {this.renderAllLabels()} + {this.props.addLabel ? this.renderAddButton() : null} +
; } } diff --git a/source/Component/LabelPicker/LabelPicker.scss b/source/Component/LabelPicker/LabelPicker.scss new file mode 100644 index 0000000..b3a74f3 --- /dev/null +++ b/source/Component/LabelPicker/LabelPicker.scss @@ -0,0 +1,31 @@ +@import "../Theme/Theme.scss"; + +$line-min-height: 26px; + +div.label-picker-root { + width: 100%; + display: flex; + flex-wrap: wrap; + min-height: $line-min-height; + padding: 5px 0; + + div.input-intro { + width: 50%; + height: 100%; + max-width: 220px; + 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; + } +} \ No newline at end of file diff --git a/source/Component/LabelPicker/LabelPicker.tsx b/source/Component/LabelPicker/LabelPicker.tsx new file mode 100644 index 0000000..0989151 --- /dev/null +++ b/source/Component/LabelPicker/LabelPicker.tsx @@ -0,0 +1,93 @@ +import { AllI18nKeys, Localization } from "@Component/Localization/Localization"; +import { PickerList } from "../PickerList/PickerList"; +import { Label } from "@Model/Label"; +import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { Component, ReactNode, createRef } from "react"; +import { LabelList } from "../LabelList/LabelList"; +import "./LabelPicker.scss" + +interface ILabelPickerProps { + keyI18n: AllI18nKeys; + infoI18n?: AllI18nKeys; + labels: Label[]; + labelAdd?: (label: Label) => any; + labelDelete?: (label: Label) => any; +} + +interface ILabelPickerState { + isPickerVisible: boolean; +} + +@useStatusWithEvent("labelAttrChange", "labelChange") +class LabelPicker extends Component { + + public constructor(props: ILabelPickerProps) { + super(props); + this.state = { + isPickerVisible: false + } + } + + private addButtonRef = createRef(); + + 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; + } + + public render(): ReactNode { + return
+
+ +
+
+ { + this.props.labelDelete ? this.props.labelDelete(label) : 0; + }} + addLabel={() => { + this.setState({ + isPickerVisible: true + }); + }} + /> + {this.state.isPickerVisible ? { + this.setState({ + isPickerVisible: false + }); + }} + click={(label) => { + if (label instanceof Label && this.props.labelAdd) { + this.props.labelAdd(label) + } + this.setState({ + isPickerVisible: false + }); + }} + target={this.addButtonRef} + /> : null} +
+
+ } +} + +export { LabelPicker } \ No newline at end of file diff --git a/source/Component/ObjectPicker/ObjectPicker.tsx b/source/Component/ObjectPicker/ObjectPicker.tsx new file mode 100644 index 0000000..63f649f --- /dev/null +++ b/source/Component/ObjectPicker/ObjectPicker.tsx @@ -0,0 +1,12 @@ +import { Component, ReactNode } from "react"; + +interface IObjectPickerProps {} + +class ObjectPicker extends Component { + + public render(): ReactNode { + return
+ } +} + +export { ObjectPicker } \ No newline at end of file diff --git a/source/Component/PickerList/PickerList.scss b/source/Component/PickerList/PickerList.scss new file mode 100644 index 0000000..f080962 --- /dev/null +++ b/source/Component/PickerList/PickerList.scss @@ -0,0 +1,72 @@ +div.picker-list-root { + max-width: 200px; + padding: 0px; + margin: 0px; + overflow-y: auto; + + div.picker-list-item { + width: 200px; + 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%; + } + } + + div.picker-list-item:hover { + background-color: rgba($color: #000000, $alpha: .1); + } + + span.picker-list-nodata { + display: inline-block; + padding: 5px; + } +} + +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: .1); +} + +div.picker-list-root::-webkit-scrollbar-track { + /*滚动条里面轨道*/ + border-radius: 8px; + background-color: rgba($color: #000000, $alpha: 0); +} \ No newline at end of file diff --git a/source/Component/PickerList/PickerList.tsx b/source/Component/PickerList/PickerList.tsx new file mode 100644 index 0000000..47daa87 --- /dev/null +++ b/source/Component/PickerList/PickerList.tsx @@ -0,0 +1,87 @@ +import { Localization } from "@Component/Localization/Localization"; +import { Callout, DirectionalHint, Icon } from "@fluentui/react"; +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; + +interface IPickerListProps { + objectList?: IPickerListItem[]; + target?: RefObject; + dismiss?: () => any; + click?: (item: IPickerListItem) => any; +} + +class PickerList extends Component { + + private renderItem(item: IPickerListItem) { + + let color: number[] = []; + let icon: string = "tag"; + let name: string = ""; + + if (item instanceof Range) { + icon = "CubeShape" + } + if (item instanceof Group) { + icon = "WebAppBuilderFragment" + } + if (item instanceof CtrlObject) { + 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) { + icon = "tag"; + color = item.color.concat([]); + name = item.name; + } + + return
{ + if (this.props.click) { + this.props.click(item) + } + }} + > +
+
+ +
+
+ {name} +
+
; + } + + public render(): ReactNode { + return +
+ {this.props.objectList ? this.props.objectList.map((item) => { + return this.renderItem(item); + }) : null} + {!this.props.objectList || (this.props.objectList && this.props.objectList.length <= 0) ? + + : null + } +
+
+ } +} + +export { PickerList } \ No newline at end of file diff --git a/source/Component/TogglesInput/TogglesInput.tsx b/source/Component/TogglesInput/TogglesInput.tsx index 0aa21c6..01a5b5a 100644 --- a/source/Component/TogglesInput/TogglesInput.tsx +++ b/source/Component/TogglesInput/TogglesInput.tsx @@ -9,7 +9,9 @@ interface ITogglesInputProps { infoI18n?: AllI18nKeys; value?: boolean; disable?: boolean; - valueChange?: (color: boolean) => any; + onIconName?: string; + offIconName?: string; + valueChange?: (value: boolean) => any; } class TogglesInput extends Component { @@ -33,9 +35,17 @@ class TogglesInput extends Component { } })} > - +
diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 957e196..8853e52 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -30,8 +30,10 @@ interface IStatusEvent { focusObjectChange: void; focusLabelChange: void; objectChange: void; + rangeLabelChange: void; labelChange: void; rangeAttrChange: void; + labelAttrChange: void; } class Status extends Emitter { @@ -120,6 +122,40 @@ class Status extends Emitter { } } + public addRangeLabel(id: ObjectID, val: Label) { + const range = this.model.getObjectById(id); + if (range && range instanceof Range) { + range.addLabel(val); + this.emit("rangeLabelChange"); + } + } + + public deleteRangeLabel(id: ObjectID, val: Label) { + const range = this.model.getObjectById(id); + if (range && range instanceof Range) { + range.removeLabel(val); + this.emit("rangeLabelChange"); + } + } + + /** + * 修改范围属性 + */ + public changeLabelAttrib + (label: Label, key: K, val: Label[K]) { + let findLabel: Label | undefined; + for (let i = 0; i < this.model.labelPool.length; i++) { + if (this.model.labelPool[i].equal(label)) { + findLabel = this.model.labelPool[i]; + break; + } + } + if (findLabel) { + findLabel[key] = val; + this.emit("labelAttrChange"); + } + } + /** * 鼠标工具状态 */ diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index 9253e47..3554142 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -23,6 +23,7 @@ const EN_US = { "Input.Error.Max": "Enter value must be less than {num}", "Input.Error.Min": "Enter value must be greater than {num}", "Input.Error.Length": "The length of the input content must be less than {num}", + "Input.Error.Length.Less": "The length of the input content must be greater than {num}", "Object.List.New.Group": "Group object {id}", "Object.List.New.Range": "Range object {id}", "Object.List.New.Label": "Label {id}", @@ -49,7 +50,10 @@ const EN_US = { "Common.Attr.Key.Color": "Color", "Common.Attr.Key.Display": "Display", "Common.Attr.Key.Update": "Update", + "Common.Attr.Key.Delete": "Delete", + "Common.Attr.Key.Label": "Label", "Common.Attr.Key.Error.Multiple": "Multiple values", + "Common.Attr.Key.Label.Picker.Nodata": "No tags can be added", "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.Label.Details.Error.Unspecified": "Label object not specified", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 7986b9e..8e63ed4 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -23,6 +23,7 @@ const ZH_CN = { "Input.Error.Max": "输入数值须小于 {number}", "Input.Error.Min": "输入数值须大于 {number}", "Input.Error.Length": "输入内容长度须小于 {number}", + "Input.Error.Length.Less": "输入内容长度须大于 {number}", "Object.List.New.Group": "组对象 {id}", "Object.List.New.Range": "范围对象 {id}", "Object.List.New.Label": "标签 {id}", @@ -49,7 +50,10 @@ const ZH_CN = { "Common.Attr.Key.Color": "颜色", "Common.Attr.Key.Display": "显示", "Common.Attr.Key.Update": "更新", + "Common.Attr.Key.Delete": "删除", + "Common.Attr.Key.Label": "标签", "Common.Attr.Key.Error.Multiple": "多重数值", + "Common.Attr.Key.Label.Picker.Nodata": "没有可以被添加的标签", "Panel.Info.Range.Details.Attr.Error.Not.Range": "对象不是一个范围", "Panel.Info.Range.Details.Attr.Error.Unspecified": "未指定范围对象", "Panel.Info.Label.Details.Error.Unspecified": "未指定标签对象", diff --git a/source/Panel/LabelDetails/LabelDetails.tsx b/source/Panel/LabelDetails/LabelDetails.tsx index 118a2df..fbda488 100644 --- a/source/Panel/LabelDetails/LabelDetails.tsx +++ b/source/Panel/LabelDetails/LabelDetails.tsx @@ -4,37 +4,32 @@ import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { AllI18nKeys } from "@Component/Localization/Localization"; import { ErrorMessage } from "@Component/ErrorMessage/ErrorMessage"; import { ColorInput } from "@Component/ColorInput/ColorInput"; -import "./LabelDetails.scss"; -import { LabelList } from "@Component/LabelList/LabelList"; import { Label } from "@Model/Label"; +import { TogglesInput } from "@Component/TogglesInput/TogglesInput"; +import "./LabelDetails.scss"; -@useStatusWithEvent("focusLabelChange") +@useStatusWithEvent("focusLabelChange", "labelAttrChange", "labelChange") class LabelDetails extends Component { - - public readonly AttrI18nKey: AllI18nKeys[] = [ - "Common.Attr.Key.Display.Name", - "Common.Attr.Key.Color", - ] private renderFrom(label: Label) { return <> - - { - if (this.props.status) { - this.props.status.model.deleteLabel(label); - this.props.status.setLabelObject(); - } - }} - /> - + { + if (this.props.status) { + this.props.status.changeLabelAttrib(label, "name", value); + } + }}/> { if (this.props.status) { - + this.props.status.changeLabelAttrib(label, "color", color); + } + }}/> + + { + if (this.props.status) { + this.props.status.model.deleteLabel(label); + this.props.status.setLabelObject(); } }}/> diff --git a/source/Panel/LabelList/LabelList.scss b/source/Panel/LabelList/LabelList.scss index b9eec31..7643f5c 100644 --- a/source/Panel/LabelList/LabelList.scss +++ b/source/Panel/LabelList/LabelList.scss @@ -2,7 +2,11 @@ div.label-list-panel-root { width: 100%; - height: 100%; + min-height: 100%; padding: 10px; box-sizing: border-box; +} + +div.label-list-pabel-err-msg { + padding-bottom: 5px; } \ No newline at end of file diff --git a/source/Panel/LabelList/LabelList.tsx b/source/Panel/LabelList/LabelList.tsx index bb41ae5..fce98be 100644 --- a/source/Panel/LabelList/LabelList.tsx +++ b/source/Panel/LabelList/LabelList.tsx @@ -3,6 +3,7 @@ import { Component } from "react"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { Label } from "@Model/Label"; +import { ErrorMessage } from "@Component/ErrorMessage/ErrorMessage"; import "./LabelList.scss"; interface ILabelListProps { @@ -10,7 +11,7 @@ interface ILabelListProps { } @useSetting -@useStatusWithEvent("labelChange", "focusLabelChange") +@useStatusWithEvent("labelChange", "focusLabelChange", "labelAttrChange") class LabelList extends Component { private labelInnerClick: boolean = false; @@ -29,8 +30,13 @@ class LabelList extends Component + {labels.length <=0 ? + : null + } { @@ -49,6 +55,10 @@ class LabelList extends Component { + this.props.status ? this.props.status.newLabel() : undefined; + }} />
; } diff --git a/source/Panel/RangeDetails/RangeDetails.tsx b/source/Panel/RangeDetails/RangeDetails.tsx index 101a440..5a74f57 100644 --- a/source/Panel/RangeDetails/RangeDetails.tsx +++ b/source/Panel/RangeDetails/RangeDetails.tsx @@ -7,13 +7,15 @@ import { Range } from "@Model/Range"; import { ObjectID } from "@Model/Renderer"; import { ColorInput } from "@Component/ColorInput/ColorInput"; import { TogglesInput } from "@Component/TogglesInput/TogglesInput"; +import { LabelPicker } from "@Component/LabelPicker/LabelPicker"; import "./RangeDetails.scss"; -@useStatusWithEvent("rangeAttrChange", "focusObjectChange") +@useStatusWithEvent("rangeAttrChange", "focusObjectChange", "rangeLabelChange") class RangeDetails extends Component { public readonly AttrI18nKey: AllI18nKeys[] = [ "Common.Attr.Key.Display.Name", + "Common.Attr.Key.Label", "Common.Attr.Key.Display", "Common.Attr.Key.Update", "Common.Attr.Key.Color", @@ -58,6 +60,20 @@ class RangeDetails extends Component { {this.renderAttrInput(range.id, keyIndex ++, range.displayName, (val, status) => { status.changeRangeAttrib(range.id, "displayName", val); })} + + { + if (this.props.status) { + this.props.status.addRangeLabel(range.id, label); + } + }} + labelDelete={(label) => { + if (this.props.status) { + this.props.status.deleteRangeLabel(range.id, label); + } + }} + /> { if (this.props.status) {