azure-devops-ui
Version:
React components for building web UI in Azure DevOps
219 lines (218 loc) • 12.9 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import * as React from "react";
import * as Resources from '../../Resources.Label';
import { WrappingBehavior } from "./LabelGroup.Props";
import { LabelGroup } from "./LabelGroup";
import { css, getSafeId, KeyCode } from '../../Util';
import { ObservableValue, ObservableArray, ObservableLike } from '../../Core/Observable';
import { Autocomplete } from "../Autocomplete/Autocomplete";
import { Button } from '../../Button';
import { Icon } from '../../Icon';
import { Observer } from '../../Observer';
export class EditableLabelGroup extends React.Component {
constructor(props) {
super(props);
this.autocompleteRef = React.createRef();
this.autocompleteValue = new ObservableValue("");
// Using a boolean instead of stopPropagation here so that people get the events up the DOM tree
// if they have onMouseDown bound on wrappers etc.
this.isMouseDownEventHandled = false;
this.labelGroupRef = React.createRef();
this.renderTitleIconFlag = new ObservableValue(false);
this.selectedLabelContents = new ObservableArray();
this.filterLabelModelAgainstContents = (testModel) => {
return ObservableLike.getValue(this.props.labelProps).filter(y => y.content.toUpperCase() === testModel.content.toUpperCase()).length == 0;
};
this.onAddButtonClicked = () => {
this.setState({ isInEditMode: true }, () => this.autocompleteRef.current.focus());
};
this.onAutocompleteFocus = (event) => {
this.selectedLabelContents.removeAll();
};
this.onAutocompleteValueChange = (newValue) => {
this.autocompleteValue.value = newValue;
};
this.onBlur = (event) => {
/**
* Delay the toggle off; _onFocus will clear the timeout
*/
this.focusOutTimeoutId = setTimeout(() => {
this.selectedLabelContents.removeAll();
this.autocompleteValue.value = "";
this.setState({ isInEditMode: false });
this.props.onBlur && this.props.onBlur();
}, 0); /**/
};
this.onCheckForDuplicates = (newInput) => {
return (ObservableLike.getValue(this.props.labelProps).filter((testModel) => testModel.content.trim().toUpperCase() === newInput.trim().toUpperCase()).length !== 0);
};
// Only for handling keyboard focus
this.onFocus = (event) => {
clearTimeout(this.focusOutTimeoutId);
if (!this.state.isInEditMode) {
const callbackFunction = () => {
this.renderTitleIconFlag.value = false;
this.autocompleteRef.current.focus();
};
this.setState({ isInEditMode: true }, callbackFunction);
}
};
this.onGetSuggestions = (content) => {
if (content === "" || !this.props.getSuggestedLabels) {
return new Promise(resolve => resolve([]));
}
const labelModels = this.props.getSuggestedLabels(content);
return Promise.resolve(labelModels).then((suggestions) => suggestions.filter(this.filterLabelModelAgainstContents));
};
this.onInnerMouseDOwn = (event) => {
// Eat the event; allow focus to move and signal that the outer wrapper shouldn't handle it
if (this.state.isInEditMode) {
this.isMouseDownEventHandled = true;
}
};
this.onInputKeyDown = (event) => {
const inputValue = this.autocompleteValue.value;
if (event.which === KeyCode.backspace || event.which === KeyCode.delete) {
if (this.props.labelProps.length > 0 && inputValue === "") {
// Make sure we have an element to remove
this.removeLabel(ObservableLike.getValue(this.props.labelProps)[this.props.labelProps.length - 1].content);
event.preventDefault();
}
}
};
this.onInputSubmit = (labelModel) => {
this.props.onLabelSubmit && this.props.onLabelSubmit(labelModel);
this.autocompleteValue.value = "";
this.autocompleteRef.current.focus();
};
this.onLabelKeyDown = (event, model, index) => {
this.props.onLabelKeyDown && this.props.onLabelKeyDown(event, model, index);
if (event.isDefaultPrevented()) {
return;
}
const selectedOrFocusedContents = new Set([...this.selectedLabelContents.value, model.content]);
if (event.which === KeyCode.backspace || event.which === KeyCode.delete) {
let nextLabelFocusIndex = -1;
let minSelectedIndex = 99999;
selectedOrFocusedContents.forEach((content) => {
const index = ObservableLike.getValue(this.props.labelProps).findIndex((testModel) => testModel.content === content);
minSelectedIndex = Math.min(minSelectedIndex, index);
});
// If it's 0, we need to focus the first unselected item in the group
if (minSelectedIndex === 0) {
const selectedNextContent = new Set(selectedOrFocusedContents);
nextLabelFocusIndex = ObservableLike.getValue(this.props.labelProps).findIndex((model) => !selectedNextContent.has(model.content));
}
else {
// Otherwise focus the one before the earliest deleted
nextLabelFocusIndex = minSelectedIndex - 1;
}
// If we didn't select a new element, focus the autocomplete
if (nextLabelFocusIndex === -1) {
this.autocompleteRef.current.focus();
}
else {
// Otherwise set a timeout
this.labelGroupRef.current.focusLabel(nextLabelFocusIndex);
}
selectedOrFocusedContents.forEach((content) => this.removeLabel(content));
this.selectedLabelContents.removeAll();
}
else if (event.which === KeyCode.upArrow ||
event.which === KeyCode.downArrow ||
event.which === KeyCode.leftArrow ||
event.which === KeyCode.rightArrow) {
this.selectedLabelContents.removeAll();
}
};
this.onLabelMouseDown = (event, model, index) => {
this.props.onLabelMouseDown && this.props.onLabelMouseDown(event, model, index);
if (event.isDefaultPrevented() || !this.state.isInEditMode) {
return;
}
const selectedContentSet = new Set(this.selectedLabelContents.value);
// Handle multiselect
if (event.ctrlKey) {
if (selectedContentSet.has(model.content)) {
this.selectedLabelContents.removeAll((value) => value === model.content);
}
else {
this.selectedLabelContents.push(model.content);
}
}
else {
// Single Select
this.selectedLabelContents.value = selectedContentSet.has(model.content) ? [] : [model.content];
}
};
this.onOuterMouseDown = (event) => {
// If the inner container handled it, reset and ignore the event
if (this.isMouseDownEventHandled) {
this.isMouseDownEventHandled = false;
return;
}
// Only go into edit mode if we're not already there
if (!this.state.isInEditMode) {
// Don't deal with mouse-related focus, we'll manage it manually
event.preventDefault();
// Focus move here is only good if we didn't eat the event in _innerMouseDown
// meaning the click was truly on the OUTER containers
const callbackFunction = () => {
this.renderTitleIconFlag.value = false;
this.autocompleteRef.current.focus();
};
this.setState({ isInEditMode: true }, callbackFunction);
}
};
this.onWrapperKeyDown = (event) => {
if (event.which === KeyCode.escape) {
this.selectedLabelContents.removeAll();
this.autocompleteValue.value = "";
this.setState({ isInEditMode: false });
this.props.onBlur && this.props.onBlur();
event.preventDefault();
}
};
this.groupId = "editable-label-group-" + EditableLabelGroup.editableGroupCount;
this.state = {
isInEditMode: false
};
}
render() {
const { addButtonText = Resources.AddLabelButtonText, className, customColors, disableColorPicker = false, disableMouseFocusOnLabels = false, duplicateLabelText, enableHoverStyles = true, labelProps, onLabelClick, shrinkToContents = false, title, useBlankZeroData = false, watermark = Resources.AddLabelWatermark } = this.props;
const { isInEditMode } = this.state;
const editClassName = isInEditMode ? "edit" : "";
const actualWrappingBehavior = isInEditMode ? WrappingBehavior.freeFlow : this.props.wrappingBehavior;
let bodyContent;
if (labelProps.length > 0 || isInEditMode) {
bodyContent = (React.createElement("div", { className: css("bolt-labelgroup--editableWrapper", editClassName, !shrinkToContents && "default-padding"), onKeyDown: this.onWrapperKeyDown, onMouseDown: this.onInnerMouseDOwn },
React.createElement(LabelGroup, Object.assign({ ref: this.labelGroupRef }, this.props, { className: undefined, id: getSafeId(this.groupId), onLabelMouseDown: this.onLabelMouseDown, onLabelKeyDown: this.onLabelKeyDown, onLabelClick: onLabelClick, selectedLabelContents: this.selectedLabelContents, title: undefined, disableMouseFocusOnLabels: disableMouseFocusOnLabels, enableHoverStyles: enableHoverStyles, wrappingBehavior: actualWrappingBehavior }), isInEditMode && (React.createElement(Autocomplete, { ref: this.autocompleteRef, suggestionProvider: this.onGetSuggestions, value: this.autocompleteValue, placeholder: watermark, onKeyDown: this.onInputKeyDown, onSubmit: this.onInputSubmit, onFocus: this.onAutocompleteFocus, onInputValueChange: this.onAutocompleteValueChange, customColors: customColors, onCheckForDuplicateInParent: this.onCheckForDuplicates, onDuplicateInParentText: duplicateLabelText, disableColorPicker: disableColorPicker, ariaDescribedBy: getSafeId(this.groupId) })))));
}
else if (!useBlankZeroData) {
bodyContent = (React.createElement(Button, { className: "bolt-labelgroup-addButton", text: addButtonText, iconProps: { iconName: "Add" }, onClick: this.onAddButtonClicked }));
}
else {
// If we have zero data and are using the blank one, render nothing at all
return null;
}
return (React.createElement("div", { className: css(className, "bolt-labelgroup-editable flex-column", editClassName), onFocus: this.onFocus, onBlur: this.onBlur, onMouseDown: this.onOuterMouseDown, onMouseEnter: () => (this.renderTitleIconFlag.value = isInEditMode ? false : true), onMouseLeave: () => (this.renderTitleIconFlag.value = false) },
title && (React.createElement(Observer, { renderIconFlag: this.renderTitleIconFlag }, (observerProps) => (React.createElement("div", { className: "bolt-labelgroup-title-wrapper body-m flex-row" },
React.createElement("div", { className: "bolt-labelgroup-title-content" }, title),
observerProps.renderIconFlag && React.createElement(Icon, { iconName: "Edit" }))))),
bodyContent));
}
componentWillUnmount() {
// Niche race condition, but let's not setstate on an unmounted component
clearTimeout(this.focusOutTimeoutId);
}
focus() {
this.setState({ isInEditMode: true }, () => this.autocompleteRef.current && this.autocompleteRef.current.focus());
}
removeLabel(content) {
this.props.onLabelRemove &&
this.props.onLabelRemove(ObservableLike.getValue(this.props.labelProps).find((testModel) => testModel.content === content));
this.selectedLabelContents.removeAll(x => x === content);
}
}
EditableLabelGroup.editableGroupCount = 0;