"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = exports.RichText = void 0;
var _reactNative = require("react-native");
var _memize = _interopRequireDefault(require("memize"));
var _colord = require("colord");
var _reactNativeAztec = _interopRequireDefault(require("@wordpress/react-native-aztec"));
var _reactNativeBridge = require("@wordpress/react-native-bridge");
var _blockEditor = require("@wordpress/block-editor");
var _components = require("@wordpress/components");
var _element = require("@wordpress/element");
var _compose = require("@wordpress/compose");
var _data = require("@wordpress/data");
var _blocks = require("@wordpress/blocks");
var _htmlEntities = require("@wordpress/html-entities");
var _keycodes = require("@wordpress/keycodes");
var _url = require("@wordpress/url");
var _icons = require("@wordpress/icons");
var _i18n = require("@wordpress/i18n");
var _richText = require("@wordpress/rich-text");
var _useFormatTypes = require("./use-format-types");
var _formatEdit = _interopRequireDefault(require("./format-edit"));
var _getFormatColors = require("./get-format-colors");
var _style = _interopRequireDefault(require("./style.scss"));
var _toolbarButtonWithOptions = _interopRequireDefault(require("./toolbar-button-with-options"));
var _jsxRuntime = require("react/jsx-runtime");
/* eslint no-console: ["error", { allow: ["warn"] }] */

/**
 * External dependencies
 */

/**
 * WordPress dependencies
 */

/**
 * Internal dependencies
 */

// The flattened color palettes array is memoized to ensure that the same array instance is
// returned for the colors palettes. This value might be used as a prop, so having the same
// instance will prevent unnecessary re-renders of the RichText component.
const flatColorPalettes = (0, _memize.default)(colorsPalettes => [...(colorsPalettes?.theme || []), ...(colorsPalettes?.custom || []), ...(colorsPalettes?.default || [])]);
const getSelectionColor = (0, _memize.default)((currentSelectionColor, defaultSelectionColor, baseGlobalStyles, isBlockBasedTheme) => {
  let selectionColor = defaultSelectionColor;
  if (currentSelectionColor) {
    selectionColor = currentSelectionColor;
  }
  if (isBlockBasedTheme) {
    const colordTextColor = (0, _colord.colord)(selectionColor);
    const colordBackgroundColor = (0, _colord.colord)(baseGlobalStyles?.color?.background);
    const isColordTextReadable = colordTextColor.isReadable(colordBackgroundColor);
    if (!isColordTextReadable) {
      selectionColor = baseGlobalStyles?.color?.text;
    }
  }
  return selectionColor;
});
const gutenbergFormatNamesToAztec = {
  'core/bold': 'bold',
  'core/italic': 'italic',
  'core/strikethrough': 'strikethrough',
  'core/text-color': 'mark'
};
const EMPTY_PARAGRAPH_TAGS = '<p></p>';
const DEFAULT_FONT_SIZE = 16;
const MIN_LINE_HEIGHT = 1;
class RichText extends _element.Component {
  constructor({
    value,
    selectionStart,
    selectionEnd
  }) {
    super(...arguments);
    this.isIOS = _reactNative.Platform.OS === 'ios';
    this.createRecord = this.createRecord.bind(this);
    this.onChangeFromAztec = this.onChangeFromAztec.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.handleEnter = this.handleEnter.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
    this.onPaste = this.onPaste.bind(this);
    this.onFocus = this.onFocus.bind(this);
    this.onBlur = this.onBlur.bind(this);
    this.onTextUpdate = this.onTextUpdate.bind(this);
    this.onContentSizeChange = this.onContentSizeChange.bind(this);
    this.onFormatChange = this.onFormatChange.bind(this);
    this.formatToValue = (0, _memize.default)(this.formatToValue.bind(this), {
      maxSize: 1
    });
    this.debounceCreateUndoLevel = (0, _compose.debounce)(this.onCreateUndoLevel, 1000);
    // This prevents a bug in Aztec which triggers onSelectionChange twice on format change.
    this.onSelectionChange = this.onSelectionChange.bind(this);
    this.onSelectionChangeFromAztec = this.onSelectionChangeFromAztec.bind(this);
    this.valueToFormat = this.valueToFormat.bind(this);
    this.getHtmlToRender = this.getHtmlToRender.bind(this);
    this.handleSuggestionFunc = this.handleSuggestionFunc.bind(this);
    this.handleUserSuggestion = this.handleSuggestionFunc(_reactNativeBridge.showUserSuggestions, '@').bind(this);
    this.handleXpostSuggestion = this.handleSuggestionFunc(_reactNativeBridge.showXpostSuggestions, '+').bind(this);
    this.suggestionOptions = this.suggestionOptions.bind(this);
    this.insertString = this.insertString.bind(this);
    this.manipulateEventCounterToForceNativeToRefresh = this.manipulateEventCounterToForceNativeToRefresh.bind(this);
    this.shouldDropEventFromAztec = this.shouldDropEventFromAztec.bind(this);
    this.state = {
      activeFormats: [],
      selectedFormat: null,
      height: 0,
      currentFontSize: this.getFontSize(arguments[0])
    };
    this.needsSelectionUpdate = false;
    this.savedContent = '';
    this.isTouched = false;
    this.lastAztecEventType = null;
    this.lastHistoryValue = value;

    // Internal values that are update synchronously, unlike props.
    this.value = value;
    this.selectionStart = selectionStart;
    this.selectionEnd = selectionEnd;
  }

