/* wp:polyfill */
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = Autocomplete;
exports.useAutocomplete = useAutocomplete;
exports.useAutocompleteProps = useAutocompleteProps;
var _removeAccents = _interopRequireDefault(require("remove-accents"));
var _element = require("@wordpress/element");
var _compose = require("@wordpress/compose");
var _richText = require("@wordpress/rich-text");
var _a11y = require("@wordpress/a11y");
var _keycodes = require("@wordpress/keycodes");
var _autocompleterUi = require("./autocompleter-ui");
var _strings = require("../utils/strings");
var _withIgnoreImeEvents = require("../utils/with-ignore-ime-events");
var _jsxRuntime = require("react/jsx-runtime");
/**
 * External dependencies
 */

/**
 * WordPress dependencies
 */

/**
 * Internal dependencies
 */

const getNodeText = node => {
  if (node === null) {
    return '';
  }
  switch (typeof node) {
    case 'string':
    case 'number':
      return node.toString();
      break;
    case 'boolean':
      return '';
      break;
    case 'object':
      {
        if (node instanceof Array) {
          return node.map(getNodeText).join('');
        }
        if ('props' in node) {
          return getNodeText(node.props.children);
        }
        break;
      }
    default:
      return '';
  }
  return '';
};
const EMPTY_FILTERED_OPTIONS = [];

// Used for generating the instance ID
const AUTOCOMPLETE_HOOK_REFERENCE = {};
function useAutocomplete({
  record,
  onChange,
  onReplace,
  completers,
  contentRef
}) {
  const instanceId = (0, _compose.useInstanceId)(AUTOCOMPLETE_HOOK_REFERENCE);
  const [selectedIndex, setSelectedIndex] = (0, _element.useState)(0);
  const [filteredOptions, setFilteredOptions] = (0, _element.useState)(EMPTY_FILTERED_OPTIONS);
  const [filterValue, setFilterValue] = (0, _element.useState)('');
  const [autocompleter, setAutocompleter] = (0, _element.useState)(null);
  const [AutocompleterUI, setAutocompleterUI] = (0, _element.useState)(null);
  const backspacingRef = (0, _element.useRef)(false);
  function insertCompletion(replacement) {
    if (autocompleter === null) {
      return;
    }
    const end = record.start;
    const start = end - autocompleter.triggerPrefix.length - filterValue.length;
    const toInsert = (0, _richText.create)({
      html: (0, _element.renderToString)(replacement)
    });
    onChange((0, _richText.insert)(record, toInsert, start, end));
  }
  function select(option) {
    const {
      getOptionCompletion
    } = autocompleter || {};
    if (option.isDisabled) {
      return;
    }
    if (getOptionCompletion) {
      const completion = getOptionCompletion(option.value, filterValue);
      const isCompletionObject = obj => {
        return obj !== null && typeof obj === 'object' && 'action' in obj && obj.action !== undefined && 'value' in obj && obj.value !== undefined;
      };
      const completionObject = isCompletionObject(completion) ? completion : {
        action: 'insert-at-caret',
        value: completion
      };
      if ('replace' === completionObject.action) {
        onReplace([completionObject.value]);
        // When replacing, the component will unmount, so don't reset
        // state (below) on an unmounted component.
        return;
      } else if ('insert-at-caret' === completionObject.action) {
        insertCompletion(completionObject.value);
      }
    }

    // Reset autocomplete state after insertion rather than before
    // so insertion events don't cause the completion menu to redisplay.
    reset();
  }
  function reset() {
    setSelectedIndex(0);
    setFilteredOptions(EMPTY_FILTERED_OPTIONS);
    setFilterValue('');
    setAutocompleter(null);
    setAutocompleterUI(null);
  }

  /**
   * Load options for an autocompleter.
   *
   * @param {Array} options
   */
  function onChangeOptions(options) {
    setSelectedIndex(options.length === filteredOptions.length ? selectedIndex : 0);
    setFilteredOptions(options);
  }
  function handleKeyDown(event) {
    backspacingRef.current = event.key === 'Backspace';
    if (!autocompleter) {
      return;
    }
    if (filteredOptions.length === 0) {
      return;
    }
    if (event.defaultPrevented) {
      return;
    }
    switch (event.key) {
      case 'ArrowUp':
        {
          const newIndex = (selectedIndex === 0 ? filteredOptions.length : selectedIndex) - 1;
          setSelectedIndex(newIndex);
          // See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
          if ((0, _keycodes.isAppleOS)()) {
            (0, _a11y.speak)(getNodeText(filteredOptions[newIndex].label), 'assertive');
          }
          break;
        }
      case 'ArrowDown':
        {
          const newIndex = (selectedIndex + 1) % filteredOptions.length;
          setSelectedIndex(newIndex);
          if ((0, _keycodes.isAppleOS)()) {
            (0, _a11y.speak)(getNodeText(filteredOptions[newIndex].label), 'assertive');
          }
          break;
        }
      case 'Escape':
        setAutocompleter(null);
        setAutocompleterUI(null);
        event.preventDefault();
        break;
      case 'Enter':
        select(filteredOptions[selectedIndex]);
        break;
      case 'ArrowLeft':
      case 'ArrowRight':
        reset();
        return;
      default:
        return;
    }

    // Any handled key should prevent original behavior. This relies on
    // the early return in the default case.
    event.preventDefault();
  }

  // textContent is a primitive (string), memoizing is not strictly necessary
  // but this is a preemptive performance improvement, since the autocompleter
  // is a potential bottleneck for the editor type metric.
  const textContent = (0, _element.useMemo)(() => {
    if ((0, _richText.isCollapsed)(record)) {
      return (0, _richText.getTextContent)((0, _richText.slice)(record, 0));
    }
    return '';
  }, [record]);
  (0, _element.useEffect)(() => {
    if (!textContent) {
      if (autocompleter) {
        reset();
      }
      return;
    }

    // Find the completer with the highest triggerPrefix index in the
    // textContent.
    const completer = completers.reduce((lastTrigger, currentCompleter) => {
      const triggerIndex = textContent.lastIndexOf(currentCompleter.triggerPrefix);
      const lastTriggerIndex = lastTrigger !== null ? textContent.lastIndexOf(lastTrigger.triggerPrefix) : -1;
      return triggerIndex > lastTriggerIndex ? currentCompleter : lastTrigger;
    }, null);
    if (!completer) {
      if (autocompleter) {
        reset();
      }
      return;
    }
    const {
      allowContext,
      triggerPrefix
    } = completer;
    const triggerIndex = textContent.lastIndexOf(triggerPrefix);
    const textWithoutTrigger = textContent.slice(triggerIndex + triggerPrefix.length);
    const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit.
    // This is a final barrier to prevent the effect from completing with
    // an extremely long string, which causes the editor to slow-down
    // significantly. This could happen, for example, if `matchingWhileBackspacing`
    // is true and one of the "words" end up being too long. If that's the case,
    // it will be caught by this guard.
    if (tooDistantFromTrigger) {
      return;
    }
    const mismatch = filteredOptions.length === 0;
    const wordsFromTrigger = textWithoutTrigger.split(/\s/);
    // We need to allow the effect to run when not backspacing and if there
    // was a mismatch. i.e when typing a trigger + the match string or when
    // clicking in an existing trigger word on the page. We do that if we
    // detect that we have one word from trigger in the current textual context.
    //
    // Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and
    // allow the effect to run. It will run until there's a mismatch.
    const hasOneTriggerWord = wordsFromTrigger.length === 1;
    // This is used to allow the effect to run when backspacing and if
    // "touching" a word that "belongs" to a trigger. We consider a "trigger
    // word" any word up to the limit of 3 from the trigger character.
    // Anything beyond that is ignored if there's a mismatch. This allows
    // us to "escape" a mismatch when backspacing, but still imposing some
    // sane limits.
    //
    // Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but
    // if the user presses backspace here, it will show the completion popup again.
    const matchingWhileBackspacing = backspacingRef.current && wordsFromTrigger.length <= 3;
    if (mismatch && !(matchingWhileBackspacing || hasOneTriggerWord)) {
      if (autocompleter) {
        reset();
      }
      return;
    }
    const textAfterSelection = (0, _richText.getTextContent)((0, _richText.slice)(record, undefined, (0, _richText.getTextContent)(record).length));
    if (allowContext && !allowContext(textContent.slice(0, triggerIndex), textAfterSelection)) {
      if (autocompleter) {
        reset();
      }
      return;
    }
    if (/^\s/.test(textWithoutTrigger) || /\s\s+$/.test(textWithoutTrigger)) {
      if (autocompleter) {
        reset();
      }
      return;
    }
    if (!/[\u0000-\uFFFF]*$/.test(textWithoutTrigger)) {
      if (autocompleter) {
        reset();
      }
      return;
    }
    const safeTrigger = (0, _strings.escapeRegExp)(completer.triggerPrefix);
    const text = (0, _removeAccents.default)(textContent);
    const match = text.slice(text.lastIndexOf(completer.triggerPrefix)).match(new RegExp(`${safeTrigger}([\u0000-\uFFFF]*)$`));
    const query = match && match[1];
    setAutocompleter(completer);
    setAutocompleterUI(() => completer !== autocompleter ? (0, _autocompleterUi.getAutoCompleterUI)(completer) : AutocompleterUI);
    setFilterValue(query === null ? '' : query);
    // We want to avoid introducing unexpected side effects.
    // See https://github.com/WordPress/gutenberg/pull/41820
  }, [textContent]);
  const {
    key: selectedKey = ''
  } = filteredOptions[selectedIndex] || {};
  const {
    className
  } = autocompleter || {};
  const isExpanded = !!autocompleter && filteredOptions.length > 0;
  const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : undefined;
  const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null;
  const hasSelection = record.start !== undefined;
  return {
    listBoxId,
    activeId,
    onKeyDown: (0, _withIgnoreImeEvents.withIgnoreIMEEvents)(handleKeyDown),
    popover: hasSelection && AutocompleterUI && /*#__PURE__*/(0, _jsxRuntime.jsx)(AutocompleterUI, {
      className: className,
      filterValue: filterValue,
      instanceId: instanceId,
      listBoxId: listBoxId,
      selectedIndex: selectedIndex,
      onChangeOptions: onChangeOptions,
      onSelect: select,
      value: record,
      contentRef: contentRef,
      reset: reset
    })
  };
}
function useLastDifferentValue(value) {
  const history = (0, _element.useRef)(new Set());
  history.current.add(value);

  // Keep the history size to 2.
  if (history.current.size > 2) {
    history.current.delete(Array.from(history.current)[0]);
  }
  return Array.from(history.current)[0];
}
function useAutocompleteProps(options) {
  const ref = (0, _element.useRef)(null);
  const onKeyDownRef = (0, _element.useRef)();
  const {
    record
  } = options;
  const previousRecord = useLastDifferentValue(record);
  const {
    popover,
    listBoxId,
    activeId,
    onKeyDown
  } = useAutocomplete({
    ...options,
    contentRef: ref
  });
  onKeyDownRef.current = onKeyDown;
  const mergedRefs = (0, _compose.useMergeRefs)([ref, (0, _compose.useRefEffect)(element => {
    function _onKeyDown(event) {
      onKeyDownRef.current?.(event);
    }
    element.addEventListener('keydown', _onKeyDown);
    return () => {
      element.removeEventListener('keydown', _onKeyDown);
    };
  }, [])]);

  // We only want to show the popover if the user has typed something.
  const didUserInput = record.text !== previousRecord?.text;
  if (!didUserInput) {
    return {
      ref: mergedRefs
    };
  }
  return {
    ref: mergedRefs,
    children: popover,
    'aria-autocomplete': listBoxId ? 'list' : undefined,
    'aria-owns': listBoxId,
    'aria-activedescendant': activeId
  };
}
function Autocomplete({
  children,
  isSelected,
  ...options
}) {
  const {
    popover,
    ...props
  } = useAutocomplete(options);
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
    children: [children(props), isSelected && popover]
  });
}
//# sourceMappingURL=index.js.map