facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

Feature: maxLength #2230

Closed jack-guy closed 1 year ago

jack-guy commented 1 year ago

Setting a maxlength for an editor was something that was quite complicated using Draft.js. It's also a common need - particularly for developers relying on an editor primarily for plaintext functionality (as a replacement for <textarea />).

I see that there's some WIP work being done on LexicalCharacterLimitPlugin but testing this feature out in the playground seems to show a different behavior than I expected - rather than limiting input it simply highlights input that's outside the character limit - image

I also tried to poke around the source code for how I might implement this myself. My current, very naive implementation:

const LexicalMaxLengthPlugin = ({ maxLength }) => {
    const [editor] = useLexicalComposerContext();

    useEffect(() => {
        editor.registerTextContentListener(textContent => {
            if (textContent.length > maxLength) {
                editor.update(
                    () => {
                        $getSelection().deleteCharacter(true);
                    },
                    {
                        tag: 'history-merge',
                    },
                );
            }
        });
    }, [editor]);

    return null;
};

this has many problems (I wasn't really expecting it to work)

I'm curious if functionality to mimic the behavior of <textarea maxlength="" can be accomplished using Lexical as it exists today, or if there will need to be some new APIs implemented. In particular without some pretty tedious and potentially slow node manipulation it's not clear to me how one could truncate excess characters in a single update.

My team is trying to be an early adopter of Lexical - we're in the process of migrating our application to React 18 and draft.js is partially blocking that. Thanks!

trueadm commented 1 year ago

I'm curious if functionality to mimic the behavior of <textarea maxlength="" can be accomplished using Lexical as it exists today, or if there will need to be some new APIs implemented. In particular without some pretty tedious and potentially slow node manipulation it's not clear to me how one could truncate excess characters in a single update.

It should definitely be possible to do this. I might take a stab at hacking something together here!

trueadm commented 1 year ago

Check out https://github.com/facebook/lexical/pull/2254!

jack-guy commented 1 year ago

@trueadm this is awesome. Thanks so much for your help on this one.

One thing I observed from testing in the playground is that HistoryPlugin continues to add entries for keypresses beyond the max length. This isn't entirely broken but is probably unexpected for users. I imagine in order to fix that for a truncated input we'd need to remove the latest history entry and replace it with a modified version that matches the text that ends up being displayed - do you have any thoughts on the best way to do that?

Separately, in the course of my testing I encountered a bug that seems to introduce an inconsistency between content editable and the editor state. Because it is broken at that level I'm not sure that it's specific to the MaxLength implementation but I figure I'd report it here and can open a new issue if appropriate:

image

If you reach the maximum number of characters, move the cursor to the middle of the text and press enter you're able to continuing typing, but the new text won't show up in the editor state. This seems to break editing as a whole.

trueadm commented 1 year ago

Thank you for the awesome feedback. I addressed them in https://github.com/facebook/lexical/pull/2258. :)

jack-guy commented 1 year ago

@trueadm thanks again for the quick fixes. From my tests the history works almost perfectly on the latest playground revision. There's only one case I found that is other than what I expected. If I copy text that's at the max length and attempt to paste it with the cursor at the start it seems to continue to add to the history (and appears to be moving the anchor and focus forward):

image

after a paste:

image

Oddly this does not apply to typing or any other strings I attempt to paste in the same manner. So very much an edge case, not sure users would ever actually run into this one.

I'm still seeing strange behavior with editor state inconsistency however.

  1. "what happens if this is trunca" image
  2. insert enter after "if". Text is truncated at the end. image
  3. press enter a second time. Now try to backspace - will not work, instead will highlight text one character at a time: image
  4. insert characters in the middle - editor state and content editable are inconsistent image

This is in Chrome 101.0.4951.64 if that's at all helpful.

Again want to say I appreciate the time you've put into this - this seems like a particularly tricky feature request so no worries at all if this has to be set aside for the time being :)

trueadm commented 1 year ago

Thanks yet again for finding that issue. It was obvious on reflection: https://github.com/facebook/lexical/pull/2265

If you check your dev tools console, you should notice the error in the log :)

jack-guy commented 1 year ago

Thanks @trueadm, that seems to have addressed the weird highlighting + editor state inconsistency!

However, pressing enter in the middle of text still truncates the end of the string. Original text: image after 6 returns: image

trueadm commented 1 year ago

That's intentional. An element has suffix text content \n\n. Maybe we should prevent adding a paragraph to begin with though :/

This isn't actually a bug, for some reason I actually intended for that to happen, so maybe it should be refined again. I'll get to it next week, as this is probably okay for the mean time.

jack-guy commented 1 year ago

Sure yeah, makes sense. Just treating <textarea maxlength /> as reference I wouldn't except a return to be treated any differently from other keyboard input (which gets blocked altogether) :)

This is a great start for my team though and your help has been invaluable. Thanks again Dominic! Hope as I familiarize myself with Lexical and the documentation gets refined I'll be able to pitch in here sometime soon too :)

trueadm commented 1 year ago

No problem. Actually, I came up with a neat heuristic that handles this in the simple case: https://github.com/facebook/lexical/pull/2273

jack-guy commented 1 year ago

After the latest revision, MaxLengthPlugin seems to be working perfectly in the playground. Looking forward to getting this all set up in production here when the latest Lexical is released!

granmoe commented 1 year ago

Where is this exposed in the public API? Is it in a package or anything?

granmoe commented 1 year ago

I'm just copying the source for now as a workaround

AntonOfTheWoods commented 5 months ago

For anyone else looking, the project maintainers seem to not have been given the bandwidth to document properly. A LOT of actually useful functionality is available in the playground code including a MaxLengthPlugin