refactor: enhance tag input UI (#44)

* refactor: enhance tag input UI

* style: fix tslint errors

* Update tagInputSize.scss

* Update tagInputSize.scss
This commit is contained in:
kunzheng 2020-02-15 18:52:31 -08:00 коммит произвёл GitHub
Родитель d1f6f5a186
Коммит 5c9fa5de3b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 450 добавлений и 620 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,53 +1,53 @@
import {createTheme} from "office-ui-fabric-react"; import {createTheme} from "office-ui-fabric-react";
const greenButtonPalette = { const greenButtonPalette = {
themePrimary: "#78ad0e", themePrimary: "#78ad0e",
themeLighterAlt: "#050701", themeLighterAlt: "#050701",
themeLighter: "#131c02", themeLighter: "#131c02",
themeLight: "#243404", themeLight: "#243404",
themeTertiary: "#486808", themeTertiary: "#486808",
themeSecondary: "#6a990c", themeSecondary: "#6a990c",
themeDarkAlt: "#83b61f", themeDarkAlt: "#83b61f",
themeDark: "#94c13a", themeDark: "#94c13a",
themeDarker: "#add165", themeDarker: "#add165",
neutralLighterAlt: "#393e43", neutralLighterAlt: "#393e43",
neutralLighter: "#40454b", neutralLighter: "#40454b",
neutralLight: "#4c5157", neutralLight: "#4c5157",
neutralQuaternaryAlt: "#53585f", neutralQuaternaryAlt: "#53585f",
neutralQuaternary: "#595f65", neutralQuaternary: "#595f65",
neutralTertiaryAlt: "#73787f", neutralTertiaryAlt: "#73787f",
neutralTertiary: "#dfdfdf", neutralTertiary: "#dfdfdf",
neutralSecondary: "#e4e4e4", neutralSecondary: "#e4e4e4",
neutralPrimaryAlt: "#e9e9e9", neutralPrimaryAlt: "#e9e9e9",
neutralPrimary: "#cfcfcf", neutralPrimary: "#cfcfcf",
neutralDark: "#f4f4f4", neutralDark: "#f4f4f4",
black: "#f9f9f9", black: "#f9f9f9",
white: "#32363B", white: "#32363B",
}; };
const whiteButtonPalette = { const whiteButtonPalette = {
themePrimary: "white", themePrimary: "white",
themeLighterAlt: "#767676", themeLighterAlt: "#767676",
themeLighter: "#a6a6a6", themeLighter: "#a6a6a6",
themeLight: "#c8c8c8", themeLight: "#c8c8c8",
themeTertiary: "#d0d0d0", themeTertiary: "#d0d0d0",
themeSecondary: "#dadada", themeSecondary: "#dadada",
themeDarkAlt: "#eaeaea", themeDarkAlt: "#eaeaea",
themeDark: "#f4f4f4", themeDark: "#f4f4f4",
themeDarker: "#f8f8f8", themeDarker: "#f8f8f8",
neutralLighterAlt: "#393e43", neutralLighterAlt: "#393e43",
neutralLighter: "#40454b", neutralLighter: "#40454b",
neutralLight: "#4c5157", neutralLight: "#4c5157",
neutralQuaternaryAlt: "#53585f", neutralQuaternaryAlt: "#53585f",
neutralQuaternary: "#595f65", neutralQuaternary: "#595f65",
neutralTertiaryAlt: "#73787f", neutralTertiaryAlt: "#73787f",
neutralTertiary: "#dfdfdf", neutralTertiary: "#dfdfdf",
neutralSecondary: "#e4e4e4", neutralSecondary: "#e4e4e4",
neutralPrimaryAlt: "#e9e9e9", neutralPrimaryAlt: "#e9e9e9",
neutralPrimary: "#cfcfcf", neutralPrimary: "#cfcfcf",
neutralDark: "#f4f4f4", neutralDark: "#f4f4f4",
black: "#f9f9f9", black: "#f9f9f9",
white: "#32363B", white: "#32363B",
}; };
const redButtonPalette = { const redButtonPalette = {
@ -76,71 +76,101 @@ const redButtonPalette = {
}; };
const greyButtonPalette = { const greyButtonPalette = {
themePrimary: "#949799", themePrimary: "#949799",
themeLighterAlt: "#060606", themeLighterAlt: "#060606",
themeLighter: "#181818", themeLighter: "#181818",
themeLight: "#2d2d2e", themeLight: "#2d2d2e",
themeTertiary: "#595b5c", themeTertiary: "#595b5c",
themeSecondary: "#838587", themeSecondary: "#838587",
themeDarkAlt: "#9fa1a3", themeDarkAlt: "#9fa1a3",
themeDark: "#adb0b1", themeDark: "#adb0b1",
themeDarker: "#c3c4c6", themeDarker: "#c3c4c6",
neutralLighterAlt: "#262a2f", neutralLighterAlt: "#262a2f",
neutralLighter: "#262a2e", neutralLighter: "#262a2e",
neutralLight: "#24282c", neutralLight: "#24282c",
neutralQuaternaryAlt: "#222529", neutralQuaternaryAlt: "#222529",
neutralQuaternary: "#202328", neutralQuaternary: "#202328",
neutralTertiaryAlt: "#1f2226", neutralTertiaryAlt: "#1f2226",
neutralTertiary: "#f0f2f5", neutralTertiary: "#f0f2f5",
neutralSecondary: "#f2f4f6", neutralSecondary: "#f2f4f6",
neutralPrimaryAlt: "#f5f6f8", neutralPrimaryAlt: "#f5f6f8",
neutralPrimary: "#e9ecef", neutralPrimary: "#e9ecef",
neutralDark: "#fafbfb", neutralDark: "#fafbfb",
black: "#fcfdfd", black: "#fcfdfd",
white: "#272B30", white: "#272B30",
}; };
const blueButtonPalette = { const blueButtonPalette = {
themePrimary: "#5bc0de", themePrimary: "#5bc0de",
themeLighterAlt: "#040809", themeLighterAlt: "#040809",
themeLighter: "#0f1f23", themeLighter: "#0f1f23",
themeLight: "#1b3943", themeLight: "#1b3943",
themeTertiary: "#377385", themeTertiary: "#377385",
themeSecondary: "#50a8c3", themeSecondary: "#50a8c3",
themeDarkAlt: "#6ac5e1", themeDarkAlt: "#6ac5e1",
themeDark: "#7fcee6", themeDark: "#7fcee6",
themeDarker: "#9edaec", themeDarker: "#9edaec",
neutralLighterAlt: "#262a2f", neutralLighterAlt: "#262a2f",
neutralLighter: "#262a2e", neutralLighter: "#262a2e",
neutralLight: "#24282c", neutralLight: "#24282c",
neutralQuaternaryAlt: "#222529", neutralQuaternaryAlt: "#222529",
neutralQuaternary: "#202328", neutralQuaternary: "#202328",
neutralTertiaryAlt: "#1f2226", neutralTertiaryAlt: "#1f2226",
neutralTertiary: "#f0f2f5", neutralTertiary: "#f0f2f5",
neutralSecondary: "#f2f4f6", neutralSecondary: "#f2f4f6",
neutralPrimaryAlt: "#f5f6f8", neutralPrimaryAlt: "#f5f6f8",
neutralPrimary: "#e9ecef", neutralPrimary: "#e9ecef",
neutralDark: "#fafbfb", neutralDark: "#fafbfb",
black: "#fcfdfd", black: "#fcfdfd",
white: "#272b30", white: "#272b30",
};
const darkThemePalette = {
neutralLighterAlt: "#282828",
neutralLighter: "#313131",
neutralLight: "#3f3f3f",
neutralQuaternaryAlt: "#484848",
neutralQuaternary: "#4f4f4f",
neutralTertiaryAlt: "#6d6d6d",
neutralTertiary: "#c8c8c8",
neutralSecondary: "#d0d0d0",
neutralPrimaryAlt: "#dadada",
neutralPrimary: "#ffffff",
neutralDark: "#f4f4f4",
black: "#f8f8f8",
white: "#1f1f1f",
themePrimary: "#ffffff",
themeLighterAlt: "#020609",
themeLighter: "#091823",
themeLight: "#112d43",
themeTertiary: "#235a85",
themeSecondary: "#3385c3",
themeDarkAlt: "#4ba0e1",
themeDark: "#65aee6",
themeDarker: "#8ac2ec",
accent: "#3a96dd",
}; };
export function getPrimaryWhiteTheme() { export function getPrimaryWhiteTheme() {
return createTheme({palette: whiteButtonPalette}); return createTheme({palette: whiteButtonPalette});
} }
export function getPrimaryRedTheme() { export function getPrimaryRedTheme() {
return createTheme({palette: redButtonPalette}); return createTheme({palette: redButtonPalette});
} }
export function getPrimaryGreenTheme() { export function getPrimaryGreenTheme() {
return createTheme({palette: greenButtonPalette}); return createTheme({palette: greenButtonPalette});
} }
export function getPrimaryGreyTheme() { export function getPrimaryGreyTheme() {
return createTheme({palette: greyButtonPalette}); return createTheme({palette: greyButtonPalette});
} }
export function getPrimaryBlueTheme() { export function getPrimaryBlueTheme() {
return createTheme({palette: blueButtonPalette}); return createTheme({palette: blueButtonPalette});
}
export function getDarkTheme() {
return createTheme({palette: darkThemePalette});
} }

Просмотреть файл

@ -1,217 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import React from "react";
import { FontIcon } from "office-ui-fabric-react";
import { Align } from "../align/align";
import { ITag, FieldType, FieldFormat } from "../../../../models/applicationState";
import { strings } from "../../../../common/strings";
import "./tagContextMenu.scss";
/**
* Properties for TagContextMenu
* @member tag - ITag
*/
export interface ITagContextMenuProps {
tag: ITag;
onChange?: (oldTag: ITag, newTag: ITag) => void;
}
/**
* State for TagContextMenu
* @member tag - ITag
*/
export interface ITagContextMenuState {
tag: ITag;
showFormat?: boolean;
showType?: boolean;
}
/**
* Generic modal that displays a message
*/
export default class TagContextMenu extends React.Component<ITagContextMenuProps, ITagContextMenuState> {
private static filterFormat(type: FieldType): FieldFormat[] {
switch (type) {
case FieldType.String:
return [
FieldFormat.NotSpecified,
FieldFormat.Alphanumberic,
FieldFormat.NoWhiteSpaces,
];
case FieldType.Number:
return [
FieldFormat.NotSpecified,
FieldFormat.Currency,
];
case FieldType.Integer:
return [
FieldFormat.NotSpecified,
];
case FieldType.Date:
return [
FieldFormat.NotSpecified,
FieldFormat.DMY,
FieldFormat.MDY,
FieldFormat.YMD,
];
case FieldType.Time:
return [
FieldFormat.NotSpecified,
];
default:
return [ FieldFormat.NotSpecified ];
}
}
public state: ITagContextMenuState = {
tag: this.props.tag,
showFormat: false,
showType: false,
};
private typeRef = React.createRef<HTMLDivElement>();
private formatRef = React.createRef<HTMLDivElement>();
public render() {
const tag = this.state.tag;
const types = Object.keys(FieldType);
const formats = TagContextMenu.filterFormat(tag.type);
const align = {
// Align top right of source node (dropdown) with top left of target node (tag name row)
points: ["tr", "br"],
// Offset source node by 0px in x and 3px in y
offset: [0, 3],
// Auto adjust position when source node is overflowed
overflow: {adjustX: true, adjustY: true},
};
return (
<div className = "field-background field-background-color">
<div className = "tag-field justify-content-start">
<div className = "row-4 tag-field-item">
<div
ref={this.typeRef}
className="field-background-container"
onClick={this.handleTypeShow}>
<FontIcon iconName="Link" />
<span className="type-selected">{tag.type ? tag.type : strings.tags.toolbar.type}</span>
<FontIcon iconName="ChevronDown" className="pr-1" />
</div>
<Align align={align} target={() => this.typeRef.current} monitorWindowResize={true}>
{
this.state.showType &&
<div className={["tag-input-portal", "format-list", "format-items-list"].join(" ")}>
{
types.filter((type) => {
return FieldType[type] !== tag.type;
}).map((type) => {
return (
this.getTypeListItem(this, type)
);
})
}
</div>
}
</Align>
</div>
<div className = "horizontal-line"></div>
<div className = "row-4 tag-field-item">
<div
ref={this.formatRef}
className = "field-background-container"
onClick={this.handleFormatShow}>
<FontIcon iconName="Link" />
<span>{tag.format ? tag.format : strings.tags.toolbar.format}</span>
<FontIcon iconName="ChevronDown" className="pr-1" />
</div>
<Align align={align} target={() => this.formatRef.current}>
{
this.state.showFormat &&
<div className = {["tag-input-portal", "format-list", "format-items-list"].join(" ")}>
{
formats.filter((format) => {
return format !== tag.format;
}).map((format) => {
return (
this.getFormatListItem(this, format)
);
})
}
</div>
}
</Align>
</div>
</div>
</div>
);
}
private handleTypeChange = (event) => {
const oldTag = this.state.tag;
const newTag: ITag = {
...oldTag,
type: event.target.value as FieldType,
format: FieldFormat.NotSpecified,
};
this.setState({ tag: newTag, showType: false }, () => {
if (this.props.onChange) {
this.props.onChange(oldTag, newTag);
}
});
}
private handleTypeShow = (e) => {
if (e.type === "click") {
this.setState({showType: !this.state.showType, showFormat: false});
}
}
private handleFormatShow = (e) => {
if (e.type === "click") {
this.setState({showFormat: !this.state.showFormat, showType: false});
}
}
private handleFormatChange = (event) => {
const oldTag = this.state.tag;
const newTag: ITag = {
...oldTag,
format: event.target.value,
};
this.setState({ tag: newTag, showFormat: false }, () => {
if (this.props.onChange) {
this.props.onChange(oldTag, newTag);
}
});
}
private getTypeListItem(props, type) {
return (
<button type="button"
key={type}
onClick={props.handleTypeChange}
value={FieldType[type]}
className="list-items list-items-color"
>
{FieldType[type]}
</button>
);
}
private getFormatListItem(props, format) {
return(
<button type="button"
key={format}
onClick={props.handleFormatChange}
value={format}
className="list-items list-items-color"
>
{format}
</button>
);
}
}

Просмотреть файл

@ -1,72 +0,0 @@
@import "tagInput.scss";
.field-background {
width: $tagInputWidth - $tagColorWidth;
padding: 0px 0px 5px 0px;
&-color {
background-color: #32363B;
}
&-container {
display: flex;
justify-content: space-between;
color: white;
padding: 0px 0px 0px 20px;
}
}
.horizontal-line {
height: 0.1;
width: 100%;
border: 0.5px solid white;
margin: 5px 0px;
}
.tag-field {
width: 100%;
height: 100%;
margin: 0px;
}
.tag-field-item {
width: 100%;
padding: 4px 0px 4px 0px;
}
.format-list {
width: $tagInputWidth - $tagColorWidth;
border: white 1px;
}
.format-items-hide {
display:none;
}
.format-items-list {
width: $tagInputWidth - $tagColorWidth;
list-style-type: none;
padding: 0px;
margin: 0px;
display: inline-flex;
flex-direction: column;
justify-items: self-start;
}
.list-items {
border: 0px;
width:100%;
display: inline-flex;
justify-items: flex-start;
padding: 0px 0px 0px 20px;
&-color {
background-color: #32363B;
color: white;
}
&-color:hover {
background-color: #2D2F31;
color: white;
}
}

Просмотреть файл

@ -176,13 +176,13 @@
cursor: pointer; cursor: pointer;
} }
&-color-edit:hover {
background: $darker-1;
cursor: pointer;
}
&-color { &-color {
width: $tagColorWidth; width: $tagColorWidth;
&-edit:hover {
background: $darker-1;
cursor: pointer;
}
} }
&-lock-icon { &-lock-icon {
@ -245,6 +245,10 @@
} }
} }
} }
&-contextual-menu {
width: $tagContextualMenuWidth;
}
} }
&-index-span { &-index-span {

Просмотреть файл

@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { ReactWrapper, mount } from "enzyme"; import { ReactWrapper, mount } from "enzyme";
import { TagInput, ITagInputProps, ITagInputState } from "./tagInput"; import { TagInput, TagOperationMode, ITagInputProps, ITagInputState } from "./tagInput";
import MockFactory from "../../../../common/mockFactory"; import MockFactory from "../../../../common/mockFactory";
import { ITag } from "../../../../models/applicationState"; import { ITag } from "../../../../models/applicationState";
import TagInputItem, { ITagInputItemProps } from "./tagInputItem"; import TagInputItem, { ITagInputItemProps } from "./tagInputItem";
@ -43,7 +43,7 @@ describe("Tag Input Component", () => {
const wrapper = createComponent(props); const wrapper = createComponent(props);
wrapper.find(".tag-color").first().simulate("click"); wrapper.find(".tag-color").first().simulate("click");
expect(props.onTagClick).toBeCalledWith(props.tags[0]); expect(props.onTagClick).toBeCalledWith(props.tags[0]);
expect(wrapper.state().clickedColor).toBe(true); expect(wrapper.state().tagOperation === TagOperationMode.ColorPicker).toBe(true);
expect(props.onCtrlTagClick).not.toBeCalled(); expect(props.onCtrlTagClick).not.toBeCalled();
}); });
@ -51,19 +51,18 @@ describe("Tag Input Component", () => {
const props = createProps(); const props = createProps();
const wrapper = createComponent(props); const wrapper = createComponent(props);
wrapper.find("div.tag-name-container").first().simulate("click", { altKey: true } ); wrapper.find("div.tag-name-container").first().simulate("click", { altKey: true } );
expect(wrapper.state().editingTag).toEqual(props.tags[0]); expect(wrapper.state().selectedTag).toEqual(props.tags[0]);
expect(wrapper.exists("input.tag-name-editor")).toBe(true); expect(wrapper.exists("input.tag-name-editor")).toBe(true);
}); });
it("Edits tag color when alt clicked", () => { it("Edits tag color when alt clicked", () => {
const props = createProps(); const props = createProps();
const wrapper = createComponent(props); const wrapper = createComponent(props);
expect(wrapper.state().clickedColor).toBe(false); expect(wrapper.state().tagOperation === TagOperationMode.None).toBe(false);
expect(wrapper.exists("div.color-picker-container")).toBe(false); expect(wrapper.exists("div.color-picker-container")).toBe(false);
wrapper.find("div.tag-color").first().simulate("click", { altKey: true } ); wrapper.find("div.tag-color").first().simulate("click", { altKey: true } );
expect(wrapper.state().clickedColor).toBe(true); expect(wrapper.state().tagOperation === TagOperationMode.ColorPicker).toBe(true);
expect(wrapper.state().showColorPicker).toBe(true); expect(wrapper.state().selectedTag).toEqual(props.tags[0]);
expect(wrapper.state().editingTag).toEqual(props.tags[0]);
expect(wrapper.exists("div.color-picker-container")).toBe(true); expect(wrapper.exists("div.color-picker-container")).toBe(true);
// Get color picker and call onEditColor function // Get color picker and call onEditColor function
const picker = wrapper.find(ColorPicker).instance() as ColorPicker; const picker = wrapper.find(ColorPicker).instance() as ColorPicker;
@ -85,7 +84,7 @@ describe("Tag Input Component", () => {
const wrapper = createComponent(props); const wrapper = createComponent(props);
wrapper.find(".tag-color").first().simulate("click", { ctrlKey: true }); wrapper.find(".tag-color").first().simulate("click", { ctrlKey: true });
expect(props.onCtrlTagClick).toBeCalledWith(props.tags[0]); expect(props.onCtrlTagClick).toBeCalledWith(props.tags[0]);
expect(wrapper.state().clickedColor).toBe(true); expect(wrapper.state().tagOperation === TagOperationMode.ColorPicker).toBe(true);
expect(props.onTagClick).not.toBeCalled(); expect(props.onTagClick).not.toBeCalled();
}); });
@ -167,7 +166,7 @@ describe("Tag Input Component", () => {
const wrapper = createComponent(props); const wrapper = createComponent(props);
wrapper.find("div.tag-name-container").first().simulate("click"); wrapper.find("div.tag-name-container").first().simulate("click");
wrapper.find("div.tag-input-toolbar-item.edit").simulate("click"); wrapper.find("div.tag-input-toolbar-item.edit").simulate("click");
expect(wrapper.state().editingTag).toEqual(tags[0]); expect(wrapper.state().selectedTag).toEqual(tags[0]);
expect(wrapper.exists("input.tag-name-editor")).toBe(true); expect(wrapper.exists("input.tag-name-editor")).toBe(true);
}); });
@ -175,13 +174,12 @@ describe("Tag Input Component", () => {
const tags = MockFactory.createTestTags(); const tags = MockFactory.createTestTags();
const props = createProps(tags); const props = createProps(tags);
const wrapper = createComponent(props); const wrapper = createComponent(props);
expect(wrapper.state().clickedColor).toBe(false); expect(wrapper.state().tagOperation === TagOperationMode.None).toBe(false);
expect(wrapper.exists("div.color-picker-container")).toBe(false); expect(wrapper.exists("div.color-picker-container")).toBe(false);
wrapper.find("div.tag-color").first().simulate("click"); wrapper.find("div.tag-color").first().simulate("click");
expect(wrapper.state().clickedColor).toBe(true); expect(wrapper.state().tagOperation === TagOperationMode.ColorPicker).toBe(true);
wrapper.find("div.tag-input-toolbar-item.edit").simulate("click"); wrapper.find("div.tag-input-toolbar-item.edit").simulate("click");
expect(wrapper.state().showColorPicker).toBe(true); expect(wrapper.state().selectedTag).toEqual(tags[0]);
expect(wrapper.state().editingTag).toEqual(tags[0]);
expect(wrapper.exists("div.color-picker-container")).toBe(true); expect(wrapper.exists("div.color-picker-container")).toBe(true);
// Get color picker and call onEditColor function // Get color picker and call onEditColor function
const picker = wrapper.find(ColorPicker).instance() as ColorPicker; const picker = wrapper.find(ColorPicker).instance() as ColorPicker;

Просмотреть файл

@ -3,8 +3,15 @@
import React, { KeyboardEvent, RefObject } from "react"; import React, { KeyboardEvent, RefObject } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { FontIcon } from "office-ui-fabric-react"; import {
import { Align } from "../align/align"; ContextualMenu,
Customizer,
FontIcon,
IContextualMenuItem,
ICustomizations,
} from "office-ui-fabric-react";
import { strings } from "../../../../common/strings";
import { getDarkTheme } from "../../../../common/themes";
import { AlignPortal } from "../align/alignPortal"; import { AlignPortal } from "../align/alignPortal";
import { randomIntInRange } from "../../../../common/utils"; import { randomIntInRange } from "../../../../common/utils";
import { IRegion, ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applicationState"; import { IRegion, ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applicationState";
@ -14,11 +21,16 @@ import "../condensedList/condensedList.scss";
import TagInputItem, { ITagInputItemProps, ITagClickProps } from "./tagInputItem"; import TagInputItem, { ITagInputItemProps, ITagClickProps } from "./tagInputItem";
import TagInputToolbar from "./tagInputToolbar"; import TagInputToolbar from "./tagInputToolbar";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { strings } from "../../../../common/strings";
import TagContextMenu from "./tagContentMenu";
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
const tagColors = require("../../common/tagColors.json"); const tagColors = require("../../common/tagColors.json");
export enum TagOperationMode {
None,
ColorPicker,
ContextualMenu,
Rename,
}
export interface ITagInputProps { export interface ITagInputProps {
/** Current list of tags */ /** Current list of tags */
tags: ITag[]; tags: ITag[];
@ -56,48 +68,102 @@ export interface ITagInputProps {
export interface ITagInputState { export interface ITagInputState {
tags: ITag[]; tags: ITag[];
clickedColor: boolean; tagOperation: TagOperationMode;
clickedDropDown: boolean;
showColorPicker: boolean;
showDropDown: boolean;
addTags: boolean; addTags: boolean;
searchTags: boolean; searchTags: boolean;
searchQuery: string; searchQuery: string;
selectedTag: ITag; selectedTag: ITag;
editingTag: ITag;
editingTagNode: Element;
} }
function defaultDOMNode(): Element { function defaultDOMNode(): Element {
return document.createElement("div"); return document.createElement("div");
} }
function filterFormat(type: FieldType): FieldFormat[] {
switch (type) {
case FieldType.String:
return [
FieldFormat.NotSpecified,
FieldFormat.Alphanumberic,
FieldFormat.NoWhiteSpaces,
];
case FieldType.Number:
return [
FieldFormat.NotSpecified,
FieldFormat.Currency,
];
case FieldType.Integer:
return [
FieldFormat.NotSpecified,
];
case FieldType.Date:
return [
FieldFormat.NotSpecified,
FieldFormat.DMY,
FieldFormat.MDY,
FieldFormat.YMD,
];
case FieldType.Time:
return [
FieldFormat.NotSpecified,
];
default:
return [ FieldFormat.NotSpecified ];
}
}
export class TagInput extends React.Component<ITagInputProps, ITagInputState> { export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
public state: ITagInputState = { public state: ITagInputState = {
tags: this.props.tags || [], tags: this.props.tags || [],
clickedColor: false, tagOperation: TagOperationMode.None,
clickedDropDown: false,
showColorPicker: false,
showDropDown: false,
addTags: this.props.showTagInputBox, addTags: this.props.showTagInputBox,
searchTags: this.props.showSearchBox, searchTags: this.props.showSearchBox,
searchQuery: "", searchQuery: "",
selectedTag: null, selectedTag: null,
editingTag: null,
editingTagNode: null,
}; };
private tagItemRefs: Map<string, TagInputItem> = new Map<string, TagInputItem>(); private tagItemRefs: Map<string, TagInputItem> = new Map<string, TagInputItem>();
private inputRef: RefObject<HTMLInputElement>; private inputRef: RefObject<HTMLInputElement>;
private colorPickerNode = defaultDOMNode();
constructor(props) { constructor(props) {
super(props); super(props);
this.inputRef = React.createRef(); this.inputRef = React.createRef();
} }
public componentDidUpdate(prevProps: ITagInputProps) {
if (prevProps.tags !== this.props.tags) {
let selectedTag = this.state.selectedTag;
if (selectedTag) {
selectedTag = this.props.tags.find((tag) => this.isNameEqual(tag, selectedTag));
}
this.setState({
tags: this.props.tags,
selectedTag,
});
}
if (prevProps.selectedRegions !== this.props.selectedRegions && this.props.selectedRegions.length > 0) {
this.setState({
selectedTag: null,
});
}
}
public render() { public render() {
const dark: ICustomizations = {
settings: {
theme: getDarkTheme(),
},
scopedSettings: {},
};
const { selectedTag } = this.state;
const selectedTagRef = selectedTag ? this.tagItemRefs.get(selectedTag.name).getTagNameRef() : null;
return ( return (
<div className="tag-input"> <div className="tag-input">
<div className="tag-input-header p-2"> <div className="tag-input-header p-2">
@ -130,10 +196,18 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
<FontIcon iconName="Search" /> <FontIcon iconName="Search" />
</div> </div>
} }
{this.getColorPickerPortal()}
{this.getTagFieldPortal()}
<div className="tag-input-items"> <div className="tag-input-items">
{this.renderTagItems()} {this.renderTagItems()}
<Customizer {...dark}>
<ContextualMenu
className="tag-input-contextual-menu"
items={this.getContextualMenuItems()}
hidden={!selectedTagRef || this.state.tagOperation !== TagOperationMode.ContextualMenu}
target={selectedTagRef}
onDismiss={this.onHideContextualMenu}
/>
</Customizer>
{this.getColorPickerPortal()}
</div> </div>
{ {
this.state.addTags && this.state.addTags &&
@ -156,26 +230,6 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
); );
} }
public componentDidUpdate(prevProps: ITagInputProps) {
if (prevProps.tags !== this.props.tags) {
let selectedTag = this.state.selectedTag;
if (selectedTag) {
selectedTag = this.props.tags.find((tag) => this.isNameEqual(tag, selectedTag));
}
this.setState({
tags: this.props.tags,
selectedTag,
});
}
if (prevProps.selectedRegions !== this.props.selectedRegions && this.props.selectedRegions.length > 0) {
this.setState({
selectedTag: null,
});
}
}
public triggerNewTagBlur() { public triggerNewTagBlur() {
if (this.inputRef.current) { if (this.inputRef.current) {
this.inputRef.current.blur(); this.inputRef.current.blur();
@ -188,17 +242,11 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
private onEditTag = (tag: ITag) => { private onEditTag = (tag: ITag) => {
const { editingTag } = this.state; const tagOperation = this.state.tagOperation === TagOperationMode.Rename
const newEditingTag = (editingTag && this.isNameEqual(editingTag, tag)) ? null : tag; ? TagOperationMode.None : TagOperationMode.Rename;
this.setState({ this.setState({
editingTag: newEditingTag, tagOperation,
editingTagNode: this.getTagNode(newEditingTag),
}); });
if (this.state.clickedColor) {
this.setState({
showColorPicker: !this.state.showColorPicker,
});
}
} }
private onLockTag = (tag: ITag) => { private onLockTag = (tag: ITag) => {
@ -232,19 +280,16 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
private handleColorChange = (color: string) => { private handleColorChange = (color: string) => {
const tag = this.state.editingTag; const tag = this.state.selectedTag;
const tags = this.state.tags.map((t) => { const tags = this.state.tags.map((t) => {
return (this.isNameEqual(t, tag)) ? { return (this.isNameEqual(t, tag)) ? {
name: t.name, ...tag,
color, color,
type: t.type,
format: t.format,
} : t; } : t;
}); });
this.setState({ this.setState({
tags, tags,
editingTag: null, tagOperation: TagOperationMode.None,
showColorPicker: false,
}, () => this.props.onChange(tags)); }, () => this.props.onChange(tags));
} }
@ -288,7 +333,6 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}); });
this.setState({ this.setState({
tags, tags,
editingTag: null,
selectedTag: newTag, selectedTag: newTag,
}, () => { }, () => {
this.props.onChange(tags); this.props.onChange(tags);
@ -303,16 +347,18 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
private getColorPickerPortal = () => { private getColorPickerPortal = () => {
const { selectedTag } = this.state;
const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker;
return ( return (
<AlignPortal align={this.getColorAlignConfig()} target={this.getEditingTagNode}> <AlignPortal align={this.getColorAlignConfig()} target={this.getSelectedTagNode}>
<div className="tag-input-portal"> <div className="tag-input-portal">
{ {
this.state.showColorPicker && showColorPicker &&
<ColorPicker <ColorPicker
color={this.state.editingTag && this.state.editingTag.color} color={selectedTag && selectedTag.color}
colors={tagColors} colors={tagColors}
onEditColor={this.handleColorChange} onEditColor={this.handleColorChange}
show={this.state.showColorPicker} show={showColorPicker}
/> />
} }
</div> </div>
@ -320,25 +366,8 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
); );
} }
private getTagFieldPortal = () => {
return (
<Align align={this.getFieldAlignConfig()} target={this.getEditingTagNameNode} monitorWindowResize={true}>
<div className="tag-input-portal">
{
this.state.showDropDown &&
<TagContextMenu
key={this.state.editingTag.name}
tag={this.state.editingTag}
onChange={this.props.onTagChanged}
/>
}
</div>
</Align>
);
}
private getColorAlignConfig = () => { private getColorAlignConfig = () => {
const coords = this.getEditingTagCoords(); const coords = this.colorPickerNode.getBoundingClientRect();
const isNearBottom = coords && coords.top > (window.innerHeight / 2); const isNearBottom = coords && coords.top > (window.innerHeight / 2);
const alignCorner = isNearBottom ? "b" : "t"; const alignCorner = isNearBottom ? "b" : "t";
const verticalOffset = isNearBottom ? 6 : -6; const verticalOffset = isNearBottom ? 6 : -6;
@ -350,28 +379,8 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}; };
} }
private getFieldAlignConfig = () => { private getSelectedTagNode = () => {
return { return this.getTagNode(this.state.selectedTag);
// Align top right of source node (dropdown) with top left of target node (tag name row)
points: ["tr", "br"],
// Offset source node by 0px in x and 3px in y
offset: [0, 3],
// Auto adjust position when source node is overflowed
overflow: {adjustX: true, adjustY: true},
};
}
private getEditingTagCoords = () => {
const node = this.state.editingTagNode;
return (node) ? node.getBoundingClientRect() : null;
}
private getEditingTagNode = () => {
return this.state.editingTagNode || document;
}
private getEditingTagNameNode = () => {
return TagInputItem.getNameNode(this.state.editingTagNode) || document;
} }
private renderTagItems = () => { private renderTagItems = () => {
@ -385,23 +394,16 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
return props.map((prop) => return props.map((prop) =>
<TagInputItem <TagInputItem
{...prop}
key={prop.tag.name} key={prop.tag.name}
labels={this.setTagLabels(prop.tag.name)} labels={this.setTagLabels(prop.tag.name)}
ref={(item) => this.setTagItemRef(item, prop.tag)} ref={(item) => this.setTagItemRef(item, prop.tag)}
onLabelEnter={this.props.onLabelEnter} onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave} onLabelLeave={this.props.onLabelLeave}
onTagChanged={this.props.onTagChanged} onTagChanged={this.props.onTagChanged}
onCallDropDown = {this.handleTagItemDropDown}
{...prop}
/>); />);
} }
private handleTagItemDropDown = () => {
this.setState((prevState) => ({
showDropDown: !prevState.showDropDown,
}));
}
private setTagItemRef = (item: TagInputItem, tag: ITag) => { private setTagItemRef = (item: TagInputItem, tag: ITag) => {
this.tagItemRefs.set(tag.name, item); this.tagItemRefs.set(tag.name, item);
return item; return item;
@ -412,19 +414,20 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
private createTagItemProps = (): ITagInputItemProps[] => { private createTagItemProps = (): ITagInputItemProps[] => {
const tags = this.state.tags; const { tags, selectedTag, tagOperation } = this.state;
const selectedRegionTagSet = this.getSelectedRegionTagSet(); const selectedRegionTagSet = this.getSelectedRegionTagSet();
return tags.map((tag) => ( return tags.map((tag) => (
{ {
tag, tag,
index: tags.findIndex((t) => this.isNameEqual(t, tag)), index: tags.findIndex((t) => this.isNameEqual(t, tag)),
isLocked: this.props.lockedTags && isLocked: this.props.lockedTags
this.props.lockedTags.findIndex((str) => this.isNameEqualTo(tag, str)) > -1, && this.props.lockedTags.findIndex((str) => this.isNameEqualTo(tag, str)) > -1,
isBeingEdited: this.state.editingTag && this.isNameEqual(this.state.editingTag, tag), isRenaming: selectedTag && this.isNameEqual(selectedTag, tag)
isSelected: this.state.selectedTag && this.isNameEqual(this.state.selectedTag, tag), && tagOperation === TagOperationMode.Rename,
isSelected: selectedTag && this.isNameEqual(this.state.selectedTag, tag),
appliedToSelectedRegions: selectedRegionTagSet.has(tag.name), appliedToSelectedRegions: selectedRegionTagSet.has(tag.name),
onClick: this.handleClick, onClick: this.onTagItemClick,
onChange: this.updateTag, onChange: this.updateTag,
} as ITagInputItemProps } as ITagInputItemProps
)); ));
@ -442,61 +445,54 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
return result; return result;
} }
private onAltClick = (tag: ITag) => { private onTagItemClick = (tag: ITag, props: ITagClickProps) => {
const { editingTag } = this.state;
const newEditingTag = this.state.showDropDown && editingTag && this.isNameEqual(editingTag, tag) ? null : tag;
this.setState({
editingTag: newEditingTag,
editingTagNode: this.getTagNode(newEditingTag),
});
}
private onSingleClick = (tag: ITag, clickedColor: boolean, clickedDropDown: boolean) => {
const { editingTag, selectedTag } = this.state;
const newEditingTag = this.state.showDropDown && editingTag && this.isNameEqual(editingTag, tag) ? null : tag;
this.setState({
editingTag: newEditingTag,
editingTagNode: this.getTagNode(newEditingTag),
clickedColor,
clickedDropDown,
showColorPicker: !this.state.showColorPicker && clickedColor,
showDropDown: !this.state.showDropDown && clickedDropDown,
selectedTag: clickedDropDown ? tag : selectedTag,
});
}
private handleClick = (tag: ITag, props: ITagClickProps) => {
if (props.ctrlKey && this.props.onCtrlTagClick) { // Lock tags if (props.ctrlKey && this.props.onCtrlTagClick) { // Lock tags
this.props.onCtrlTagClick(tag); this.props.onCtrlTagClick(tag);
this.setState({ clickedColor: props.clickedColor, clickedDropDown: props.clickedDropDown });
} else if (props.altKey) { // Edit tag } else if (props.altKey) { // Edit tag
this.onAltClick(tag);
} else if (props.keyClick) {
this.onSingleClick(tag, props.clickedColor, props.clickedDropDown);
} else { // Select tag
const { editingTag, selectedTag } = this.state;
const inEditMode = editingTag && this.isNameEqual(editingTag, tag);
const alreadySelected = selectedTag && this.isNameEqual(selectedTag, tag);
const newEditingTag = inEditMode ? null : editingTag;
this.setState({ this.setState({
editingTag: newEditingTag, selectedTag: tag,
editingTagNode: this.getTagNode(newEditingTag), tagOperation: TagOperationMode.Rename,
selectedTag: (alreadySelected && !inEditMode) ? null : tag,
clickedColor: props.clickedColor,
clickedDropDown: props.clickedDropDown,
showColorPicker: false,
showDropDown: false,
}); });
} else if (props.clickedDropDown) {
const { selectedTag } = this.state;
const showContextualMenu = !selectedTag || !this.isNameEqual(selectedTag, tag)
|| this.state.tagOperation !== TagOperationMode.ContextualMenu;
const tagOperation = showContextualMenu ? TagOperationMode.ContextualMenu : TagOperationMode.None;
this.setState({
selectedTag: tag,
tagOperation,
});
} else if (props.clickedColor) {
const { selectedTag, tagOperation } = this.state;
const showColorPicker = tagOperation !== TagOperationMode.ColorPicker;
const newTagOperation = showColorPicker ? TagOperationMode.ColorPicker : TagOperationMode.None;
if (showColorPicker) {
this.colorPickerNode = this.getTagNode(tag);
}
this.setState({
selectedTag: showColorPicker ? tag : selectedTag,
tagOperation: newTagOperation,
});
} else { // Select tag
const { selectedTag, tagOperation: oldTagOperation } = this.state;
const selected = selectedTag && this.isNameEqual(selectedTag, tag);
const tagOperation = selected ? oldTagOperation : TagOperationMode.None;
let deselect = selected && oldTagOperation === TagOperationMode.None;
// Only fire click event if a region is selected // Only fire click event if a region is selected
if (this.props.selectedRegions && if (this.props.selectedRegions &&
this.props.selectedRegions.length > 0 && this.props.selectedRegions.length > 0 &&
this.props.onTagClick && this.props.onTagClick) {
!inEditMode) { deselect = false;
this.props.onTagClick(tag); this.props.onTagClick(tag);
} }
}
this.setState({
selectedTag: deselect ? null : tag,
tagOperation,
});
}
} }
private onSearchKeyDown = (event: KeyboardEvent): void => { private onSearchKeyDown = (event: KeyboardEvent): void => {
@ -585,4 +581,107 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
private isNameEqualTo = (tag: ITag, str: string) => { private isNameEqualTo = (tag: ITag, str: string) => {
return tag.name.trim().toLocaleLowerCase() === str.trim().toLocaleLowerCase(); return tag.name.trim().toLocaleLowerCase() === str.trim().toLocaleLowerCase();
} }
private onHideContextualMenu = () => {
this.setState({tagOperation: TagOperationMode.None});
}
private getContextualMenuItems = (): IContextualMenuItem[] => {
const tag = this.state.selectedTag;
if (!tag) {
return [];
}
const menuItems: IContextualMenuItem[] = [
{
key: "type",
iconProps: {
iconName: "Link",
},
text: tag.type ? tag.type : strings.tags.toolbar.type,
subMenuProps: {
items: this.getTypeSubMenuItems(),
},
},
{
key: "format",
iconProps: {
iconName: "Link",
},
text: tag.format ? tag.format : strings.tags.toolbar.format,
subMenuProps: {
items: this.getFormatSubMenuItems(),
},
},
];
return menuItems;
}
private getTypeSubMenuItems = (): IContextualMenuItem[] => {
const tag = this.state.selectedTag;
const types = Object.values(FieldType);
return types.map((type) => {
return {
key: type,
text: type,
canCheck: true,
isChecked: type === tag.type,
onClick: this.onTypeSelect,
} as IContextualMenuItem;
});
}
private getFormatSubMenuItems = (): IContextualMenuItem[] => {
const tag = this.state.selectedTag;
const formats = filterFormat(tag.type);
return formats.map((format) => {
return {
key: format,
text: format,
canCheck: true,
isChecked: format === tag.format,
onClick: this.onFormatSelect,
} as IContextualMenuItem;
});
}
private onTypeSelect = (event: React.MouseEvent<HTMLButtonElement>, item?: IContextualMenuItem): void => {
event.preventDefault();
const type = item.text as FieldType;
const tag = this.state.selectedTag;
if (type === tag.type) {
return;
}
const newTag = {
...tag,
type,
format: FieldFormat.NotSpecified,
};
if (this.props.onTagChanged) {
this.props.onTagChanged(tag, newTag);
}
}
private onFormatSelect = (event: React.MouseEvent<HTMLButtonElement>, item?: IContextualMenuItem): void => {
event.preventDefault();
const format = item.text as FieldFormat;
const tag = this.state.selectedTag;
if (format === tag.format) {
return;
}
const newTag = {
...tag,
format,
};
if (this.props.onTagChanged) {
this.props.onTagChanged(tag, newTag);
}
}
} }

Просмотреть файл

@ -13,7 +13,7 @@ describe("Tag Input Item", () => {
tag: MockFactory.createTestTag(), tag: MockFactory.createTestTag(),
index: 0, index: 0,
labels: [], labels: [],
isBeingEdited: false, isRenaming: false,
isLocked: false, isLocked: false,
isSelected: false, isSelected: false,
appliedToSelectedRegions: false, appliedToSelectedRegions: false,
@ -21,7 +21,6 @@ describe("Tag Input Item", () => {
onChange: jest.fn(), onChange: jest.fn(),
onLabelEnter: jest.fn(), onLabelEnter: jest.fn(),
onLabelLeave: jest.fn(), onLabelLeave: jest.fn(),
onCallDropDown: jest.fn(),
}; };
} }

Просмотреть файл

@ -7,12 +7,6 @@ import { ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applica
import { strings } from "../../../../common/strings"; import { strings } from "../../../../common/strings";
import TagInputItemLabel from "./tagInputItemLabel"; import TagInputItemLabel from "./tagInputItemLabel";
export enum TagEditMode {
Color = "color",
Name = "name",
Dropdown = "inputField",
}
export interface ITagClickProps { export interface ITagClickProps {
ctrlKey?: boolean; ctrlKey?: boolean;
altKey?: boolean; altKey?: boolean;
@ -31,8 +25,8 @@ export interface ITagInputItemProps {
index: number; index: number;
/** Labels owned by the tag */ /** Labels owned by the tag */
labels: ILabel[]; labels: ILabel[];
/** Tag is currently being edited */ /** Tag is currently renaming */
isBeingEdited: boolean; isRenaming: boolean;
/** Tag is currently locked for application */ /** Tag is currently locked for application */
isLocked: boolean; isLocked: boolean;
/** Tag is currently selected */ /** Tag is currently selected */
@ -46,39 +40,30 @@ export interface ITagInputItemProps {
onLabelEnter: (label: ILabel) => void; onLabelEnter: (label: ILabel) => void;
onLabelLeave: (label: ILabel) => void; onLabelLeave: (label: ILabel) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
onCallDropDown: () => void;
} }
export interface ITagInputItemState { export interface ITagInputItemState {
/** Tag is currently being edited */ /** Tag is currently renaming */
isBeingEdited: boolean; isRenaming: boolean;
/** Tag is currently locked for application */ /** Tag is currently locked for application */
isLocked: boolean; isLocked: boolean;
/** Mode of tag editing (text or color) */
tagEditMode: TagEditMode;
} }
export default class TagInputItem extends React.Component<ITagInputItemProps, ITagInputItemState> { export default class TagInputItem extends React.Component<ITagInputItemProps, ITagInputItemState> {
public static getNameNode(tagNode: Element): Element | undefined {
if (tagNode) {
return tagNode.getElementsByClassName(TagInputItem.TAG_NAME_CLASS_NAME)[0];
}
return undefined;
}
private static TAG_NAME_CLASS_NAME = "tag-item";
public state: ITagInputItemState = { public state: ITagInputItemState = {
isBeingEdited: false, isRenaming: false,
isLocked: false, isLocked: false,
tagEditMode: null,
}; };
private itemRef = React.createRef<HTMLDivElement>();
public render() { public render() {
const style: any = { const style: any = {
background: this.props.tag.color, background: this.props.tag.color,
}; };
return ( return (
<div className={"tag-item-block"}> <div className={"tag-item-block"}>
<div <div
@ -89,7 +74,10 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
<div className={"tag-item-block-2"}> <div className={"tag-item-block-2"}>
{ {
this.props.tag && this.props.tag &&
<div className={this.getItemClassName()} style={style}> <div
ref={this.itemRef}
className={this.getItemClassName()}
style={style}>
<div <div
className={"tag-content pr-2"} className={"tag-content pr-2"}
onClick={this.onNameClick}> onClick={this.onNameClick}>
@ -108,9 +96,9 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
} }
public componentDidUpdate(prevProps: ITagInputItemProps) { public componentDidUpdate(prevProps: ITagInputItemProps) {
if (prevProps.isBeingEdited !== this.props.isBeingEdited) { if (prevProps.isRenaming !== this.props.isRenaming) {
this.setState({ this.setState({
isBeingEdited: this.props.isBeingEdited, isRenaming: this.props.isRenaming,
}); });
} }
@ -121,21 +109,24 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
} }
} }
private onInputFieldClick = (e: any) => { public getTagNameRef() {
return this.itemRef;
}
private onDropdownClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
this.setState({
tagEditMode: TagEditMode.Dropdown, const clickedDropDown = true;
}, () => this.props.onClick(this.props.tag, { keyClick: true, clickedDropDown: true })); this.props.onClick(this.props.tag, { clickedDropDown });
} }
private onColorClick = (e: MouseEvent) => { private onColorClick = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
const ctrlKey = e.ctrlKey || e.metaKey; const ctrlKey = e.ctrlKey || e.metaKey;
const keyClick = (e.type === "click"); const altKey = e.altKey;
this.setState({ const clickedColor = true;
tagEditMode: TagEditMode.Color, this.props.onClick(this.props.tag, { ctrlKey, altKey, clickedColor });
}, () => this.props.onClick(this.props.tag, { ctrlKey, keyClick, clickedColor: true }));
} }
private onNameClick = (e: MouseEvent) => { private onNameClick = (e: MouseEvent) => {
@ -143,13 +134,11 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
const ctrlKey = e.ctrlKey || e.metaKey; const ctrlKey = e.ctrlKey || e.metaKey;
const altKey = e.altKey; const altKey = e.altKey;
this.setState({ this.props.onClick(this.props.tag, { ctrlKey, altKey });
tagEditMode: TagEditMode.Name,
}, () => this.props.onClick(this.props.tag, { ctrlKey, altKey }));
} }
private getItemClassName = () => { private getItemClassName = () => {
const classNames = [TagInputItem.TAG_NAME_CLASS_NAME]; const classNames = ["tag-item"];
if (this.props.isSelected) { if (this.props.isSelected) {
classNames.push("tag-item-selected"); classNames.push("tag-item-selected");
} }
@ -169,19 +158,19 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
} }
<div className="tag-name-body"> <div className="tag-name-body">
{ {
(this.state.isBeingEdited && this.state.tagEditMode === TagEditMode.Name) this.state.isRenaming
? ?
<input <input
className={`tag-name-editor ${this.getContentClassName()}`} className={`tag-name-editor ${this.getContentClassName()}`}
type="text" type="text"
defaultValue={this.props.tag.name} defaultValue={this.props.tag.name}
onKeyDown={(e) => this.handleNameEdit(e)} onKeyDown={(e) => this.handleNameEdit(e)}
autoFocus={true} autoFocus={true}
/> />
: :
<span title={this.props.tag.name} className={this.getContentClassName()}> <span title={this.props.tag.name} className={this.getContentClassName()}>
{this.props.tag.name} {this.props.tag.name}
</span> </span>
} }
</div> </div>
<div className={"tag-icons-container"}> <div className={"tag-icons-container"}>
@ -196,7 +185,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
ariaLabel={strings.tags.toolbar.contextualMenu} ariaLabel={strings.tags.toolbar.contextualMenu}
className="tag-input-toolbar-iconbutton ml-2" className="tag-input-toolbar-iconbutton ml-2"
iconProps={{iconName: "ChevronDown"}} iconProps={{iconName: "ChevronDown"}}
onClick={this.onInputFieldClick} /> onClick={this.onDropdownClick} />
</div> </div>
</div> </div>
); );
@ -221,16 +210,13 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
}); });
} else if (e.key === "Escape") { } else if (e.key === "Escape") {
this.setState({ this.setState({
isBeingEdited: false, isRenaming: false,
}); });
} }
} }
private getContentClassName = () => { private getContentClassName = () => {
const classNames = ["tag-name-text px-2 pb-1"]; const classNames = ["tag-name-text px-2 pb-1"];
if (this.state.isBeingEdited && this.state.tagEditMode === TagEditMode.Color) {
classNames.push("tag-color-edit");
}
if (this.isTypeOrFormatSpecified()) { if (this.isTypeOrFormatSpecified()) {
classNames.push("tag-name-text-typed"); classNames.push("tag-name-text-typed");
} }
@ -243,7 +229,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
return (displayIndex < 10) ? displayIndex : null; return (displayIndex < 10) ? displayIndex : null;
} }
private isTypeOrFormatSpecified() { private isTypeOrFormatSpecified = () => {
const {tag} = this.props; const {tag} = this.props;
return (tag.type && tag.type !== FieldType.String) || return (tag.type && tag.type !== FieldType.String) ||
(tag.format && tag.format !== FieldFormat.NotSpecified); (tag.format && tag.format !== FieldFormat.NotSpecified);

Просмотреть файл

@ -6,4 +6,5 @@ $tagColorWidth: 8px;
$tagLinkWidth: 22px; $tagLinkWidth: 22px;
$tagItemWidth: $tagInputWidth - $tagColorWidth; $tagItemWidth: $tagInputWidth - $tagColorWidth;
$tagTextWidth: $tagItemWidth - 55px; $tagTextWidth: $tagItemWidth - 55px;
$tagTextLinkedWidth: $tagTextWidth - $tagLinkWidth; $tagTextLinkedWidth: $tagTextWidth - $tagLinkWidth;
$tagContextualMenuWidth: $tagItemWidth - 8px;

Просмотреть файл

@ -19,6 +19,7 @@ export function registerIcons() {
Settings: "\uE713", Settings: "\uE713",
Link: "\uE71B", Link: "\uE71B",
Search: "\uE721", Search: "\uE721",
CheckMark: "\uE73E",
Up: "\uE74A", Up: "\uE74A",
Down: "\uE74B", Down: "\uE74B",
Delete: "\uE74D", Delete: "\uE74D",