sodenn / lexical-beautiful-mentions

A mentions plugin for the lexical text editor.
https://lexical-beautiful-mentions-docs.vercel.app/
MIT License
160 stars 24 forks source link

Show suggestions when cursor moves inside the query string #630

Open djyadav opened 1 week ago

djyadav commented 1 week ago

Right now, the suggestions dropdown shows only when the cursor is at the end of queryString after the operator. But it doesn't show when cursor moves inside the word.

This is required if the user edits the query string and not just adds characters at the end. So far, I'm not able to find any alternate or approach to achieve the same. Is that really possible somehow ?

For example:

[operator]queryString[cursor] -> Shows suggestions dropdown [operator]query[cursor]string -> Doesn't show suggestions dropdown

dosubot[bot] commented 1 week ago

Hey there, @djyadav! I'm here to help you with any bugs, questions, or contributions you have. Let's tackle this issue together!

Yes, it is possible to show suggestions in the dropdown when the cursor moves inside the queryString and not just at the end after the operator. The BeautifulMentionsPlugin is configured to handle mentions and suggestions based on the cursor position within the queryString.

The checkForTriggers function is used to detect triggers based on the cursor's position within the queryString. This function checks for potential triggers and returns the relevant match information. The ComboboxPlugin then uses this information to show suggestions dynamically as the user types or moves the cursor within the queryString.

Here is the relevant part of the code that handles this:

export function checkForTriggers(
  text: string,
  triggers: string[],
): MenuTextMatch | null {
  const last = text.split(/\s/).pop() || text;
  const offset = text !== last ? text.lastIndexOf(last) : 0;
  const match = triggers.some((t) => t.startsWith(last) && t !== last);
  if (match) {
    return {
      leadOffset: offset,
      matchingString: last,
      replaceableString: last,
    };
  }
  return null;
}

Additionally, the ComboboxPlugin listens for updates to the editor state and dynamically updates the suggestions based on the current position of the cursor within the queryString:

useEffect(() => {
  const updateListener = () => {
    editor.getEditorState().read(() => {
      const text = getQueryTextForSearch(editor);
      if (text === null) {
        onReset();
        setTriggerMatch(null);
        setValueMatch(null);
        onQueryChange(null);
        setTriggerQueryString(null);
        return;
      }
      const triggerMatch = checkForTriggers(text, triggers);
      setTriggerMatch(triggerMatch);
      if (triggerMatch) {
        setTriggerQueryString(triggerMatch.matchingString);
        setValueMatch(null);
        return;
      }
      const valueMatch = triggerFn(text, editor);
      setValueMatch(valueMatch);
      onQueryChange(valueMatch ? valueMatch.matchingString : null);
      if (valueMatch && valueMatch.matchingString) {
        setTriggerQueryString(valueMatch.matchingString);
        return;
      }
      setTriggerQueryString(null);
    });
  };
  return editor.registerUpdateListener(updateListener);
}, [editor, triggerFn, onQueryChange, onReset, triggers]);

This configuration ensures that suggestions are shown based on the current position of the cursor within the queryString [1][2][3][4].

To continue talking to Dosu, mention @dosu.

djyadav commented 1 week ago

Is this specific to comboBox plugin If so, I'm not using that.

dosubot[bot] commented 1 week ago

Yes, the provided code and context are specific to ComboboxPlugin. If you are not using ComboboxPlugin, this information does not apply to your situation [1].

To continue talking to Dosu, mention @dosu.

djyadav commented 1 week ago

How can I show cursor aware suggestions in Menu then ? Is there a way or missing functionality ?

dosubot[bot] commented 1 week ago

To show cursor-aware suggestions in the Menu, you can dynamically position the menu based on the cursor's position. This functionality is demonstrated in the test cases provided in the commands.spec.ts file. Here are some relevant examples:

import { expect, test } from "@playwright/test";
import { testUtils } from "./test-utils";

test.describe("Open Suggestions", () => {
  test("should open the menu at the end of the editor", async ({ page, browserName }) => {
    const utils = await testUtils(
      { page, browserName },
      {
        initialValue: "Hey @John, the task is #urgent and due:tomorrow",
      },
    );
    await expect(utils.mentionsMenu).not.toBeVisible();
    await page.getByText("Open Suggestions").click();
    await expect(utils.mentionsMenu).toBeVisible();
    await utils.hasText("Hey [@John], the task is [#urgent] and [due:tomorrow] @");
  });

  test("should open the menu even if the editor was never focused", async ({ page, browserName }) => {
    const utils = await testUtils(
      { page, browserName },
      {
        autofocus: "none",
        initialValue: "Hey",
      },
    );
    await expect(utils.mentionsMenu).not.toBeVisible();
    await page.getByText("Open Suggestions").click();
    await expect(utils.mentionsMenu).toBeVisible();
    await utils.hasText("Hey @");
  });

  test("should open the menu at the start of the editor", async ({ page, browserName }) => {
    const utils = await testUtils(
      { page, browserName },
      {
        autofocus: "start",
        initialValue: "Hey @John, the task is #urgent and due:tomorrow",
      },
    );
    await page.getByText("Open Suggestions").click();
    await expect(utils.mentionsMenu).toBeVisible();
    await utils.hasText("@ Hey [@John], the task is [#urgent] and [due:tomorrow]");
  });

  test("should open the menu after a word", async ({ page, browserName }) => {
    const utils = await testUtils(
      { page, browserName },
      {
        autofocus: "start",
        initialValue: "Hey @John, the task is #urgent and due:tomorrow",
      },
    );
    await utils.moveCursorForward(3);
    await page.getByText("Open Suggestions").click();
    await expect(utils.mentionsMenu).toBeVisible();
    await utils.hasText("Hey @ [@John], the task is [#urgent] and [due:tomorrow]");
  });

  test("should open the menu before a mention", async ({ page, browserName }) => {
    const utils = await testUtils(
      { page, browserName },
      {
        autofocus: "start",
        initialValue: "Hey @John, the task is #urgent and due:tomorrow",
      },
    );
    await utils.moveCursorForward(4);
    await page.getByText("Open Suggestions").click();
    await expect(utils.mentionsMenu).toBeVisible();
    await utils.hasText("Hey @ [@John], the task is [#urgent] and [due:tomorrow]");
  });
});

Additionally, you can use the BeautifulMentionsPlugin from the lexical-beautiful-mentions package to handle mentions in a lexical editor. This plugin includes functionality to dynamically position the menu and show cursor-aware suggestions. Here is an example of how to use it:

import { BeautifulMentionsPlugin } from "./BeautifulMentionsPlugin";

export function MyEditor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin contentEditable={<ContentEditable />} />
      <BeautifulMentionsPlugin
        items={mentionItems}
        onSearch={handleSearch}
        menuComponent={CustomMenuComponent}
        menuItemComponent={CustomMenuItemComponent}
      />
    </LexicalComposer>
  );
}

The BeautifulMentionsPlugin handles the dynamic positioning of the menu and provides a customizable way to show suggestions based on the cursor's position [1][2].

To continue talking to Dosu, mention @dosu.