(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/pm/state'), require('@tiptap/pm/view'), require('@tiptap/core')) :
  typeof define === 'function' && define.amd ? define(['exports', '@tiptap/pm/state', '@tiptap/pm/view', '@tiptap/core'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/suggestion"] = {}, global.state, global.view, global.core));
})(this, (function (exports, state, view, core) { 'use strict';

  function findSuggestionMatch(config) {
      var _a;
      const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position, } = config;
      const allowSpaces = allowSpacesOption && !allowToIncludeChar;
      const escapedChar = core.escapeForRegEx(char);
      const suffix = new RegExp(`\\s${escapedChar}$`);
      const prefix = startOfLine ? '^' : '';
      const finalEscapedChar = allowToIncludeChar ? '' : escapedChar;
      const regexp = allowSpaces
          ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${finalEscapedChar}|$)`, 'gm')
          : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${finalEscapedChar}]*`, 'gm');
      const text = ((_a = $position.nodeBefore) === null || _a === void 0 ? void 0 : _a.isText) && $position.nodeBefore.text;
      if (!text) {
          return null;
      }
      const textFrom = $position.pos - text.length;
      const match = Array.from(text.matchAll(regexp)).pop();
      if (!match || match.input === undefined || match.index === undefined) {
          return null;
      }
      // JavaScript doesn't have lookbehinds. This hacks a check that first character
      // is a space or the start of the line
      const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index);
      const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes === null || allowedPrefixes === void 0 ? void 0 : allowedPrefixes.join('')}\0]?$`).test(matchPrefix);
      if (allowedPrefixes !== null && !matchPrefixIsAllowed) {
          return null;
      }
      // The absolute position of the match in the document
      const from = textFrom + match.index;
      let to = from + match[0].length;
      // Edge case handling; if spaces are allowed and we're directly in between
      // two triggers
      if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
          match[0] += ' ';
          to += 1;
      }
      // If the $position is located within the matched substring, return that range
      if (from < $position.pos && to >= $position.pos) {
          return {
              range: {
                  from,
                  to,
              },
              query: match[0].slice(char.length),
              text: match[0],
          };
      }
      return null;
  }

  const SuggestionPluginKey = new state.PluginKey('suggestion');
  /**
   * This utility allows you to create suggestions.
   * @see https://tiptap.dev/api/utilities/suggestion
   */
  function Suggestion({ pluginKey = SuggestionPluginKey, editor, char = '@', allowSpaces = false, allowToIncludeChar = false, allowedPrefixes = [' '], startOfLine = false, decorationTag = 'span', decorationClass = 'suggestion', decorationContent = '', decorationEmptyClass = 'is-empty', command = () => null, items = () => [], render = () => ({}), allow = () => true, findSuggestionMatch: findSuggestionMatch$1 = findSuggestionMatch, }) {
      let props;
      const renderer = render === null || render === void 0 ? void 0 : render();
      const plugin = new state.Plugin({
          key: pluginKey,
          view() {
              return {
                  update: async (view, prevState) => {
                      var _a, _b, _c, _d, _e, _f, _g;
                      const prev = (_a = this.key) === null || _a === void 0 ? void 0 : _a.getState(prevState);
                      const next = (_b = this.key) === null || _b === void 0 ? void 0 : _b.getState(view.state);
                      // See how the state changed
                      const moved = prev.active && next.active && prev.range.from !== next.range.from;
                      const started = !prev.active && next.active;
                      const stopped = prev.active && !next.active;
                      const changed = !started && !stopped && prev.query !== next.query;
                      const handleStart = started || (moved && changed);
                      const handleChange = changed || moved;
                      const handleExit = stopped || (moved && changed);
                      // Cancel when suggestion isn't active
                      if (!handleStart && !handleChange && !handleExit) {
                          return;
                      }
                      const state = handleExit && !handleStart ? prev : next;
                      const decorationNode = view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`);
                      props = {
                          editor,
                          range: state.range,
                          query: state.query,
                          text: state.text,
                          items: [],
                          command: commandProps => {
                              return command({
                                  editor,
                                  range: state.range,
                                  props: commandProps,
                              });
                          },
                          decorationNode,
                          // virtual node for popper.js or tippy.js
                          // this can be used for building popups without a DOM node
                          clientRect: decorationNode
                              ? () => {
                                  var _a;
                                  // because of `items` can be asynchrounous we’ll search for the current decoration node
                                  const { decorationId } = (_a = this.key) === null || _a === void 0 ? void 0 : _a.getState(editor.state); // eslint-disable-line
                                  const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`);
                                  return (currentDecorationNode === null || currentDecorationNode === void 0 ? void 0 : currentDecorationNode.getBoundingClientRect()) || null;
                              }
                              : null,
                      };
                      if (handleStart) {
                          (_c = renderer === null || renderer === void 0 ? void 0 : renderer.onBeforeStart) === null || _c === void 0 ? void 0 : _c.call(renderer, props);
                      }
                      if (handleChange) {
                          (_d = renderer === null || renderer === void 0 ? void 0 : renderer.onBeforeUpdate) === null || _d === void 0 ? void 0 : _d.call(renderer, props);
                      }
                      if (handleChange || handleStart) {
                          props.items = await items({
                              editor,
                              query: state.query,
                          });
                      }
                      if (handleExit) {
                          (_e = renderer === null || renderer === void 0 ? void 0 : renderer.onExit) === null || _e === void 0 ? void 0 : _e.call(renderer, props);
                      }
                      if (handleChange) {
                          (_f = renderer === null || renderer === void 0 ? void 0 : renderer.onUpdate) === null || _f === void 0 ? void 0 : _f.call(renderer, props);
                      }
                      if (handleStart) {
                          (_g = renderer === null || renderer === void 0 ? void 0 : renderer.onStart) === null || _g === void 0 ? void 0 : _g.call(renderer, props);
                      }
                  },
                  destroy: () => {
                      var _a;
                      if (!props) {
                          return;
                      }
                      (_a = renderer === null || renderer === void 0 ? void 0 : renderer.onExit) === null || _a === void 0 ? void 0 : _a.call(renderer, props);
                  },
              };
          },
          state: {
              // Initialize the plugin's internal state.
              init() {
                  const state = {
                      active: false,
                      range: {
                          from: 0,
                          to: 0,
                      },
                      query: null,
                      text: null,
                      composing: false,
                  };
                  return state;
              },
              // Apply changes to the plugin state from a view transaction.
              apply(transaction, prev, _oldState, state) {
                  const { isEditable } = editor;
                  const { composing } = editor.view;
                  const { selection } = transaction;
                  const { empty, from } = selection;
                  const next = { ...prev };
                  next.composing = composing;
                  // We can only be suggesting if the view is editable, and:
                  //   * there is no selection, or
                  //   * a composition is active (see: https://github.com/ueberdosis/tiptap/issues/1449)
                  if (isEditable && (empty || editor.view.composing)) {
                      // Reset active state if we just left the previous suggestion range
                      if ((from < prev.range.from || from > prev.range.to)
                          && !composing
                          && !prev.composing) {
                          next.active = false;
                      }
                      // Try to match against where our cursor currently is
                      const match = findSuggestionMatch$1({
                          char,
                          allowSpaces,
                          allowToIncludeChar,
                          allowedPrefixes,
                          startOfLine,
                          $position: selection.$from,
                      });
                      const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`;
                      // If we found a match, update the current state to show it
                      if (match
                          && allow({
                              editor,
                              state,
                              range: match.range,
                              isActive: prev.active,
                          })) {
                          next.active = true;
                          next.decorationId = prev.decorationId
                              ? prev.decorationId
                              : decorationId;
                          next.range = match.range;
                          next.query = match.query;
                          next.text = match.text;
                      }
                      else {
                          next.active = false;
                      }
                  }
                  else {
                      next.active = false;
                  }
                  // Make sure to empty the range if suggestion is inactive
                  if (!next.active) {
                      next.decorationId = null;
                      next.range = { from: 0, to: 0 };
                      next.query = null;
                      next.text = null;
                  }
                  return next;
              },
          },
          props: {
              // Call the keydown hook if suggestion is active.
              handleKeyDown(view, event) {
                  var _a;
                  const { active, range } = plugin.getState(view.state);
                  if (!active) {
                      return false;
                  }
                  return ((_a = renderer === null || renderer === void 0 ? void 0 : renderer.onKeyDown) === null || _a === void 0 ? void 0 : _a.call(renderer, { view, event, range })) || false;
              },
              // Setup decorator on the currently active suggestion.
              decorations(state) {
                  const { active, range, decorationId, query, } = plugin.getState(state);
                  if (!active) {
                      return null;
                  }
                  const isEmpty = !(query === null || query === void 0 ? void 0 : query.length);
                  const classNames = [decorationClass];
                  if (isEmpty) {
                      classNames.push(decorationEmptyClass);
                  }
                  return view.DecorationSet.create(state.doc, [
                      view.Decoration.inline(range.from, range.to, {
                          nodeName: decorationTag,
                          class: classNames.join(' '),
                          'data-decoration-id': decorationId,
                          'data-decoration-content': decorationContent,
                      }),
                  ]);
              },
          },
      });
      return plugin;
  }

  exports.Suggestion = Suggestion;
  exports.SuggestionPluginKey = SuggestionPluginKey;
  exports.default = Suggestion;
  exports.findSuggestionMatch = findSuggestionMatch;

  Object.defineProperty(exports, '__esModule', { value: true });

}));
//# sourceMappingURL=index.umd.js.map