  /**
   * Get the current record (value and selection) from props and state.
   *
   * @return {Object} The current record (value and selection).
   */
  getRecord() {
    const {
      selectionStart: start,
      selectionEnd: end,
      colorPalette
    } = this.props;
    const {
      value
    } = this.props;
    const currentValue = this.formatToValue(value);
    const {
      formats,
      replacements,
      text
    } = currentValue;
    const {
      activeFormats
    } = this.state;
    const newFormats = (0, _getFormatColors.getFormatColors)(formats, colorPalette);
    return {
      formats: newFormats,
      replacements,
      text,
      start,
      end,
      activeFormats
    };
  }

  /**
   * Creates a RichText value "record" from the current content and selection
   * information
   *
   *
   * @return {Object} A RichText value with formats and selection.
   */
  createRecord() {
    const {
      preserveWhiteSpace
    } = this.props;
    const value = {
      start: this.selectionStart,
      end: this.selectionEnd,
      ...(0, _richText.create)({
        html: this.value,
        range: null,
        preserveWhiteSpace
      })
    };
    const start = Math.min(this.selectionStart, value.text.length);
    const end = Math.min(this.selectionEnd, value.text.length);
    return {
      ...value,
      start,
      end
    };
  }
  valueToFormat(value) {
    // Remove the outer root tags.
    return this.removeRootTagsProducedByAztec((0, _richText.toHTMLString)({
      value
    }));
  }
  getActiveFormatNames(record) {
    const {
      formatTypes
    } = this.props;
    return formatTypes.map(({
      name
    }) => name).filter(name => {
      return (0, _richText.getActiveFormat)(record, name) !== undefined;
    }).map(name => gutenbergFormatNamesToAztec[name]).filter(Boolean);
  }
  onFormatChange(record) {
    const {
      start = 0,
      end = 0,
      activeFormats = []
    } = record;
    const changeHandlers = Object.fromEntries(Object.entries(this.props).filter(([key]) => key.startsWith('format_on_change_functions_')));
    Object.values(changeHandlers).forEach(changeHandler => {
      changeHandler(record.formats, record.text);
    });
    this.value = this.valueToFormat(record);
    this.props.onChange(this.value);
    this.setState({
      activeFormats
    });
    this.props.onSelectionChange(start, end);
    this.selectionStart = start;
    this.selectionEnd = end;
    this.onCreateUndoLevel();
    this.lastAztecEventType = 'format change';
  }
  insertString(record, string) {
    if (record && string) {
      this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      const toInsert = (0, _richText.insert)(record, string);
      this.onFormatChange(toInsert);
    }
  }
  onCreateUndoLevel() {
    const {
      __unstableOnCreateUndoLevel: onCreateUndoLevel
    } = this.props;
    // If the content is the same, no level needs to be created.
    if (this.lastHistoryValue?.toString() === this.value?.toString()) {
      return;
    }
    onCreateUndoLevel();
    this.lastHistoryValue = this.value;
  }

  /*
   * Cleans up any root tags produced by aztec.
   * TODO: This should be removed on a later version when aztec doesn't return the top tag of the text being edited
   */
  removeRootTagsProducedByAztec(html) {
    let result = this.removeRootTag(this.props.tagName, html);
    if (this.props.tagsToEliminate) {
      this.props.tagsToEliminate.forEach(element => {
        result = this.removeTag(element, result);
      });
    }
    return result;
  }
  removeRootTag(tag, html) {
    const openingTagRegexp = RegExp('^<' + tag + '[^>]*>', 'gim');
    const closingTagRegexp = RegExp('</' + tag + '>$', 'gim');
    return html.replace(openingTagRegexp, '').replace(closingTagRegexp, '');
  }
  removeTag(tag, html) {
    const openingTagRegexp = RegExp('<' + tag + '>', 'gim');
    const closingTagRegexp = RegExp('</' + tag + '>', 'gim');
    return html.replace(openingTagRegexp, '').replace(closingTagRegexp, '');
  }

  /*
   * Handles any case where the content of the AztecRN instance has changed
   */
  onChangeFromAztec(event) {
    if (this.shouldDropEventFromAztec(event, 'onChange')) {
      return;
    }
    const contentWithoutRootTag = this.removeRootTagsProducedByAztec(event.nativeEvent.text);
    const {
      __unstableInputRule
    } = this.props;
    const currentValuePosition = {
      end: this.isIOS ? this.selectionEnd : this.selectionEnd + 1,
      start: this.isIOS ? this.selectionStart : this.selectionStart + 1
    };
    if (__unstableInputRule && __unstableInputRule({
      ...currentValuePosition,
      ...this.formatToValue(contentWithoutRootTag)
    })) {
      return;
    }

    // On iOS, onChange can be triggered after selection changes, even though there are no content changes.
    if (contentWithoutRootTag === this.value?.toString()) {
      return;
    }
    this.lastEventCount = event.nativeEvent.eventCount;
    this.comesFromAztec = true;
    this.firedAfterTextChanged = true; // The onChange event always fires after the fact.
    this.onTextUpdate(event);
    this.lastAztecEventType = 'input';
  }
  onTextUpdate(event) {
    const contentWithoutRootTag = this.removeRootTagsProducedByAztec(event.nativeEvent.text);
    this.debounceCreateUndoLevel();
    const refresh = this.value?.toString() !== contentWithoutRootTag;
    this.value = contentWithoutRootTag;

    // We don't want to refresh if our goal is just to create a record.
    if (refresh) {
      this.props.onChange(contentWithoutRootTag);
    }
  }

