UNPKG

@atlaskit/editor-plugin-find-replace

Version:

find replace plugin for @atlaskit/editor-core

253 lines (249 loc) 9.24 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; /* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */ /** * @jsxRuntime classic * @jsx jsx */ import React from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { jsx } from '@emotion/react'; import debounce from 'lodash/debounce'; import rafSchd from 'raf-schd'; import { injectIntl } from 'react-intl'; import { TRIGGER_METHOD } from '@atlaskit/editor-common/analytics'; import { findReplaceMessages as messages } from '@atlaskit/editor-common/messages'; import { Label } from '@atlaskit/form'; import TextLetterCaseIcon from '@atlaskit/icon-lab/core/text-letter-case'; import MatchCaseIcon from '@atlaskit/icon/core/text-style'; import Textfield from '@atlaskit/textfield'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { FindReplaceTooltipButton } from './FindReplaceTooltipButton'; import { afterInputSection, countStyles, countStylesAlternateStyles, matchCaseSection, sectionWrapperStyles, sectionWrapperStylesAlternate, textFieldWrapper } from './ui-styles'; export const FIND_DEBOUNCE_MS = 100; // eslint-disable-next-line @repo/internal/react/no-class-components class Find extends React.Component { constructor(props) { super(props); _defineProperty(this, "findTextfieldRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "isComposing", false); _defineProperty(this, "syncFindText", onSynced => { var _this$state; // If the external prop findText changes and we aren't in a composition we should update to // use the external prop value. // // An example of where this may happen is when a find occurs through the user selecting some text // and pressing Mod-f. if (!this.isComposing && this.props.findText !== ((_this$state = this.state) === null || _this$state === void 0 ? void 0 : _this$state.localFindText)) { this.updateFindValue(this.props.findText || '', onSynced); } }); _defineProperty(this, "focusFindTextfield", () => { const input = this.findTextfieldRef.current; if (this.props.shouldFocus && input) { input.select(); } }); _defineProperty(this, "handleFindChange", event => { this.updateFindValue(event.target.value); }); // debounce (vs throttle) to not block typing inside find input while onFind runs _defineProperty(this, "debouncedFind", debounce(value => { this.props.onFind(value); }, FIND_DEBOUNCE_MS)); _defineProperty(this, "updateFindValue", (value, onSynced) => { this.setState({ localFindText: value }, () => { if (this.isComposing) { return; } onSynced && onSynced(); this.debouncedFind(value); }); this.props.setFindTyped(true); }); // throtlle between animation frames gives better experience on Enter compared to arbitrary value // it adjusts based on performance (and document size) _defineProperty(this, "handleFindKeyDownThrottled", rafSchd(event => { if (event.key === 'Enter') { if (event.shiftKey) { this.props.onFindPrev({ triggerMethod: TRIGGER_METHOD.KEYBOARD }); } else { this.props.onFindNext({ triggerMethod: TRIGGER_METHOD.KEYBOARD }); } } else if (event.key === 'ArrowDown') { // we want to move focus between find & replace texfields when user hits up/down arrows this.props.onArrowDown(); } })); _defineProperty(this, "handleFindKeyDown", event => { if (this.isComposing) { return; } event.persist(); this.handleFindKeyDownThrottled(event); }); _defineProperty(this, "handleFindKeyUp", () => { this.handleFindKeyDownThrottled.cancel(); }); _defineProperty(this, "handleFindNextClick", () => { if (this.isComposing) { return; } this.props.onFindNext({ triggerMethod: TRIGGER_METHOD.BUTTON }); }); _defineProperty(this, "handleFindPrevClick", () => { if (this.isComposing) { return; } this.props.onFindPrev({ triggerMethod: TRIGGER_METHOD.BUTTON }); }); _defineProperty(this, "handleCompositionStart", () => { this.isComposing = true; }); _defineProperty(this, "handleCompositionEnd", event => { this.isComposing = false; // type for React.CompositionEvent doesn't set type for target correctly this.updateFindValue(event.target.value); }); _defineProperty(this, "clearSearch", () => { this.props.onCancel({ triggerMethod: TRIGGER_METHOD.BUTTON }); }); _defineProperty(this, "handleMatchCaseClick", () => { if (this.props.onToggleMatchCase) { this.props.onToggleMatchCase(); this.props.onFind(this.props.findText); } }); _defineProperty(this, "matchCaseIconEle", iconProps => { return expValEquals('platform_editor_find_and_replace_improvements', 'isEnabled', true) ? jsx(TextLetterCaseIcon, { label: iconProps.label, size: "small" }) : jsx(MatchCaseIcon, { label: this.matchCase }); }); const { intl: { formatMessage } } = props; this.find = formatMessage(messages.find); this.noResultsFound = formatMessage(messages.noResultsFound); this.matchCase = formatMessage(messages.matchCase); // We locally manage the value of the input inside this component in order to support compositions. // This requires some additional work inside componentDidUpdate to ensure we support changes that // occur to this value which do not originate from this component. this.state = { localFindText: '' }; } componentDidMount() { this.props.onFindTextfieldRefSet(this.findTextfieldRef); // focus initially on dialog mount if there is no find text provided if (!this.props.findText) { // Wait for findTextfieldRef to become available then focus setTimeout(() => { this.focusFindTextfield(); }, 100); } this.syncFindText(() => { // focus after input is synced if find text provided if (this.props.findText) { this.focusFindTextfield(); } }); } componentDidUpdate(prevProps) { var _this$state2; // focus on update if find text did not change if (this.props.findText === ((_this$state2 = this.state) === null || _this$state2 === void 0 ? void 0 : _this$state2.localFindText)) { this.focusFindTextfield(); } if (this.props.findText !== prevProps.findText && this.props.shouldFocus) { this.syncFindText(() => { // focus after input is synced if find text provided if (this.props.findText) { this.focusFindTextfield(); } }); } } componentWillUnmount() { this.debouncedFind.cancel(); this.handleFindKeyDownThrottled.cancel(); } render() { const { findText, count, shouldMatchCase, intl: { formatMessage } } = this.props; const resultsCount = formatMessage(messages.resultsCount, { selectedMatchPosition: count.index + 1, totalResultsCount: count.total }); const elemAfterInput = // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 jsx("div", { css: afterInputSection }, jsx("div", { "aria-live": "polite" }, findText && // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 jsx("span", { "data-testid": "textfield-count", css: [countStyles, countStylesAlternateStyles] }, count.total === 0 ? this.noResultsFound : resultsCount)), jsx("div", { css: matchCaseSection }, jsx(FindReplaceTooltipButton, { title: this.matchCase, appearance: "default", icon: this.matchCaseIconEle, iconLabel: this.matchCase, onClick: this.handleMatchCaseClick, isPressed: shouldMatchCase }))); return ( // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 jsx("div", { css: [sectionWrapperStyles, sectionWrapperStylesAlternate] }, jsx("div", { css: textFieldWrapper }, jsx(Label, { htmlFor: "find-text-field" }, this.find), jsx(Textfield, { name: "find", id: "find-text-field", testId: "find-field", appearance: "standard", value: this.state.localFindText, ref: this.findTextfieldRef, autoComplete: "off", onChange: this.handleFindChange, onKeyDown: this.handleFindKeyDown, onKeyUp: this.handleFindKeyUp, onBlur: this.props.onFindBlur, onCompositionStart: this.handleCompositionStart, onCompositionEnd: this.handleCompositionEnd, elemAfterInput: elemAfterInput }))) ); } } // eslint-disable-next-line @typescript-eslint/ban-types const _default_1 = injectIntl(Find); export default _default_1;