UNPKG

@blueprintjs/core

Version:
321 lines 15.5 kB
/* * Copyright 2016 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { __assign, __extends } from "tslib"; import classNames from "classnames"; import * as React from "react"; import { AbstractPureComponent, Classes } from "../../common"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { clamp } from "../../common/utils"; var BUFFER_WIDTH_DEFAULT = 5; /** * EditableText component. * * @see https://blueprintjs.com/docs/#core/components/editable-text */ var EditableText = /** @class */ (function (_super) { __extends(EditableText, _super); function EditableText(props) { var _this = _super.call(this, props) || this; _this.inputElement = null; _this.valueElement = null; _this.refHandlers = { content: function (spanElement) { _this.valueElement = spanElement; }, input: function (input) { if (input != null) { _this.inputElement = input; // temporary fix for #3882 if (!_this.props.alwaysRenderInput) { _this.inputElement.focus(); } if (_this.state != null && _this.state.isEditing) { var supportsSelection = inputSupportsSelection(input); if (supportsSelection) { var length_1 = input.value.length; input.setSelectionRange(_this.props.selectAllOnFocus ? 0 : length_1, length_1); } if (!supportsSelection || !_this.props.selectAllOnFocus) { input.scrollLeft = input.scrollWidth; } } } }, }; _this.cancelEditing = function () { var _a, _b, _c, _d; var _e = _this.state, lastValue = _e.lastValue, value = _e.value; _this.setState({ isEditing: false, value: lastValue }); if (value !== lastValue) { (_b = (_a = _this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, lastValue); } (_d = (_c = _this.props).onCancel) === null || _d === void 0 ? void 0 : _d.call(_c, lastValue); }; _this.toggleEditing = function () { var _a, _b; if (_this.state.isEditing) { var value = _this.state.value; _this.setState({ isEditing: false, lastValue: value }); (_b = (_a = _this.props).onConfirm) === null || _b === void 0 ? void 0 : _b.call(_a, value); } else if (!_this.props.disabled) { _this.setState({ isEditing: true }); } }; _this.handleFocus = function () { var _a = _this.props, alwaysRenderInput = _a.alwaysRenderInput, disabled = _a.disabled, selectAllOnFocus = _a.selectAllOnFocus; if (!disabled) { _this.setState({ isEditing: true }); } if (alwaysRenderInput && selectAllOnFocus && _this.inputElement != null) { var length_2 = _this.inputElement.value.length; _this.inputElement.setSelectionRange(0, length_2); } }; _this.handleTextChange = function (event) { var _a, _b; var value = event.target.value; // state value should be updated only when uncontrolled if (_this.props.value == null) { _this.setState({ value: value }); } (_b = (_a = _this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, value); }; _this.handleKeyEvent = function (event) { var altKey = event.altKey, ctrlKey = event.ctrlKey, metaKey = event.metaKey, shiftKey = event.shiftKey; if (event.key === "Escape") { _this.cancelEditing(); return; } var hasModifierKey = altKey || ctrlKey || metaKey || shiftKey; if (event.key === "Enter") { // prevent browsers (Edge?) from full screening with alt + enter // shift + enter adds a newline by default if (altKey || shiftKey) { event.preventDefault(); } if (_this.props.confirmOnEnterKey && _this.props.multiline) { if (event.target != null && hasModifierKey) { insertAtCaret(event.target, "\n"); _this.handleTextChange(event); } else { _this.toggleEditing(); } } else if (!_this.props.multiline || hasModifierKey) { _this.toggleEditing(); } } }; var value = props.value == null ? props.defaultValue : props.value; _this.state = { inputHeight: 0, inputWidth: 0, isEditing: props.isEditing === true && props.disabled === false, lastValue: value, value: value, }; return _this; } EditableText.prototype.render = function () { var _a; var _b; var _c = this.props, alwaysRenderInput = _c.alwaysRenderInput, disabled = _c.disabled, elementRef = _c.elementRef, multiline = _c.multiline, contentId = _c.contentId; var value = (_b = this.props.value) !== null && _b !== void 0 ? _b : this.state.value; var hasValue = value != null && value !== ""; var classes = classNames(Classes.EDITABLE_TEXT, Classes.intentClass(this.props.intent), (_a = {}, _a[Classes.DISABLED] = disabled, _a[Classes.EDITABLE_TEXT_EDITING] = this.state.isEditing, _a[Classes.EDITABLE_TEXT_PLACEHOLDER] = !hasValue, _a[Classes.MULTILINE] = multiline, _a), this.props.className); var contentStyle; if (multiline) { // set height only in multiline mode when not editing // otherwise we're measuring this element to determine appropriate height of text contentStyle = { height: !this.state.isEditing ? this.state.inputHeight : undefined }; } else { // minWidth only applies in single line mode (multiline == width 100%) contentStyle = { height: this.state.inputHeight, lineHeight: this.state.inputHeight != null ? "".concat(this.state.inputHeight, "px") : undefined, minWidth: this.props.minWidth, }; } // If we are always rendering an input, then NEVER make the container div focusable. // Otherwise, make container div focusable when not editing, so it can still be tabbed // to focus (when the input is rendered, it is itself focusable so container div doesn't need to be) var tabIndex = alwaysRenderInput || this.state.isEditing || disabled ? undefined : 0; // we need the contents to be rendered while editing so that we can measure their height // and size the container element responsively var shouldHideContents = alwaysRenderInput && !this.state.isEditing; var spanProps = contentId != null ? { id: contentId } : {}; return (React.createElement("div", { className: classes, onFocus: this.handleFocus, tabIndex: tabIndex, ref: elementRef }, alwaysRenderInput || this.state.isEditing ? this.renderInput(value) : undefined, shouldHideContents ? undefined : (React.createElement("span", __assign({}, spanProps, { className: Classes.EDITABLE_TEXT_CONTENT, ref: this.refHandlers.content, style: contentStyle }), hasValue ? value : this.props.placeholder)))); }; EditableText.prototype.componentDidMount = function () { this.updateInputDimensions(); }; EditableText.prototype.componentDidUpdate = function (prevProps, prevState) { var _a, _b; var newState = {}; // allow setting the value to undefined/null in controlled mode if (this.props.value !== prevProps.value && (prevProps.value != null || this.props.value != null)) { newState.value = this.props.value; } if (this.props.isEditing != null && this.props.isEditing !== prevProps.isEditing) { newState.isEditing = this.props.isEditing; } if (this.props.disabled || (this.props.disabled == null && prevProps.disabled)) { newState.isEditing = false; } this.setState(newState); if (this.state.isEditing && !prevState.isEditing) { (_b = (_a = this.props).onEdit) === null || _b === void 0 ? void 0 : _b.call(_a, this.state.value); } // updateInputDimensions is an expensive method. Call it only when the props // it depends on change if (this.state.value !== prevState.value || this.props.alwaysRenderInput !== prevProps.alwaysRenderInput || this.props.maxLines !== prevProps.maxLines || this.props.minLines !== prevProps.minLines || this.props.minWidth !== prevProps.minWidth || this.props.multiline !== prevProps.multiline) { this.updateInputDimensions(); } }; EditableText.prototype.renderInput = function (value) { var _a = this.props, disabled = _a.disabled, maxLength = _a.maxLength, multiline = _a.multiline, type = _a.type, placeholder = _a.placeholder; var props = { className: Classes.EDITABLE_TEXT_INPUT, disabled: disabled, maxLength: maxLength, onBlur: this.toggleEditing, onChange: this.handleTextChange, onKeyDown: this.handleKeyEvent, placeholder: placeholder, value: value, }; var _b = this.state, inputHeight = _b.inputHeight, inputWidth = _b.inputWidth; if (inputHeight !== 0 && inputWidth !== 0) { props.style = { height: inputHeight, lineHeight: !multiline && inputHeight != null ? "".concat(inputHeight, "px") : undefined, width: multiline ? "100%" : inputWidth, }; } return multiline ? (React.createElement("textarea", __assign({ ref: this.refHandlers.input }, props))) : (React.createElement("input", __assign({ ref: this.refHandlers.input, type: type }, props))); }; EditableText.prototype.updateInputDimensions = function () { if (this.valueElement != null) { var _a = this.props, maxLines = _a.maxLines, minLines = _a.minLines, minWidth = _a.minWidth, multiline = _a.multiline; var _b = this.valueElement, parentElement_1 = _b.parentElement, textContent = _b.textContent; var _c = this.valueElement, scrollHeight_1 = _c.scrollHeight, scrollWidth = _c.scrollWidth; var lineHeight = getLineHeight(this.valueElement); // add one line to computed <span> height if text ends in newline // because <span> collapses that trailing whitespace but <textarea> shows it if (multiline && this.state.isEditing && /\n$/.test(textContent !== null && textContent !== void 0 ? textContent : "")) { scrollHeight_1 += lineHeight; } if (lineHeight > 0) { // line height could be 0 if the isNaN block from getLineHeight kicks in scrollHeight_1 = clamp(scrollHeight_1, minLines * lineHeight, maxLines * lineHeight); } // Chrome's input caret height misaligns text so the line-height must be larger than font-size. // The computed scrollHeight must also account for a larger inherited line-height from the parent. scrollHeight_1 = Math.max(scrollHeight_1, getFontSize(this.valueElement) + 1, getLineHeight(parentElement_1)); // Need to add a small buffer so text does not shift prior to resizing, causing an infinite loop. scrollWidth += BUFFER_WIDTH_DEFAULT; this.setState({ inputHeight: scrollHeight_1, inputWidth: Math.max(scrollWidth, minWidth), }); // synchronizes the ::before pseudo-element's height while editing for Chrome 53 if (multiline && this.state.isEditing) { this.setTimeout(function () { return (parentElement_1.style.height = "".concat(scrollHeight_1, "px")); }); } } }; EditableText.displayName = "".concat(DISPLAYNAME_PREFIX, ".EditableText"); EditableText.defaultProps = { alwaysRenderInput: false, confirmOnEnterKey: false, defaultValue: "", disabled: false, maxLines: Infinity, minLines: 1, minWidth: 80, multiline: false, placeholder: "Click to Edit", type: "text", }; return EditableText; }(AbstractPureComponent)); export { EditableText }; function getFontSize(element) { var fontSize = getComputedStyle(element).fontSize; return fontSize === "" ? 0 : parseInt(fontSize.slice(0, -2), 10); } function getLineHeight(element) { // getComputedStyle() => 18.0001px => 18 var lineHeight = parseInt(getComputedStyle(element).lineHeight.slice(0, -2), 10); // this check will be true if line-height is a keyword like "normal" if (isNaN(lineHeight)) { // @see http://stackoverflow.com/a/18430767/6342931 var line = document.createElement("span"); line.innerHTML = "<br>"; element.appendChild(line); var singleLineHeight = element.offsetHeight; line.innerHTML = "<br><br>"; var doubleLineHeight = element.offsetHeight; element.removeChild(line); // this can return 0 in edge cases lineHeight = doubleLineHeight - singleLineHeight; } return lineHeight; } function insertAtCaret(el, text) { var selectionEnd = el.selectionEnd, selectionStart = el.selectionStart, value = el.value; if (selectionStart >= 0) { var before_1 = value.substring(0, selectionStart); var after_1 = value.substring(selectionEnd, value.length); var len = text.length; el.value = "".concat(before_1).concat(text).concat(after_1); el.selectionStart = selectionStart + len; el.selectionEnd = selectionStart + len; } } function inputSupportsSelection(input) { switch (input.type) { // HTMLTextAreaElement case "textarea": return true; // HTMLInputElement // see https://html.spec.whatwg.org/multipage/input.html#do-not-apply case "text": case "search": case "tel": case "url": case "password": return true; default: return false; } } //# sourceMappingURL=editableText.js.map