  /*
   * Handles any case where the content of the AztecRN instance has changed in size
   */
  onContentSizeChange(contentSize) {
    this.setState(contentSize);
    this.lastAztecEventType = 'content size change';
  }
  onKeyDown(event) {
    if (event.defaultPrevented) {
      return;
    }

    // Add stubs for conformance in downstream autocompleters logic.
    this.customEditableOnKeyDown?.({
      preventDefault: () => undefined,
      ...event,
      key: _reactNativeAztec.default.KeyCodes[event?.keyCode]
    });
    this.handleDelete(event);
    this.handleEnter(event);
    this.handleTriggerKeyCodes(event);
  }
  handleEnter(event) {
    if (event.keyCode !== _keycodes.ENTER) {
      return;
    }
    const {
      onEnter
    } = this.props;
    if (!onEnter) {
      return;
    }
    onEnter({
      value: this.createRecord(),
      onChange: this.onFormatChange,
      shiftKey: event.shiftKey
    });
    this.lastAztecEventType = 'input';
  }
  handleDelete(event) {
    if (this.shouldDropEventFromAztec(event, 'handleDelete')) {
      return;
    }
    const {
      keyCode
    } = event;
    if (keyCode !== _keycodes.DELETE && keyCode !== _keycodes.BACKSPACE) {
      return;
    }
    const isReverse = keyCode === _keycodes.BACKSPACE;
    const {
      onDelete
    } = this.props;
    this.lastEventCount = event.nativeEvent.eventCount;
    this.comesFromAztec = true;
    this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged;
    const value = this.createRecord();
    const {
      start,
      end,
      text,
      activeFormats
    } = value;
    const hasActiveFormats = activeFormats && !!activeFormats.length;
    let newValue;

    // Always handle full content deletion ourselves.
    if (start === 0 && end !== 0 && end >= text.length) {
      newValue = (0, _richText.remove)(value);
      this.onFormatChange(newValue);
      event.preventDefault();
      return;
    }

    // Only process delete if the key press occurs at an uncollapsed edge.
    if (!(0, _richText.isCollapsed)(value) || hasActiveFormats || isReverse && start !== 0 || !isReverse && end !== text.length) {
      return;
    }
    if (onDelete) {
      onDelete({
        isReverse,
        value
      });
    }
    event.preventDefault();
    this.lastAztecEventType = 'input';
  }
  handleTriggerKeyCodes(event) {
    const {
      keyCode
    } = event;
    const triggeredOption = this.suggestionOptions().find(option => {
      const triggeredKeyCode = option.triggerChar.charCodeAt(0);
      return triggeredKeyCode === keyCode;
    });
    if (triggeredOption) {
      const record = this.getRecord();
      const text = (0, _richText.getTextContent)(record);
      // Only respond to the trigger if the selection is on the start of text or line
      // or if the character before is a space.
      const useTrigger = text.length === 0 || record.start === 0 || text.charAt(record.start - 1) === '\n' || text.charAt(record.start - 1) === ' ';
      if (useTrigger && triggeredOption.onClick) {
        triggeredOption.onClick();
      } else {
        this.insertString(record, triggeredOption.triggerChar);
      }
    }
  }
  suggestionOptions() {
    const {
      areMentionsSupported,
      areXPostsSupported
    } = this.props;
    const allOptions = [{
      supported: areMentionsSupported,
      title: (0, _i18n.__)('Insert mention'),
      onClick: this.handleUserSuggestion,
      triggerChar: '@',
      value: 'mention',
      label: (0, _i18n.__)('Mention'),
      icon: _icons.atSymbol
    }, {
      supported: areXPostsSupported,
      title: (0, _i18n.__)('Insert crosspost'),
      onClick: this.handleXpostSuggestion,
      triggerChar: '+',
      value: 'crosspost',
      label: (0, _i18n.__)('Crosspost'),
      icon: _icons.plus
    }];
    return allOptions.filter(op => op.supported);
  }
  handleSuggestionFunc(suggestionFunction, prefix) {
    return () => {
      const record = this.getRecord();
      suggestionFunction().then(suggestion => {
        this.insertString(record, `${prefix}${suggestion} `);
      }).catch(() => {});
    };
  }

