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:
Родитель
d1f6f5a186
Коммит
5c9fa5de3b
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче