Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempting to limit text insertion by overriding insertText causes content to be out of sync with node representation #5010

Open
davisg123 opened this issue May 26, 2022 · 8 comments
Labels

Comments

@davisg123
Copy link

Description
We're attempting to limit the content to a maximum length by overriding insertText, but as of version 0.67 Slate will continue to display characters that are typed even though they don't exist in the node representation. This can lead to data loss

  editor.insertText = (text) => {
    const currentLength = getChildrenStringLength(editor.children);
    const totalLength = currentLength + text.length;
    if (totalLength <= limit) {
      insertText(text);
    } else {
      const available = limit - currentLength;
      if (available > 0) {
        insertText(text.substr(0, available));
      }
    }
  };

Recording

Screen.Recording.2022-05-26.at.3.04.27.PM.mov

Sandbox
https://codesandbox.io/s/slate-character-limit-bug-rdursq?file=/index.js

Change slate-react to < 0.67 to see the desired behavior

Steps
To reproduce the behavior:

  1. Override insertText
  2. Type characters in the editor

Expectation
The editor should not allow any characters to be typed

Environment

  • Slate Version: [e.g. 0.59] Regression in 0.67
  • Operating System: [e.g. iOS] macOS
  • Browser: [e.g. Chrome, Safari] Chrome, Safari
  • TypeScript Version: [e.g. 3.9.7 - required only if it's a TypeScript issue]

Context

@davisg123 davisg123 added the bug label May 26, 2022
@annatraussnig
Copy link

annatraussnig commented Jun 8, 2022

Facing the exact same issue.

–– EDIT ––

Adding an if clause to the onKeyDown handler of <Editable /> helps with the behaviour:

<Editable
  renderElement={renderElement}
  renderLeaf={renderLeaf}
  onKeyDown={(event) => {
    // disable key down events if max signs is reached
    // (except nav keys such as Tab/Arrows)
    if (
      config.maxSigns &&
      getCharCount(editor.children) >= config.maxSigns &&
      !NAV_KEYS.concat('Backspace').includes(event.key)
    ) {
      event.preventDefault()
      return false
    }
  }}
/>

@OceanApart
Copy link

OceanApart commented Jun 21, 2022

Same issue. Only number input is limited.

@pchiwan
Copy link

pchiwan commented Aug 1, 2022

Facing the exact same issue.

–– EDIT ––

Adding an if clause to the onKeyDown handler of <Editable /> helps with the behaviour:

<Editable
  renderElement={renderElement}
  renderLeaf={renderLeaf}
  onKeyDown={(event) => {
    // disable key down events if max signs is reached
    // (except nav keys such as Tab/Arrows)
    if (
      config.maxSigns &&
      getCharCount(editor.children) >= config.maxSigns &&
      !NAV_KEYS.concat('Backspace').includes(event.key)
    ) {
      event.preventDefault()
      return false
    }
  }}
/>

this worked perfectly, thanks a lot @annatraussnig! 🙌

@guoyianlin
Copy link

Facing the exact same issue.

–– EDIT ––

Adding an if clause to the onKeyDown handler of <Editable /> helps with the behaviour:

<Editable
  renderElement={renderElement}
  renderLeaf={renderLeaf}
  onKeyDown={(event) => {
    // disable key down events if max signs is reached
    // (except nav keys such as Tab/Arrows)
    if (
      config.maxSigns &&
      getCharCount(editor.children) >= config.maxSigns &&
      !NAV_KEYS.concat('Backspace').includes(event.key)
    ) {
      event.preventDefault()
      return false
    }
  }}
/>

If it is a Chinese input method, it will make an error.

@mshndev
Copy link

mshndev commented Jan 12, 2023

I have two more different approaches.

The first one is based on predicting the result:

<Editable
  renderLeaf={renderLeaf}
  renderElement={renderElement}
  renderPlaceholder={renderPlaceholder}
  placeholder={placeholder}
  onDOMBeforeInput={(e) => {
    if (
      e.inputType !== 'insertText' &&
      e.inputType !== 'insertFromPaste'
    )
      return

    const input = e.data || e.dataTransfer?.getData('text/plain')
    if (!input) return

    const sel = [
      editor.selection?.anchor?.offset || 0,
      editor.selection?.focus?.offset || 0
    ].sort()

    const text = serializeString(editor.children)

    const newText =
      text.substring(0, sel[0]) + input + text.substring(sel[1])

    if (newText.length > config.maxSigns) e.preventDefault()
  }}
/>

It also works with pasting some (not every 🤷‍♂️) clipboard data.

The second one I made for formatting on the go, so it may be overkill for just limiting the characters length.

In the editor:

const editor = useMemo(
  () =>
    withFormatting(
     withHistory(withReact(createEditor())),
      (s) =>
        s.trimStart().replace(/\s+/gi, ' ') // some formatting
          .substring(0, config.maxSigns) // limit char length
    ),
  []
)

the plugin:

function withFormatting<T extends Editor>(editor: T, format?: FormatCb) {
  const { insertText, deleteFragment, deleteBackward, deleteForward } = editor

  const canFormat = typeof format === 'function'
  if (!canFormat) return editor

  editor.insertText = (textPart) => {
    insertText(textPart)

    const text = serializeString(currentNode(editor))
    const sel = editor.selection?.anchor?.offset || text.length
    const formattedText = format(text)
    
    // replacing with formatted text
    Transforms.insertText(editor, formattedText, {
      at: changeSelectionOffset(editor.selection, [0, text.length])
    })

    const textDiff = formattedText.length - text.length
    const newOffset = Math.min(sel + textDiff, formattedText.length)
    editor.selection = changeSelectionOffset(editor.selection, newOffset)
  }

  editor.deleteFragment = (d) => {
    deleteFragment(d)
    editor.insertText('')
  }
  editor.deleteBackward = (d) => {
    deleteBackward(d)
    editor.insertText('')
  }
  editor.deleteForward = (d) => {
    deleteForward(d)
    editor.insertText('')
  }

  return editor
}

function currentNode<T extends Editor>({
  children,
  selection
}: T): Descendant[] {
  if (!selection) return children
  const path = selection?.anchor.path || [0, 0]
  const url = path
    .slice(0, path.length - 1)
    .map((i) => `[${i}]`)
    .join('.children')

  const node = _.get(children, url)
  return node?.children || children
}

function changeSelectionOffset(
  selection: BaseSelection,
  offsetOrArray: [number, number] | number
) {
  const offset =
    typeof offsetOrArray === 'number' ? [offsetOrArray] : offsetOrArray
  return {
    anchor: {
      ...selection.anchor,
      offset: offset[0]
    },
    focus: {
      ...selection.focus,
      offset: offset[1] || offset[0]
    }
  }
}

function serializeString (value?: Descendant[]){
  return (value || []).map((n) => Node.string(n)).join()
}

@codeingforcoffee
Copy link

not work while useing Chinese.....😮‍💨

@delijah
Copy link
Contributor

delijah commented Oct 16, 2023

Thx for the workarounds, but shouldn't this work correctly out of the box?

@cguellner
Copy link

how can i get the NAV_KEYS variable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants