UNPKG

@blueprintjs/core

Version:
288 lines 12.3 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 * as tslib_1 from "tslib"; import classNames from "classnames"; import * as React from "react"; import { polyfill } from "react-lifecycles-compat"; import { AbstractPureComponent2, Classes, Keys } from "../../common"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { clamp, safeInvoke } from "../../common/utils"; import { Browser } from "../../compatibility"; const BUFFER_WIDTH_EDGE = 5; const BUFFER_WIDTH_IE = 30; let EditableText = class EditableText extends AbstractPureComponent2 { constructor(props, context) { super(props, context); this.refHandlers = { content: (spanElement) => { this.valueElement = spanElement; }, input: (input) => { if (input != null) { input.focus(); const supportsSelection = inputSupportsSelection(input); if (supportsSelection) { const { length } = input.value; input.setSelectionRange(this.props.selectAllOnFocus ? 0 : length, length); } if (!supportsSelection || !this.props.selectAllOnFocus) { input.scrollLeft = input.scrollWidth; } } }, }; this.cancelEditing = () => { const { lastValue, value } = this.state; this.setState({ isEditing: false, value: lastValue }); if (value !== lastValue) { safeInvoke(this.props.onChange, lastValue); } safeInvoke(this.props.onCancel, lastValue); }; this.toggleEditing = () => { if (this.state.isEditing) { const { value } = this.state; this.setState({ isEditing: false, lastValue: value }); safeInvoke(this.props.onConfirm, value); } else if (!this.props.disabled) { this.setState({ isEditing: true }); } }; this.handleFocus = () => { if (!this.props.disabled) { this.setState({ isEditing: true }); } }; this.handleTextChange = (event) => { const value = event.target.value; // state value should be updated only when uncontrolled if (this.props.value == null) { this.setState({ value }); } safeInvoke(this.props.onChange, value); }; this.handleKeyEvent = (event) => { const { altKey, ctrlKey, metaKey, shiftKey, which } = event; if (which === Keys.ESCAPE) { this.cancelEditing(); return; } const hasModifierKey = altKey || ctrlKey || metaKey || shiftKey; if (which === Keys.ENTER) { // prevent IE11 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(); } } }; const value = props.value == null ? props.defaultValue : props.value; this.state = { inputHeight: 0, inputWidth: 0, isEditing: props.isEditing === true && props.disabled === false, lastValue: value, value, }; } render() { const { disabled, multiline } = this.props; const value = this.props.value == null ? this.state.value : this.props.value; const hasValue = value != null && value !== ""; const classes = classNames(Classes.EDITABLE_TEXT, Classes.intentClass(this.props.intent), { [Classes.DISABLED]: disabled, [Classes.EDITABLE_TEXT_EDITING]: this.state.isEditing, [Classes.EDITABLE_TEXT_PLACEHOLDER]: !hasValue, [Classes.MULTILINE]: multiline, }, this.props.className); let 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 : null }; } else { // minWidth only applies in single line mode (multiline == width 100%) contentStyle = { height: this.state.inputHeight, lineHeight: this.state.inputHeight != null ? `${this.state.inputHeight}px` : null, minWidth: this.props.minWidth, }; } // make enclosing div focusable when not editing, so it can still be tabbed to focus // (when editing, input itself is focusable so div doesn't need to be) const tabIndex = this.state.isEditing || disabled ? null : 0; return (React.createElement("div", { className: classes, onFocus: this.handleFocus, tabIndex: tabIndex }, this.maybeRenderInput(value), React.createElement("span", { className: Classes.EDITABLE_TEXT_CONTENT, ref: this.refHandlers.content, style: contentStyle }, hasValue ? value : this.props.placeholder))); } componentDidMount() { this.updateInputDimensions(); } componentDidUpdate(prevProps, prevState) { const state = {}; if (this.props.value != null && this.props.value !== prevProps.value) { state.value = this.props.value; } if (this.props.isEditing != null && this.props.isEditing !== prevProps.isEditing) { state.isEditing = this.props.isEditing; } if (this.props.disabled || (this.props.disabled == null && prevProps.disabled)) { state.isEditing = false; } this.setState(state); if (this.state.isEditing && !prevState.isEditing) { safeInvoke(this.props.onEdit, this.state.value); } this.updateInputDimensions(); } maybeRenderInput(value) { const { maxLength, multiline, type, placeholder } = this.props; if (!this.state.isEditing) { return undefined; } const props = { className: Classes.EDITABLE_TEXT_INPUT, maxLength, onBlur: this.toggleEditing, onChange: this.handleTextChange, onKeyDown: this.handleKeyEvent, placeholder, style: { height: this.state.inputHeight, lineHeight: !multiline && this.state.inputHeight != null ? `${this.state.inputHeight}px` : null, width: multiline ? "100%" : this.state.inputWidth, }, value, }; return multiline ? (React.createElement("textarea", Object.assign({ ref: this.refHandlers.input }, props))) : (React.createElement("input", Object.assign({ ref: this.refHandlers.input, type: type }, props))); } updateInputDimensions() { if (this.valueElement != null) { const { maxLines, minLines, minWidth, multiline } = this.props; const { parentElement, textContent } = this.valueElement; let { scrollHeight, scrollWidth } = this.valueElement; const 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)) { scrollHeight += lineHeight; } if (lineHeight > 0) { // line height could be 0 if the isNaN block from getLineHeight kicks in scrollHeight = clamp(scrollHeight, 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 = Math.max(scrollHeight, getFontSize(this.valueElement) + 1, getLineHeight(parentElement)); // IE11 & Edge needs a small buffer so text does not shift prior to resizing if (Browser.isEdge()) { scrollWidth += BUFFER_WIDTH_EDGE; } else if (Browser.isInternetExplorer()) { scrollWidth += BUFFER_WIDTH_IE; } this.setState({ inputHeight: scrollHeight, inputWidth: Math.max(scrollWidth, minWidth), }); // synchronizes the ::before pseudo-element's height while editing for Chrome 53 if (multiline && this.state.isEditing) { this.setTimeout(() => (parentElement.style.height = `${scrollHeight}px`)); } } } }; EditableText.displayName = `${DISPLAYNAME_PREFIX}.EditableText`; EditableText.defaultProps = { confirmOnEnterKey: false, defaultValue: "", disabled: false, maxLines: Infinity, minLines: 1, minWidth: 80, multiline: false, placeholder: "Click to Edit", type: "text", }; EditableText = tslib_1.__decorate([ polyfill ], EditableText); export { EditableText }; function getFontSize(element) { const fontSize = getComputedStyle(element).fontSize; return fontSize === "" ? 0 : parseInt(fontSize.slice(0, -2), 10); } function getLineHeight(element) { // getComputedStyle() => 18.0001px => 18 let 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 const line = document.createElement("span"); line.innerHTML = "<br>"; element.appendChild(line); const singleLineHeight = element.offsetHeight; line.innerHTML = "<br><br>"; const doubleLineHeight = element.offsetHeight; element.removeChild(line); // this can return 0 in edge cases lineHeight = doubleLineHeight - singleLineHeight; } return lineHeight; } function insertAtCaret(el, text) { const { selectionEnd, selectionStart, value } = el; if (selectionStart >= 0) { const before = value.substring(0, selectionStart); const after = value.substring(selectionEnd, value.length); const len = text.length; el.value = `${before}${text}${after}`; 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