"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _clsx = _interopRequireDefault(require("clsx"));
var _components = require("@wordpress/components");
var _i18n = require("@wordpress/i18n");
var _element = require("@wordpress/element");
var _dom = require("@wordpress/dom");
var _keycodes = require("@wordpress/keycodes");
var _isShallowEqual = require("@wordpress/is-shallow-equal");
var _data = require("@wordpress/data");
var _preferences = require("@wordpress/preferences");
var _icons = require("@wordpress/icons");
var _settingsDrawer = _interopRequireDefault(require("./settings-drawer"));
var _searchInput = _interopRequireDefault(require("./search-input"));
var _linkPreview = _interopRequireDefault(require("./link-preview"));
var _settings = _interopRequireDefault(require("./settings"));
var _useCreatePage = _interopRequireDefault(require("./use-create-page"));
var _useInternalValue = _interopRequireDefault(require("./use-internal-value"));
var _viewerSlot = require("./viewer-slot");
var _constants = require("./constants");
var _jsxRuntime = require("react/jsx-runtime");
/**
 * External dependencies
 */

/**
 * WordPress dependencies
 */

/**
 * Internal dependencies
 */

/**
 * Default properties associated with a link control value.
 *
 * @typedef WPLinkControlDefaultValue
 *
 * @property {string}   url           Link URL.
 * @property {string=}  title         Link title.
 * @property {boolean=} opensInNewTab Whether link should open in a new browser
 *                                    tab. This value is only assigned if not
 *                                    providing a custom `settings` prop.
 */

/* eslint-disable jsdoc/valid-types */
/**
 * Custom settings values associated with a link.
 *
 * @typedef {{[setting:string]:any}} WPLinkControlSettingsValue
 */
/* eslint-enable */

/**
 * Custom settings values associated with a link.
 *
 * @typedef WPLinkControlSetting
 *
 * @property {string} id    Identifier to use as property for setting value.
 * @property {string} title Human-readable label to show in user interface.
 */

/**
 * Properties associated with a link control value, composed as a union of the
 * default properties and any custom settings values.
 *
 * @typedef {WPLinkControlDefaultValue&WPLinkControlSettingsValue} WPLinkControlValue
 */

/** @typedef {(nextValue:WPLinkControlValue)=>void} WPLinkControlOnChangeProp */

/**
 * Properties associated with a search suggestion used within the LinkControl.
 *
 * @typedef WPLinkControlSuggestion
 *
 * @property {string} id    Identifier to use to uniquely identify the suggestion.
 * @property {string} type  Identifies the type of the suggestion (eg: `post`,
 *                          `page`, `url`...etc)
 * @property {string} title Human-readable label to show in user interface.
 * @property {string} url   A URL for the suggestion.
 */

/** @typedef {(title:string)=>WPLinkControlSuggestion} WPLinkControlCreateSuggestionProp */

/**
 * @typedef WPLinkControlProps
 *
 * @property {(WPLinkControlSetting[])=}  settings                   An array of settings objects. Each object will used to
 *                                                                   render a `ToggleControl` for that setting.
 * @property {boolean=}                   forceIsEditingLink         If passed as either `true` or `false`, controls the
 *                                                                   internal editing state of the component to respective
 *                                                                   show or not show the URL input field.
 * @property {WPLinkControlValue=}        value                      Current link value.
 * @property {WPLinkControlOnChangeProp=} onChange                   Value change handler, called with the updated value if
 *                                                                   the user selects a new link or updates settings.
 * @property {boolean=}                   noDirectEntry              Whether to allow turning a URL-like search query directly into a link.
 * @property {boolean=}                   showSuggestions            Whether to present suggestions when typing the URL.
 * @property {boolean=}                   showInitialSuggestions     Whether to present initial suggestions immediately.
 * @property {boolean=}                   withCreateSuggestion       Whether to allow creation of link value from suggestion.
 * @property {Object=}                    suggestionsQuery           Query parameters to pass along to wp.blockEditor.__experimentalFetchLinkSuggestions.
 * @property {boolean=}                   noURLSuggestion            Whether to add a fallback suggestion which treats the search query as a URL.
 * @property {boolean=}                   hasTextControl             Whether to add a text field to the UI to update the value.title.
 * @property {string|Function|undefined}  createSuggestionButtonText The text to use in the button that calls createSuggestion.
 * @property {Function}                   renderControlBottom        Optional controls to be rendered at the bottom of the component.
 */const noop = () => {};
const PREFERENCE_SCOPE = 'core/block-editor';
const PREFERENCE_KEY = 'linkControlSettingsDrawer';

/**
 * Renders a link control. A link control is a controlled input which maintains
 * a value associated with a link (HTML anchor element) and relevant settings
 * for how that link is expected to behave.
 *
 * @param {WPLinkControlProps} props Component props.
 */
function LinkControl({
  searchInputPlaceholder,
  value,
  settings = _constants.DEFAULT_LINK_SETTINGS,
  onChange = noop,
  onRemove,
  onCancel,
  noDirectEntry = false,
  showSuggestions = true,
  showInitialSuggestions,
  forceIsEditingLink,
  createSuggestion,
  withCreateSuggestion,
  inputValue: propInputValue = '',
  suggestionsQuery = {},
  noURLSuggestion = false,
  createSuggestionButtonText,
  hasRichPreviews = false,
  hasTextControl = false,
  renderControlBottom = null
}) {
  if (withCreateSuggestion === undefined && createSuggestion) {
    withCreateSuggestion = true;
  }
  const [settingsOpen, setSettingsOpen] = (0, _element.useState)(false);
  const {
    advancedSettingsPreference
  } = (0, _data.useSelect)(select => {
    var _prefsStore$get;
    const prefsStore = select(_preferences.store);
    return {
      advancedSettingsPreference: (_prefsStore$get = prefsStore.get(PREFERENCE_SCOPE, PREFERENCE_KEY)) !== null && _prefsStore$get !== void 0 ? _prefsStore$get : false
    };
  }, []);
  const {
    set: setPreference
  } = (0, _data.useDispatch)(_preferences.store);

  /**
   * Sets the open/closed state of the Advanced Settings Drawer,
   * optionlly persisting the state to the user's preferences.
   *
   * Note that Block Editor components can be consumed by non-WordPress
   * environments which may not have preferences setup.
   * Therefore a local state is also  used as a fallback.
   *
   * @param {boolean} prefVal the open/closed state of the Advanced Settings Drawer.
   */
  const setSettingsOpenWithPreference = prefVal => {
    if (setPreference) {
      setPreference(PREFERENCE_SCOPE, PREFERENCE_KEY, prefVal);
    }
    setSettingsOpen(prefVal);
  };

  // Block Editor components can be consumed by non-WordPress environments
  // which may not have these preferences setup.
  // Therefore a local state is used as a fallback.
  const isSettingsOpen = advancedSettingsPreference || settingsOpen;
  const isMounting = (0, _element.useRef)(true);
  const wrapperNode = (0, _element.useRef)();
  const textInputRef = (0, _element.useRef)();
  const isEndingEditWithFocus = (0, _element.useRef)(false);
  const settingsKeys = settings.map(({
    id
  }) => id);
  const [internalControlValue, setInternalControlValue, setInternalURLInputValue, setInternalTextInputValue, createSetInternalSettingValueHandler] = (0, _useInternalValue.default)(value);
  const valueHasChanges = value && !(0, _isShallowEqual.isShallowEqualObjects)(internalControlValue, value);
  const [isEditingLink, setIsEditingLink] = (0, _element.useState)(forceIsEditingLink !== undefined ? forceIsEditingLink : !value || !value.url);
  const {
    createPage,
    isCreatingPage,
    errorMessage
  } = (0, _useCreatePage.default)(createSuggestion);
  (0, _element.useEffect)(() => {
    if (forceIsEditingLink === undefined) {
      return;
    }
    setIsEditingLink(forceIsEditingLink);
  }, [forceIsEditingLink]);
  (0, _element.useEffect)(() => {
    // We don't auto focus into the Link UI on mount
    // because otherwise using the keyboard to select text
    // *within* the link format is not possible.
    if (isMounting.current) {
      return;
    }

    // Scenario - when:
    // - switching between editable and non editable LinkControl
    // - clicking on a link
    // ...then move focus to the *first* element to avoid focus loss
    // and to ensure focus is *within* the Link UI.
    const nextFocusTarget = _dom.focus.focusable.find(wrapperNode.current)[0] || wrapperNode.current;
    nextFocusTarget.focus();
    isEndingEditWithFocus.current = false;
  }, [isEditingLink, isCreatingPage]);

  // The component mounting reference is maintained separately
  // to correctly reset values in `StrictMode`.
  (0, _element.useEffect)(() => {
    isMounting.current = false;
    return () => {
      isMounting.current = true;
    };
  }, []);
  const hasLinkValue = value?.url?.trim()?.length > 0;

  /**
   * Cancels editing state and marks that focus may need to be restored after
   * the next render, if focus was within the wrapper when editing finished.
   */
  const stopEditing = () => {
    isEndingEditWithFocus.current = !!wrapperNode.current?.contains(wrapperNode.current.ownerDocument.activeElement);
    setIsEditingLink(false);
  };
  const handleSelectSuggestion = updatedValue => {
    // Suggestions may contains "settings" values (e.g. `opensInNewTab`)
    // which should not overide any existing settings values set by the
    // user. This filters out any settings values from the suggestion.
    const nonSettingsChanges = Object.keys(updatedValue).reduce((acc, key) => {
      if (!settingsKeys.includes(key)) {
        acc[key] = updatedValue[key];
      }
      return acc;
    }, {});
    onChange({
      ...internalControlValue,
      ...nonSettingsChanges,
      // As title is not a setting, it must be manually applied
      // in such a way as to preserve the users changes over
      // any "title" value provided by the "suggestion".
      title: internalControlValue?.title || updatedValue?.title
    });
    stopEditing();
  };
  const handleSubmit = () => {
    if (valueHasChanges) {
      // Submit the original value with new stored values applied
      // on top. URL is a special case as it may also be a prop.
      onChange({
        ...value,
        ...internalControlValue,
        url: currentUrlInputValue
      });
    }
    stopEditing();
  };
  const handleSubmitWithEnter = event => {
    const {
      keyCode
    } = event;
    if (keyCode === _keycodes.ENTER && !currentInputIsEmpty // Disallow submitting empty values.
    ) {
      event.preventDefault();
      handleSubmit();
    }
  };
  const resetInternalValues = () => {
    setInternalControlValue(value);
  };
  const handleCancel = event => {
    event.preventDefault();
    event.stopPropagation();

    // Ensure that any unsubmitted input changes are reset.
    resetInternalValues();
    if (hasLinkValue) {
      // If there is a link then exist editing mode and show preview.
      stopEditing();
    } else {
      // If there is no link value, then remove the link entirely.
      onRemove?.();
    }
    onCancel?.();
  };
  const currentUrlInputValue = propInputValue || internalControlValue?.url || '';
  const currentInputIsEmpty = !currentUrlInputValue?.trim()?.length;
  const shownUnlinkControl = onRemove && value && !isEditingLink && !isCreatingPage;
  const showActions = isEditingLink && hasLinkValue;

  // Only show text control once a URL value has been committed
  // and it isn't just empty whitespace.
  // See https://github.com/WordPress/gutenberg/pull/33849/#issuecomment-932194927.
  const showTextControl = hasLinkValue && hasTextControl;
  const isEditing = (isEditingLink || !value) && !isCreatingPage;
  const isDisabled = !valueHasChanges || currentInputIsEmpty;
  const showSettings = !!settings?.length && isEditingLink && hasLinkValue;
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
    tabIndex: -1,
    ref: wrapperNode,
    className: "block-editor-link-control",
    children: [isCreatingPage && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
      className: "block-editor-link-control__loading",
      children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Spinner, {}), " ", (0, _i18n.__)('Creating'), "\u2026"]
    }), isEditing && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
      children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
        className: (0, _clsx.default)({
          'block-editor-link-control__search-input-wrapper': true,
          'has-text-control': showTextControl,
          'has-actions': showActions
        }),
        children: [showTextControl && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, {
          __nextHasNoMarginBottom: true,
          ref: textInputRef,
          className: "block-editor-link-control__field block-editor-link-control__text-content",
          label: (0, _i18n.__)('Text'),
          value: internalControlValue?.title,
          onChange: setInternalTextInputValue,
          onKeyDown: handleSubmitWithEnter,
          size: "__unstable-large"
        }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_searchInput.default, {
          currentLink: value,
          className: "block-editor-link-control__field block-editor-link-control__search-input",
          placeholder: searchInputPlaceholder,
          value: currentUrlInputValue,
          withCreateSuggestion: withCreateSuggestion,
          onCreateSuggestion: createPage,
          onChange: setInternalURLInputValue,
          onSelect: handleSelectSuggestion,
          showInitialSuggestions: showInitialSuggestions,
          allowDirectEntry: !noDirectEntry,
          showSuggestions: showSuggestions,
          suggestionsQuery: suggestionsQuery,
          withURLSuggestion: !noURLSuggestion,
          createSuggestionButtonText: createSuggestionButtonText,
          hideLabelFromVision: !showTextControl
        }), !showActions && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
          className: "block-editor-link-control__search-enter",
          children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, {
            onClick: isDisabled ? noop : handleSubmit,
            label: (0, _i18n.__)('Submit'),
            icon: _icons.keyboardReturn,
            className: "block-editor-link-control__search-submit",
            "aria-disabled": isDisabled
          })
        })]
      }), errorMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Notice, {
        className: "block-editor-link-control__search-error",
        status: "error",
        isDismissible: false,
        children: errorMessage
      })]
    }), value && !isEditingLink && !isCreatingPage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_linkPreview.default, {
      // force remount when URL changes to avoid race conditions for rich previews
      value: value,
      onEditClick: () => setIsEditingLink(true),
      hasRichPreviews: hasRichPreviews,
      hasUnlinkControl: shownUnlinkControl,
      onRemove: () => {
        onRemove();
        setIsEditingLink(true);
      }
    }, value?.url), showSettings && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
      className: "block-editor-link-control__tools",
      children: !currentInputIsEmpty && /*#__PURE__*/(0, _jsxRuntime.jsx)(_settingsDrawer.default, {
        settingsOpen: isSettingsOpen,
        setSettingsOpen: setSettingsOpenWithPreference,
        children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_settings.default, {
          value: internalControlValue,
          settings: settings,
          onChange: createSetInternalSettingValueHandler(settingsKeys)
        })
      })
    }), showActions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalHStack, {
      justify: "right",
      className: "block-editor-link-control__search-actions",
      children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, {
        variant: "tertiary",
        onClick: handleCancel,
        children: (0, _i18n.__)('Cancel')
      }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, {
        variant: "primary",
        onClick: isDisabled ? noop : handleSubmit,
        className: "block-editor-link-control__search-submit",
        "aria-disabled": isDisabled,
        children: (0, _i18n.__)('Save')
      })]
    }), !isCreatingPage && renderControlBottom && renderControlBottom()]
  });
}
LinkControl.ViewerFill = _viewerSlot.ViewerFill;
LinkControl.DEFAULT_LINK_SETTINGS = _constants.DEFAULT_LINK_SETTINGS;
var _default = exports.default = LinkControl;
//# sourceMappingURL=index.js.map