@atlaskit/editor-plugin-find-replace
Version:
find replace plugin for @atlaskit/editor-core
253 lines (249 loc) • 9.24 kB
JavaScript
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;