UNPKG

14 kBJavaScriptView Raw
1/*
2 * Copyright 2017 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import { __assign, __decorate, __extends, __spreadArrays } from "tslib";
17import classNames from "classnames";
18import * as React from "react";
19import { polyfill } from "react-lifecycles-compat";
20import { AbstractPureComponent2, Classes, Keys, refHandler, setRef, Utils } from "../../common";
21import { DISPLAYNAME_PREFIX } from "../../common/props";
22import { Icon, IconSize } from "../icon/icon";
23import { Tag } from "../tag/tag";
24/** special value for absence of active tag */
25var NONE = -1;
26var TagInput = /** @class */ (function (_super) {
27 __extends(TagInput, _super);
28 function TagInput() {
29 var _this = _super !== null && _super.apply(this, arguments) || this;
30 _this.state = {
31 activeIndex: NONE,
32 inputValue: _this.props.inputValue || "",
33 isInputFocused: false,
34 };
35 _this.inputElement = null;
36 _this.handleRef = refHandler(_this, "inputElement", _this.props.inputRef);
37 _this.addTags = function (value, method) {
38 if (method === void 0) { method = "default"; }
39 var _a = _this.props, inputValue = _a.inputValue, onAdd = _a.onAdd, onChange = _a.onChange, values = _a.values;
40 var newValues = _this.getValues(value);
41 var shouldClearInput = (onAdd === null || onAdd === void 0 ? void 0 : onAdd(newValues, method)) !== false && inputValue === undefined;
42 // avoid a potentially expensive computation if this prop is omitted
43 if (Utils.isFunction(onChange)) {
44 shouldClearInput = onChange(__spreadArrays(values, newValues)) !== false && shouldClearInput;
45 }
46 // only explicit return false cancels text clearing
47 if (shouldClearInput) {
48 _this.setState({ inputValue: "" });
49 }
50 };
51 _this.maybeRenderTag = function (tag, index) {
52 if (!tag) {
53 return null;
54 }
55 var _a = _this.props, large = _a.large, tagProps = _a.tagProps;
56 var props = Utils.isFunction(tagProps) ? tagProps(tag, index) : tagProps;
57 return (React.createElement(Tag, __assign({ active: index === _this.state.activeIndex, "data-tag-index": index, key: tag + "__" + index, large: large, onRemove: _this.props.disabled ? undefined : _this.handleRemoveTag }, props), tag));
58 };
59 _this.handleContainerClick = function () {
60 var _a;
61 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.focus();
62 };
63 _this.handleContainerBlur = function (_a) {
64 var currentTarget = _a.currentTarget;
65 _this.requestAnimationFrame(function () {
66 // we only care if the blur event is leaving the container.
67 // defer this check using rAF so activeElement will have updated.
68 if (!currentTarget.contains(document.activeElement)) {
69 if (_this.props.addOnBlur && _this.state.inputValue !== undefined && _this.state.inputValue.length > 0) {
70 _this.addTags(_this.state.inputValue, "blur");
71 }
72 _this.setState({ activeIndex: NONE, isInputFocused: false });
73 }
74 });
75 };
76 _this.handleInputFocus = function (event) {
77 var _a, _b;
78 _this.setState({ isInputFocused: true });
79 (_b = (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, event);
80 };
81 _this.handleInputChange = function (event) {
82 var _a, _b, _c, _d;
83 _this.setState({ activeIndex: NONE, inputValue: event.currentTarget.value });
84 (_b = (_a = _this.props).onInputChange) === null || _b === void 0 ? void 0 : _b.call(_a, event);
85 (_d = (_c = _this.props.inputProps) === null || _c === void 0 ? void 0 : _c.onChange) === null || _d === void 0 ? void 0 : _d.call(_c, event);
86 };
87 _this.handleInputKeyDown = function (event) {
88 // HACKHACK: https://github.com/palantir/blueprint/issues/4165
89 /* eslint-disable deprecation/deprecation */
90 var _a = event.currentTarget, selectionEnd = _a.selectionEnd, value = _a.value;
91 var activeIndex = _this.state.activeIndex;
92 var activeIndexToEmit = activeIndex;
93 if (event.which === Keys.ENTER && value.length > 0) {
94 _this.addTags(value, "default");
95 }
96 else if (selectionEnd === 0 && _this.props.values.length > 0) {
97 // cursor at beginning of input allows interaction with tags.
98 // use selectionEnd to verify cursor position and no text selection.
99 if (event.which === Keys.ARROW_LEFT || event.which === Keys.ARROW_RIGHT) {
100 var nextActiveIndex = _this.getNextActiveIndex(event.which === Keys.ARROW_RIGHT ? 1 : -1);
101 if (nextActiveIndex !== activeIndex) {
102 event.stopPropagation();
103 activeIndexToEmit = nextActiveIndex;
104 _this.setState({ activeIndex: nextActiveIndex });
105 }
106 }
107 else if (event.which === Keys.BACKSPACE) {
108 _this.handleBackspaceToRemove(event);
109 }
110 else if (event.which === Keys.DELETE) {
111 _this.handleDeleteToRemove(event);
112 }
113 }
114 _this.invokeKeyPressCallback("onKeyDown", event, activeIndexToEmit);
115 };
116 _this.handleInputKeyUp = function (event) {
117 _this.invokeKeyPressCallback("onKeyUp", event, _this.state.activeIndex);
118 };
119 _this.handleInputPaste = function (event) {
120 var separator = _this.props.separator;
121 var value = event.clipboardData.getData("text");
122 if (!_this.props.addOnPaste || value.length === 0) {
123 return;
124 }
125 // special case as a UX nicety: if the user pasted only one value with no delimiters in it, leave that value in
126 // the input field so that the user can refine it before converting it to a tag manually.
127 if (separator === false || value.split(separator).length === 1) {
128 return;
129 }
130 event.preventDefault();
131 _this.addTags(value, "paste");
132 };
133 _this.handleRemoveTag = function (event) {
134 // using data attribute to simplify callback logic -- one handler for all children
135 var index = +event.currentTarget.parentElement.getAttribute("data-tag-index");
136 _this.removeIndexFromValues(index);
137 };
138 return _this;
139 }
140 TagInput.getDerivedStateFromProps = function (props, state) {
141 if (props.inputValue !== state.prevInputValueProp) {
142 return {
143 inputValue: props.inputValue,
144 prevInputValueProp: props.inputValue,
145 };
146 }
147 return null;
148 };
149 TagInput.prototype.render = function () {
150 var _a;
151 var _b = this.props, className = _b.className, disabled = _b.disabled, fill = _b.fill, inputProps = _b.inputProps, intent = _b.intent, large = _b.large, leftIcon = _b.leftIcon, placeholder = _b.placeholder, values = _b.values;
152 var classes = classNames(Classes.INPUT, Classes.TAG_INPUT, (_a = {},
153 _a[Classes.ACTIVE] = this.state.isInputFocused,
154 _a[Classes.DISABLED] = disabled,
155 _a[Classes.FILL] = fill,
156 _a[Classes.LARGE] = large,
157 _a), Classes.intentClass(intent), className);
158 var isLarge = classes.indexOf(Classes.LARGE) > NONE;
159 // use placeholder prop only if it's defined and values list is empty or contains only falsy values
160 var isSomeValueDefined = values.some(function (val) { return !!val; });
161 var resolvedPlaceholder = placeholder == null || isSomeValueDefined ? inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder : placeholder;
162 return (React.createElement("div", { className: classes, onBlur: this.handleContainerBlur, onClick: this.handleContainerClick },
163 React.createElement(Icon, { className: Classes.TAG_INPUT_ICON, icon: leftIcon, size: isLarge ? IconSize.LARGE : IconSize.STANDARD }),
164 React.createElement("div", { className: Classes.TAG_INPUT_VALUES },
165 values.map(this.maybeRenderTag),
166 this.props.children,
167 React.createElement("input", __assign({ value: this.state.inputValue }, inputProps, { onFocus: this.handleInputFocus, onChange: this.handleInputChange, onKeyDown: this.handleInputKeyDown, onKeyUp: this.handleInputKeyUp, onPaste: this.handleInputPaste, placeholder: resolvedPlaceholder, ref: this.handleRef, className: classNames(Classes.INPUT_GHOST, inputProps === null || inputProps === void 0 ? void 0 : inputProps.className), disabled: disabled }))),
168 this.props.rightElement));
169 };
170 TagInput.prototype.componentDidUpdate = function (prevProps) {
171 if (prevProps.inputRef !== this.props.inputRef) {
172 setRef(prevProps.inputRef, null);
173 this.handleRef = refHandler(this, "inputElement", this.props.inputRef);
174 setRef(this.props.inputRef, this.inputElement);
175 }
176 };
177 TagInput.prototype.getNextActiveIndex = function (direction) {
178 var activeIndex = this.state.activeIndex;
179 if (activeIndex === NONE) {
180 // nothing active & moving left: select last defined value. otherwise select nothing.
181 return direction < 0 ? this.findNextIndex(this.props.values.length, -1) : NONE;
182 }
183 else {
184 // otherwise, move in direction and clamp to bounds.
185 // note that upper bound allows going one beyond last item
186 // so focus can move off the right end, into the text input.
187 return this.findNextIndex(activeIndex, direction);
188 }
189 };
190 TagInput.prototype.findNextIndex = function (startIndex, direction) {
191 var values = this.props.values;
192 var index = startIndex + direction;
193 while (index > 0 && index < values.length && !values[index]) {
194 index += direction;
195 }
196 return Utils.clamp(index, 0, values.length);
197 };
198 /**
199 * Splits inputValue on separator prop,
200 * trims whitespace from each new value,
201 * and ignores empty values.
202 */
203 TagInput.prototype.getValues = function (inputValue) {
204 var separator = this.props.separator;
205 // NOTE: split() typings define two overrides for string and RegExp.
206 // this does not play well with our union prop type, so we'll just declare it as a valid type.
207 return (separator === false ? [inputValue] : inputValue.split(separator))
208 .map(function (val) { return val.trim(); })
209 .filter(function (val) { return val.length > 0; });
210 };
211 TagInput.prototype.handleBackspaceToRemove = function (event) {
212 var previousActiveIndex = this.state.activeIndex;
213 // always move leftward one item (this will focus last item if nothing is focused)
214 this.setState({ activeIndex: this.getNextActiveIndex(-1) });
215 // delete item if there was a previous valid selection (ignore first backspace to focus last item)
216 if (this.isValidIndex(previousActiveIndex)) {
217 event.stopPropagation();
218 this.removeIndexFromValues(previousActiveIndex);
219 }
220 };
221 TagInput.prototype.handleDeleteToRemove = function (event) {
222 var activeIndex = this.state.activeIndex;
223 if (this.isValidIndex(activeIndex)) {
224 event.stopPropagation();
225 this.removeIndexFromValues(activeIndex);
226 }
227 };
228 /** Remove the item at the given index by invoking `onRemove` and `onChange` accordingly. */
229 TagInput.prototype.removeIndexFromValues = function (index) {
230 var _a = this.props, onChange = _a.onChange, onRemove = _a.onRemove, values = _a.values;
231 onRemove === null || onRemove === void 0 ? void 0 : onRemove(values[index], index);
232 if (Utils.isFunction(onChange)) {
233 onChange(values.filter(function (_, i) { return i !== index; }));
234 }
235 };
236 TagInput.prototype.invokeKeyPressCallback = function (propCallbackName, event, activeIndex) {
237 var _a, _b, _c, _d;
238 (_b = (_a = this.props)[propCallbackName]) === null || _b === void 0 ? void 0 : _b.call(_a, event, activeIndex === NONE ? undefined : activeIndex);
239 (_d = (_c = this.props.inputProps)[propCallbackName]) === null || _d === void 0 ? void 0 : _d.call(_c, event);
240 };
241 /** Returns whether the given index represents a valid item in `this.props.values`. */
242 TagInput.prototype.isValidIndex = function (index) {
243 return index !== NONE && index < this.props.values.length;
244 };
245 TagInput.displayName = DISPLAYNAME_PREFIX + ".TagInput";
246 TagInput.defaultProps = {
247 addOnBlur: false,
248 addOnPaste: true,
249 inputProps: {},
250 separator: /[,\n\r]/,
251 tagProps: {},
252 };
253 TagInput = __decorate([
254 polyfill
255 ], TagInput);
256 return TagInput;
257}(AbstractPureComponent2));
258export { TagInput };
259//# sourceMappingURL=tagInput.js.map
\No newline at end of file