  /**
   * Handles a paste event from the native Aztec Wrapper.
   *
   * @param {Object} event The paste event which wraps `nativeEvent`.
   */
  onPaste(event) {
    const {
      onPaste,
      onChange
    } = this.props;
    const {
      activeFormats = []
    } = this.state;
    const {
      pastedText,
      pastedHtml,
      files
    } = event.nativeEvent;
    const currentRecord = this.createRecord();
    event.preventDefault();

    // There is a selection, check if a URL is pasted.
    if (!(0, _richText.isCollapsed)(currentRecord)) {
      const trimmedText = (pastedHtml || pastedText).replace(/<[^>]+>/g, '').trim();

      // A URL was pasted, turn the selection into a link.
      if ((0, _url.isURL)(trimmedText)) {
        const linkedRecord = (0, _richText.applyFormat)(currentRecord, {
          type: 'a',
          attributes: {
            href: (0, _htmlEntities.decodeEntities)(trimmedText)
          }
        });
        this.value = this.valueToFormat(linkedRecord);
        onChange(this.value);

        // Allows us to ask for this information when we get a report.
        window.console.log('Created link:\n\n', trimmedText);
        return;
      }
    }
    if (onPaste) {
      onPaste({
        value: currentRecord,
        onChange: this.onFormatChange,
        html: pastedHtml,
        plainText: pastedText,
        files,
        activeFormats
      });
    }
  }
  onFocus() {
    this.isTouched = true;
    const {
      unstableOnFocus,
      onSelectionChange
    } = this.props;
    if (unstableOnFocus) {
      unstableOnFocus();
    }

    // We know for certain that on focus, the old selection is invalid. It
    // will be recalculated on `selectionchange`.

    onSelectionChange(this.selectionStart, this.selectionEnd);
    this.lastAztecEventType = 'focus';
  }
  onBlur(event) {
    this.isTouched = false;

    // Check if value is up to date with latest state of native AztecView.
    if (event.nativeEvent.text && event.nativeEvent.text !== this.props.value?.toString()) {
      this.onTextUpdate(event);
    }
    if (this.props.onBlur) {
      this.props.onBlur(event);
    }
    this.lastAztecEventType = 'blur';
  }
  onSelectionChange(start, end) {
    const hasChanged = this.selectionStart !== start || this.selectionEnd !== end;
    this.selectionStart = start;
    this.selectionEnd = end;

    // This is a manual selection change event if onChange was not triggered just before
    // and we did not just trigger a text update
    // `onChange` could be the last event and could have been triggered a long time ago so
    // this approach is not perfectly reliable.
    const isManual = this.lastAztecEventType !== 'input' && this.props.value?.toString() === this.value?.toString();
    if (hasChanged && isManual) {
      const value = this.createRecord();
      const activeFormats = (0, _richText.getActiveFormats)(value);
      this.setState({
        activeFormats
      });
    }
    this.props.onSelectionChange(start, end);
  }
  shouldDropEventFromAztec(event, logText) {
    const shouldDrop = !this.isIOS && event.nativeEvent.eventCount <= this.lastEventCount;
    if (shouldDrop) {
      window.console.log(`Dropping ${logText} from Aztec as its event counter is older than latest sent to the native side. Got ${event.nativeEvent.eventCount} but lastEventCount is ${this.lastEventCount}.`);
    }
    return shouldDrop;
  }

  /**
   * Determines whether the text input should receive focus after an update.
   * For cases where a RichText with a value is merged with an empty one.
   *
   * @param {Object} prevProps - The previous props of the component.
   * @return {boolean} True if the text input should receive focus, false otherwise.
   */
  shouldFocusTextInputAfterMerge(prevProps) {
    const {
      __unstableIsSelected: isSelected,
      blockIsSelected,
      selectionStart,
      selectionEnd,
      __unstableMobileNoFocusOnMount
    } = this.props;
    const {
      __unstableIsSelected: prevIsSelected,
      blockIsSelected: prevBlockIsSelected
    } = prevProps;
    const noSelectionValues = selectionStart === undefined && selectionEnd === undefined;
    const textInputWasNotFocused = !prevIsSelected && !isSelected;
    return !__unstableMobileNoFocusOnMount && noSelectionValues && textInputWasNotFocused && !prevBlockIsSelected && blockIsSelected;
  }
  onSelectionChangeFromAztec(start, end, text, event) {
    if (this.shouldDropEventFromAztec(event, 'onSelectionChange')) {
      return;
    }

    // `end` can be less than `start` on iOS
    // Let's fix that here so `rich-text/slice` can work properly.
    const realStart = Math.min(start, end);
    const realEnd = Math.max(start, end);

    // Check and dicsard stray event, where the text and selection is equal to the ones already cached.
    const contentWithoutRootTag = this.removeRootTagsProducedByAztec(event.nativeEvent.text);
    if (contentWithoutRootTag === this.value?.toString() && realStart === this.selectionStart && realEnd === this.selectionEnd) {
      return;
    }
    this.comesFromAztec = true;
    this.firedAfterTextChanged = true; // Selection change event always fires after the fact.

    // Update text before updating selection
    // Make sure there are changes made to the content before upgrading it upward.
    this.onTextUpdate(event);

    // Aztec can send us selection change events after it has lost focus.
    // For instance the autocorrect feature will complete a partially written
    // word when resigning focus, causing a selection change event.
    // Forwarding this selection change could cause this RichText to regain
    // focus and start a focus loop.
    //
    // See https://github.com/wordpress-mobile/gutenberg-mobile/issues/1696
    if (this.props.__unstableIsSelected) {
      this.onSelectionChange(realStart, realEnd);
    }
    // Update lastEventCount to prevent Aztec from re-rendering the content it just sent.
    this.lastEventCount = event.nativeEvent.eventCount;
    this.lastAztecEventType = 'selection change';
  }
  isEmpty() {
    return (0, _richText.isEmpty)(this.formatToValue(this.props.value));
  }
  formatToValue(value) {
    const {
      preserveWhiteSpace
    } = this.props;
    // Handle deprecated `children` and `node` sources.
    if (Array.isArray(value)) {
      return (0, _richText.create)({
        html: _blocks.childrenBlock.toHTML(value),
        preserveWhiteSpace
      });
    }
    if (this.props.format === 'string') {
      return (0, _richText.create)({
        html: value,
        preserveWhiteSpace
      });
    }

    // Guard for blocks passing `null` in onSplit callbacks. May be removed
    // if onSplit is revised to not pass a `null` value.
    if (value === null) {
      return (0, _richText.create)();
    }
    return value;
  }
  manipulateEventCounterToForceNativeToRefresh() {
    if (this.isIOS) {
      this.lastEventCount = undefined;
      return;
    }
    if (typeof this.lastEventCount !== 'undefined') {
      this.lastEventCount += 100; // bump by a hundred, hopefully native hasn't bombarded the JS side in the meantime.
    } // no need to bump when 'undefined' as native side won't receive the key when the value is undefined, and that will cause force updating anyway,
    //   see https://github.com/WordPress/gutenberg/blob/82e578dcc75e67891c750a41a04c1e31994192fc/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java#L213-L215
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.tagName !== this.props.tagName || nextProps.reversed !== this.props.reversed || nextProps.start !== this.props.start) {
      this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      this.value = undefined;
      return true;
    }

    // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call.
    // It was removed in https://github.com/WordPress/gutenberg/pull/12417 to fix undo/redo problem.

    // If the component is changed React side (undo/redo/merging/splitting/custom text actions)
    // we need to make sure the native is updated as well.

    // Also, don't trust the "this.lastContent" as on Android, incomplete text events arrive
    //  with only some of the text, while the virtual keyboard's suggestion system does its magic.
    // ** compare with this.lastContent for optimizing performance by not forcing Aztec with text it already has
    // , but compare with props.value to not lose "half word" text because of Android virtual keyb autosuggestion behavior
    if (typeof nextProps.value !== 'undefined' && typeof this.props.value !== 'undefined' && (!this.comesFromAztec || !this.firedAfterTextChanged) && nextProps.value?.toString() !== this.props.value?.toString()) {
      // Gutenberg seems to try to mirror the caret state even on events that only change the content so,
      //  let's force caret update if state has selection set.
      if (typeof nextProps.selectionStart !== 'undefined' && typeof nextProps.selectionEnd !== 'undefined') {
        this.needsSelectionUpdate = true;
      }
      this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
    }
    if (!this.comesFromAztec) {
      if (typeof nextProps.selectionStart !== 'undefined' && typeof nextProps.selectionEnd !== 'undefined' && nextProps.selectionStart !== this.props.selectionStart && nextProps.selectionStart !== this.selectionStart && nextProps.__unstableIsSelected) {
        this.needsSelectionUpdate = true;
        this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      }

      // For font size changes from a prop value a force refresh
      // is needed without the selection update.
      if (nextProps?.fontSize !== this.props?.fontSize) {
        this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      }
      if (nextProps?.style?.fontSize !== this.props?.style?.fontSize && nextState.currentFontSize !== this.state.currentFontSize || nextState.currentFontSize !== this.state.currentFontSize || nextProps?.style?.lineHeight !== this.props?.style?.lineHeight) {
        this.needsSelectionUpdate = true;
        this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      }
    }
    return true;
  }
  componentDidMount() {
    // Request focus if wrapping block is selected and parent hasn't inhibited the focus request. This method of focusing
    //  is trying to implement the web-side counterpart of BlockList's `focusTabbable` where the BlockList is focusing an
    //  inputbox by searching the DOM. We don't have the DOM in RN so, using the combination of blockIsSelected and __unstableMobileNoFocusOnMount
    //  to determine if we should focus the RichText.
    if (this.props.blockIsSelected && !this.props.__unstableMobileNoFocusOnMount) {
      this._editor.focus();
      this.onSelectionChange(this.props.selectionStart || 0, this.props.selectionEnd || 0);
    }
  }
  componentDidUpdate(prevProps) {
    const {
      style,
      tagName
    } = this.props;
    const {
      currentFontSize
    } = this.state;
    if (this.props.value?.toString() !== this.value?.toString()) {
      this.value = this.props.value;
    }
    const {
      __unstableIsSelected: prevIsSelected
    } = prevProps;
    const {
      __unstableIsSelected: isSelected
    } = this.props;
    if (isSelected && !prevIsSelected) {
      this._editor.focus();
      // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange
      // if its internal value hasn't change. When created, default value is 0, 0.
      this.onSelectionChange(this.props.selectionStart || 0, this.props.selectionEnd || 0);
    } else if (this.shouldFocusTextInputAfterMerge(prevProps)) {
      var _this$value$toString$;
      // Since this is happening when merging blocks, the selection should be at the last character position.
      // As a fallback the internal selectionEnd value is used.
      const lastCharacterPosition = (_this$value$toString$ = this.value?.toString().length) !== null && _this$value$toString$ !== void 0 ? _this$value$toString$ : this.selectionEnd;
      this._editor.focus();
      this.props.onSelectionChange(lastCharacterPosition, lastCharacterPosition);
    } else if (!isSelected && prevIsSelected) {
      this._editor.blur();
    }

    // For font size values changes from the font size picker
    // we compare previous values to refresh the selected font size,
    // this is also used when the tag name changes
    // e.g Heading block and a level change like h1->h2.
    const currentFontSizeStyle = this.getParsedFontSize(style?.fontSize);
    const prevFontSizeStyle = this.getParsedFontSize(prevProps?.style?.fontSize);
    const isDifferentTag = prevProps.tagName !== tagName;
    if (currentFontSize && (currentFontSizeStyle || prevFontSizeStyle) && currentFontSizeStyle !== currentFontSize || isDifferentTag) {
      this.setState({
        currentFontSize: this.getFontSize(this.props)
      });
    }
  }
  componentWillUnmount() {
    const {
      clearCurrentSelectionOnUnmount
    } = this.props;

    // There are cases when the component is unmounted e.g. scrolling in a
    // long post due to virtualization, so the block selection needs to be cleared
    // so it doesn't auto-focus when it's added back.
    if (this._editor?.isFocused()) {
      clearCurrentSelectionOnUnmount?.();
    }
  }
  getHtmlToRender(record, tagName) {
    // Save back to HTML from React tree.
    let value = this.valueToFormat(record);
    if (value === undefined) {
      this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side
      value = '';
    }
    // On android if content is empty we need to send no content or else the placeholder will not show.
    if (!this.isIOS && (value?.toString() === '' || value?.toString() === EMPTY_PARAGRAPH_TAGS)) {
      return '';
    }
    if (tagName) {
      let extraAttributes = ``;
      if (tagName === `ol`) {
        if (this.props.reversed) {
          extraAttributes += ` reversed`;
        }
        if (this.props.start) {
          extraAttributes += ` start=${this.props.start}`;
        }
      }
      value = `<${tagName}${extraAttributes}>${value}</${tagName}>`;
    }
    return value;
  }
  getEditableProps() {
    return {
      // Overridable props.
      style: {},
      className: 'rich-text',
      onKeyDown: () => null
    };
  }
  getParsedFontSize(fontSize) {
    var _getPxFromCssUnit;
    const {
      height,
      width
    } = _reactNative.Dimensions.get('window');
    const cssUnitOptions = {
      height,
      width,
      fontSize: DEFAULT_FONT_SIZE
    };
    if (!fontSize) {
      return fontSize;
    }
    const selectedPxValue = (_getPxFromCssUnit = (0, _components.getPxFromCssUnit)(fontSize, cssUnitOptions)) !== null && _getPxFromCssUnit !== void 0 ? _getPxFromCssUnit : DEFAULT_FONT_SIZE;
    return parseFloat(selectedPxValue);
  }
  getFontSize(props) {
    const {
      baseGlobalStyles,
      tagName,
      fontSize,
      style
    } = props;
    const tagNameFontSize = baseGlobalStyles?.elements?.[tagName]?.typography?.fontSize;
    let newFontSize = DEFAULT_FONT_SIZE;

    // Disables line-height rendering for pre elements until we fix some issues with AztecAndroid.
    if (tagName === 'pre' && !this.isIOS) {
      return undefined;
    }

    // For block-based themes, get the default editor font size.
    if (baseGlobalStyles?.typography?.fontSize && tagName === 'p') {
      newFontSize = baseGlobalStyles?.typography?.fontSize;
    }

    // For block-based themes, get the default element font size
    // e.g h1, h2.
    if (tagNameFontSize) {
      newFontSize = tagNameFontSize;
    }

    // For font size values provided from the styles,
    // usually from values set from the font size picker.
    if (style?.fontSize) {
      newFontSize = style.fontSize;
    }

    // Fall-back to a font size provided from its props (if there's any)
    // and there are no other default values to use.
    if (fontSize && !tagNameFontSize && !style?.fontSize) {
      newFontSize = fontSize;
    }

    // We need to always convert to px units because the selected value
    // could be coming from the web where it could be stored as a different unit.
    const selectedPxValue = this.getParsedFontSize(newFontSize);
    return selectedPxValue;
  }
  getLineHeight() {
    const {
      baseGlobalStyles,
      tagName,
      lineHeight,
      style
    } = this.props;
    const tagNameLineHeight = baseGlobalStyles?.elements?.[tagName]?.typography?.lineHeight;
    let newLineHeight;

    // Disables line-height rendering for pre elements until we fix some issues with AztecAndroid.
    if (tagName === 'pre' && !this.isIOS) {
      return undefined;
    }
    if (!this.getIsBlockBasedTheme()) {
      return;
    }

    // For block-based themes, get the default editor line height.
    if (baseGlobalStyles?.typography?.lineHeight && tagName === 'p') {
      newLineHeight = parseFloat(baseGlobalStyles?.typography?.lineHeight);
    }

    // For block-based themes, get the default element line height
    // e.g h1, h2.
    if (tagNameLineHeight) {
      newLineHeight = parseFloat(tagNameLineHeight);
    }

    // For line height values provided from the styles,
    // usually from values set from the line height picker.
    if (style?.lineHeight) {
      newLineHeight = parseFloat(style.lineHeight);
    }

    // Fall-back to a line height provided from its props (if there's any)
    // and there are no other default values to use.
    if (lineHeight && !tagNameLineHeight && !style?.lineHeight) {
      newLineHeight = lineHeight;
    }

    // Check the final value is not over the minimum supported value.
    if (newLineHeight && newLineHeight < MIN_LINE_HEIGHT) {
      newLineHeight = MIN_LINE_HEIGHT;
    }

    // Until we parse CSS values correctly, avoid passing NaN values to Aztec
    if (isNaN(newLineHeight)) {
      return undefined;
    }
    return newLineHeight;
  }
  getIsBlockBasedTheme() {
    const {
      baseGlobalStyles
    } = this.props;
    return baseGlobalStyles && Object.entries(baseGlobalStyles).length !== 0;
  }
  getBlockUseDefaultFont() {
    // For block-based themes it enables using the defaultFont
    // in Aztec for iOS so it allows customizing the font size
    // for the Preformatted/Code and Heading blocks.
    if (!this.isIOS) {
      return;
    }
    const {
      tagName
    } = this.props;
    const isBlockBasedTheme = this.getIsBlockBasedTheme();
    const tagsToMatch = /pre|h([1-6])$/gm;
    return isBlockBasedTheme && tagsToMatch.test(tagName);
  }
  getLinkTextColor(defaultColor) {
    const {
      style
    } = this.props;
    const customColor = style?.linkColor && (0, _colord.colord)(style.linkColor);
    return customColor && customColor.isValid() ? customColor.toHex() : defaultColor;
  }
  getPlaceholderTextColor() {
    var _ref, _ref2;
    const {
      baseGlobalStyles,
      getStylesFromColorScheme,
      placeholderTextColor,
      style
    } = this.props;

    // Default placeholder text color.
    const placeholderStyle = getStylesFromColorScheme(_style.default.richTextPlaceholder, _style.default.richTextPlaceholderDark);
    const {
      color: defaultPlaceholderTextColor
    } = placeholderStyle;
    // Custom 63% opacity for theme and inherited colors.
    const placeholderOpacity = 'A1';

    // Determine inherited placeholder color if available.
    const inheritPlaceholderColor = style?.placeholderColor ? `${style.placeholderColor}${placeholderOpacity}` : undefined;

    // If using block-based themes, derive the placeholder color from global styles.
    const globalStylesPlaceholderColor = baseGlobalStyles?.color?.text ? `${baseGlobalStyles.color.text}${placeholderOpacity}` : undefined;
    return (_ref = (_ref2 = inheritPlaceholderColor !== null && inheritPlaceholderColor !== void 0 ? inheritPlaceholderColor : placeholderTextColor) !== null && _ref2 !== void 0 ? _ref2 : globalStylesPlaceholderColor) !== null && _ref !== void 0 ? _ref : defaultPlaceholderTextColor;
  }
  render() {
    const {
      tagName,
      style,
      __unstableIsSelected: isSelected,
      children,
      getStylesFromColorScheme,
      minWidth,
      maxWidth,
      formatTypes,
      parentBlockStyles,
      accessibilityLabel,
      disableEditingMenu = false,
      baseGlobalStyles,
      selectionStart,
      selectionEnd,
      disableSuggestions,
      containerWidth
    } = this.props;
    const {
      currentFontSize
    } = this.state;
    const record = this.getRecord();
    const html = this.getHtmlToRender(record, tagName);
    const editableProps = this.getEditableProps();
    const blockUseDefaultFont = this.getBlockUseDefaultFont();
    const fontSize = currentFontSize;
    const lineHeight = this.getLineHeight();
    const {
      color: defaultColor,
      textDecorationColor: defaultTextDecorationColor,
      fontFamily: defaultFontFamily
    } = getStylesFromColorScheme(_style.default.richText, _style.default.richTextDark);
    const linkTextColor = this.getLinkTextColor(defaultTextDecorationColor);
    const currentSelectionStart = selectionStart !== null && selectionStart !== void 0 ? selectionStart : 0;
    const currentSelectionEnd = selectionEnd !== null && selectionEnd !== void 0 ? selectionEnd : 0;
    let selection = null;
    if (this.needsSelectionUpdate) {
      this.needsSelectionUpdate = false;
      selection = {
        start: currentSelectionStart,
        end: currentSelectionEnd
      };

      // On AztecAndroid, setting the caret to an out-of-bounds position will crash the editor so, let's check for some cases.
      if (!this.isIOS) {
        // The following regular expression is used in Aztec here:
        // https://github.com/wordpress-mobile/AztecEditor-Android/blob/b1fad439d56fa6d4aa0b78526fef355c59d00dd3/aztec/src/main/kotlin/org/wordpress/aztec/AztecParser.kt#L656
        const brBeforeParaMatches = html.match(/(<br>)+<\/p>$/g);
        if (brBeforeParaMatches) {
          console.warn('Oops, BR tag(s) at the end of content. Aztec will remove them, adapting the selection...');
          const count = (brBeforeParaMatches[0].match(/br/g) || []).length;
          if (count > 0) {
            let newSelectionStart = currentSelectionStart - count;
            if (newSelectionStart < 0) {
              newSelectionStart = 0;
            }
            let newSelectionEnd = currentSelectionEnd - count;
            if (newSelectionEnd < 0) {
              newSelectionEnd = 0;
            }
            selection = {
              start: newSelectionStart,
              end: newSelectionEnd
            };
          }
        }
      }
    }
    if (this.comesFromAztec) {
      this.comesFromAztec = false;
      this.firedAfterTextChanged = false;
    }

    // Logic below assures that `RichText` width will always have equal value when container is almost fully filled.
    const width = maxWidth && this.state.width && maxWidth - this.state.width < 10 ? maxWidth : this.state.width;
    const containerStyles = [style?.padding && style?.backgroundColor && {
      padding: style.padding,
      backgroundColor: style.backgroundColor
    }, containerWidth && {
      width: containerWidth
    }];
    const defaultSelectionColor = getStylesFromColorScheme(_style.default['rich-text-selection'], _style.default['rich-text-selection--dark']).color;
    const selectionColor = getSelectionColor(this.props.selectionColor, defaultSelectionColor, baseGlobalStyles, this.getIsBlockBasedTheme());
    const EditableView = props => {
      this.customEditableOnKeyDown = props?.onKeyDown;
      return /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {});
    };
    return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
      style: containerStyles,
      children: [children && children({
        isSelected,
        value: record,
        onChange: this.onFormatChange,
        onFocus: () => {},
        editableProps,
        editableTagName: EditableView
      }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeAztec.default, {
        accessibilityLabel: accessibilityLabel,
        ref: ref => {
          this._editor = ref;
          if (this.props.nativeEditorRef) {
            this.props.nativeEditorRef(ref);
          }
        },
        style: {
          backgroundColor: _style.default.richText.backgroundColor,
          ...style,
          ...(this.isIOS && minWidth && maxWidth ? {
            width
          } : {
            maxWidth
          }),
          minHeight: this.state.height
        },
        blockUseDefaultFont: blockUseDefaultFont,
        text: {
          text: html,
          eventCount: this.lastEventCount,
          selection,
          linkTextColor,
          tag: tagName
        },
        placeholder: this.props.placeholder,
        placeholderTextColor: this.getPlaceholderTextColor(),
        deleteEnter: this.props.deleteEnter,
        onChange: this.onChangeFromAztec,
        onFocus: this.onFocus,
        onBlur: this.onBlur,
        onKeyDown: this.onKeyDown,
        triggerKeyCodes: disableEditingMenu ? [] : this.suggestionOptions().map(op => op.triggerChar),
        onPaste: this.onPaste,
        activeFormats: this.getActiveFormatNames(record),
        onContentSizeChange: this.onContentSizeChange,
        onSelectionChange: this.onSelectionChangeFromAztec,
        blockType: {
          tag: tagName
        },
        color: style && style.color || parentBlockStyles && parentBlockStyles.color || baseGlobalStyles && baseGlobalStyles?.color?.text || defaultColor,
        maxImagesWidth: 200,
        fontFamily: this.props.fontFamily || defaultFontFamily,
        fontSize: fontSize,
        lineHeight: lineHeight,
        fontWeight: this.props.fontWeight,
        fontStyle: this.props.fontStyle,
        disableEditingMenu: disableEditingMenu,
        isMultiline: false,
        textAlign: this.props.textAlign,
        ...(this.isIOS ? {
          maxWidth
        } : {}),
        minWidth: minWidth,
        id: this.props.id,
        selectionColor: selectionColor,
        disableAutocorrection: this.props.disableAutocorrection
      }), isSelected && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
        children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_formatEdit.default, {
          forwardedRef: this._editor,
          formatTypes: formatTypes,
          value: record,
          onChange: this.onFormatChange,
          onFocus: () => {}
        }), !disableSuggestions && /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.BlockFormatControls, {
          children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_toolbarButtonWithOptions.default, {
            options: this.suggestionOptions()
          })
        })]
      })]
    });
  }
}
exports.RichText = RichText;
RichText.defaultProps = {
  format: 'string',
  value: '',
  tagName: 'div'
};
const withFormatTypes = WrappedComponent => props => {
  const {
    clientId,
    identifier,
    withoutInteractiveFormatting,
    allowedFormats
  } = props;
  const {
    formatTypes
  } = (0, _useFormatTypes.useFormatTypes)({
    clientId,
    identifier,
    withoutInteractiveFormatting,
    allowedFormats
  });
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(WrappedComponent, {
    ...props,
    formatTypes: formatTypes
  });
};
var _default = exports.default = (0, _compose.compose)([(0, _data.withSelect)((select, {
  clientId
}) => {
  const {
    getBlockParents,
    getBlock,
    getSettings
  } = select('core/block-editor');
  const parents = getBlockParents(clientId, true);
  const parentBlock = parents ? getBlock(parents[0]) : undefined;
  const parentBlockStyles = parentBlock?.attributes?.childrenStyles;
  const settings = getSettings();
  const baseGlobalStyles = settings?.__experimentalGlobalStylesBaseStyles;
  const colorPalettes = settings?.__experimentalFeatures?.color?.palette;
  const colorPalette = colorPalettes ? flatColorPalettes(colorPalettes) : settings?.colors;
  return {
    areMentionsSupported: settings?.capabilities?.mentions === true,
    areXPostsSupported: settings?.capabilities?.xposts === true,
    parentBlockStyles,
    baseGlobalStyles,
    colorPalette
  };
}), _compose.withPreferredColorScheme, withFormatTypes])(RichText);
//# sourceMappingURL=index.native.js.map