quill icon indicating copy to clipboard operation
quill copied to clipboard

Cursor not moved when newline typed in Android

Open dmcqfano opened this issue 4 years ago • 18 comments

Using Chrome in Android using a demo versions of quill typing "a" newline puts in a newline but leaves the cursor after a. More strange things happen too with deletes of newline - e.g. A newline cursor B after backdelete gives AB cursor.

Steps for Reproduction

  1. Use a mobile phone with Chrome browser
  2. Visit https://quilljs.com/standalone/snow/
  3. Type "a" then newline

Expected behavior: Should get "a" newline cursor

Actual behavior: Get "a" cursor newline

Platforms: Android 9 on a mobile phone in Chrome 86.0.4240.198

Version: Quill 1.3.6 in cdn.quilljs.com

dmcqfano avatar Nov 30 '20 23:11 dmcqfano

Not sure what I did to close it! I added a comment to say it worked on my desktop with chrome set towork like mobile inthe developer tools

dmcqfano avatar Nov 30 '20 23:11 dmcqfano

Have the same issue

andrey-lezhnev avatar Dec 01 '20 11:12 andrey-lezhnev

There seems to be some strange problem with gboard, there is a workaround at Android app text editing newline

Type a space before typing newline.

It really needs a workaround in quill as well as waiting for the fix for gboard.

dmcqfano avatar Dec 01 '20 22:12 dmcqfano

Can reproduce this issue when showing a quill editor inside a WebView or in the Chrome browser. I encountered the problem with Android 10, with the most current GBoard and WebView. It occurs if a normal character is the last char before the cursor, as soon the last character is a special char like space or comma, the cursor goes correctly to the new line.

The problem does not happen with alternative keyboards, but those do not always use IME composition. It seems that after the event.keyCode === 229, there is no following keydown event with the newline. Normal

Tried the same with the slate editor and could reproduce it as well.

martinstoeckli avatar Jan 11 '21 14:01 martinstoeckli

We're evaluating Quill for our new editor and are experiencing the same issue. Before we move to another editor, is there any hope of having this resolved in Quill, be that with a workaround or otherwise?

mcrichards avatar Mar 26 '21 22:03 mcrichards

@mcrichards - I opened up an issue on Googles page, it affects other editors too, like slate or even CKEditor.

Edit: About a good year later there isn't any reaction from Google and so I decided to switch to TipTap/ProseMirror, which is one of few editors which doesn't suffer from this problem. The entry hurdle is higher, but I didn't regret it so far, this solution opens up many possibilities.

martinstoeckli avatar Mar 26 '21 23:03 martinstoeckli

@martinstoeckli - Thanks for the heads up. Good to know it's affect is being felt elsewhere. Hopefully that yields additional attention and a quicker resolution.

Do you know if it is possible to develop a workaround in Quill while we wait?

mcrichards avatar Mar 27 '21 22:03 mcrichards

I wonder if the workaround mentioned here might also resolve this? https://github.com/quilljs/quill/issues/3153

sachinrekhi avatar Mar 27 '21 23:03 sachinrekhi

Found a simple workaround. Idea pretty simple: If there a new \n character and after 35ms selection does not change to the next position => move cursor to the next position.

function applyGoogleKeyboardWorkaround(editor) {
    try {
        if (editor.applyGoogleKeyboardWorkaround) {
            return
        }

        editor.applyGoogleKeyboardWorkaround = true
        editor.quill.on('editor-change', function (eventName, ...args) {
            if (eventName === 'text-change') {
              // args[0] will be delta
              var ops = args[0]['ops']
              var oldSelection = editor.getSelection()
              var oldPos = oldSelection.index
              var oldSelectionLength = oldSelection.length

              if (ops[0]["retain"] === undefined || !ops[1] || !ops[1]["insert"] || !ops[1]["insert"] || ops[1]["insert"] != "\n"  || oldSelectionLength > 0) {
                return
              }

              setTimeout(function () {
                var newPos = editor.getSelection().index
                if (newPos === oldPos) {
                  console.log("Change selection bad pos")
                  editor.setSelection(editor.getSelection().index + 1, 0)
                }
              }, 30);
            } 
          });
    } catch {
    }
}

applyGoogleKeyboardWorkaround(editor)

albertaleksieiev avatar May 27 '21 14:05 albertaleksieiev

is there any news about it? We have the same issue

nlitterat avatar Jun 25 '21 05:06 nlitterat

Found a simple workaround. Idea pretty simple: If there a new \n character and after 35ms selection does not change to the next position => move cursor to the next position.

function applyGoogleKeyboardWorkaround(editor) {
    try {
        if (editor.applyGoogleKeyboardWorkaround) {
            return
        }

        editor.applyGoogleKeyboardWorkaround = true
        editor.quill.on('editor-change', function (eventName, ...args) {
            if (eventName === 'text-change') {
              // args[0] will be delta
              var ops = args[0]['ops']
              var oldSelection = editor.getSelection()
              var oldPos = oldSelection.index
              var oldSelectionLength = oldSelection.length

              if (ops[0]["retain"] === undefined || !ops[1] || !ops[1]["insert"] || !ops[1]["insert"] || ops[1]["insert"] != "\n"  || oldSelectionLength > 0) {
                return
              }

              setTimeout(function () {
                var newPos = editor.getSelection().index
                if (newPos === oldPos) {
                  console.log("Change selection bad pos")
                  editor.setSelection(editor.getSelection().index + 1, 0)
                }
              }, 30);
            } 
          });
    } catch {
    }
}

applyGoogleKeyboardWorkaround(editor)

Worked for me

nlitterat avatar Jun 30 '21 07:06 nlitterat

Translate it to typescript if it is of any interest

  applyGoogleKeyboardWorkaround(editor): void {
    try {
        if (!editor.applyGoogleKeyboardWorkaround) {
            editor.applyGoogleKeyboardWorkaround = true;
            editor.on('editor-change', (eventName, ...args) => {
                if (eventName === 'text-change') {
                    // args[0] will be delta
                    const ops = args[0].ops;
                    const oldSelection = editor.getSelection();
                    const oldPos = oldSelection?.index;
                    const oldSelectionLength = oldSelection ?  oldSelection.length : 0;

                    if (ops[0].retain === undefined ||
                        !ops[1] ||
                        !ops[1].insert ||
                        !ops[1].insert ||
                        ops[1].insert !== '\n' ||
                        oldSelectionLength > 0) {
                        return;
                    }

                    setTimeout(() => {
                        const newPos = editor.getSelection().index;
                        if (newPos === oldPos) {
                            console.log('Change selection bad pos');
                            editor.setSelection(editor.getSelection().index + 1, 0);
                        }
                    }, 30);
                }
            });
        }
    } catch {
    }

}

nlitterat avatar Jun 30 '21 08:06 nlitterat

Found a simple workaround. Idea pretty simple: If there a new \n character and after 35ms selection does not change to the next position => move cursor to the next position.

function applyGoogleKeyboardWorkaround(editor) {
    try {
        if (editor.applyGoogleKeyboardWorkaround) {
            return
        }

        editor.applyGoogleKeyboardWorkaround = true
        editor.quill.on('editor-change', function (eventName, ...args) {
            if (eventName === 'text-change') {
              // args[0] will be delta
              var ops = args[0]['ops']
              var oldSelection = editor.getSelection()
              var oldPos = oldSelection.index
              var oldSelectionLength = oldSelection.length

              if (ops[0]["retain"] === undefined || !ops[1] || !ops[1]["insert"] || !ops[1]["insert"] || ops[1]["insert"] != "\n"  || oldSelectionLength > 0) {
                return
              }

              setTimeout(function () {
                var newPos = editor.getSelection().index
                if (newPos === oldPos) {
                  console.log("Change selection bad pos")
                  editor.setSelection(editor.getSelection().index + 1, 0)
                }
              }, 30);
            } 
          });
    } catch {
    }
}

applyGoogleKeyboardWorkaround(editor)

this one works for me on VueJS

function applyGoogleKeyboardWorkaround(editor){
  try {
      if (!editor.applyGoogleKeyboardWorkaround) {
          editor.applyGoogleKeyboardWorkaround = true;
          editor.on('editor-change', (eventName, ...args) => {
              if (eventName === 'text-change') {
                  // args[0] will be delta
                  const ops = args[0].ops;
                  const oldSelection = editor.getSelection();
                  const oldPos = oldSelection?.index;
                  const oldSelectionLength = oldSelection ?  oldSelection.length : 0;

                  if (ops[0].retain === undefined ||
                      !ops[1] ||
                      !ops[1].insert ||
                      !ops[1].insert ||
                      ops[1].insert !== '\n' ||
                      oldSelectionLength > 0) {
                      return;
                  }

                  setTimeout(() => {
                      const newPos = editor.getSelection().index;
                      if (newPos === oldPos) {
                          console.log('Change selection bad pos');
                          editor.setSelection(editor.getSelection().index + 1, 0);
                      }
                  }, 30);
              }
          });
      }
  } catch {
    console.log('error gboard');
  }
applyGoogleKeyboardWorkaround(this.commentEditor)

thank you for your help

lucahttp avatar Nov 29 '21 23:11 lucahttp

Any updates to this issue? Can confirm still occuring.

timi2shoes avatar Mar 08 '22 03:03 timi2shoes

I love Quill, mainly because of it's ease of use with the toolbar. However, I found an editor call "remirror" that's built on top of ProseMirror . It doesn't seem to suffer from this issue when using Gboard, but I am unable (don't know how) to customise the toolbar with that editor. I'm trying to get the "left-align, center align, etc buttons on it. If anyone likes it, and can figure out how to add those buttons, please share with me the steps or code sample

kachmul2004 avatar Mar 26 '22 23:03 kachmul2004

Personally I switched to Tiptap editor, it also built on top ProseMirror and it has formatting example

nkonev avatar Mar 27 '22 08:03 nkonev

Personally I switched to Tiptap editor, it also built on top ProseMirror and it has formatting example

This could be it for me, thanks a lot

kachmul2004 avatar Mar 27 '22 08:03 kachmul2004

reproduced on version 1.3.7

marcT21 avatar May 25 '22 05:05 marcT21

I think this is due to GBoard handling a RETURN differently if the word is still underlined (e.g. it might autocorrect on RETURN, I believe?) The issue goes away when entering a period ("."), a comma (",") or a blank (" ") and hitting RETURN afterwards.

I'm not sure if other android keyboards are doing the same, but here are some findings:

When the word is still underlined, GBoard sends the usual keycode "13" when hitting RETURN, but also emits a UI Event "isComposing". Once you enter a space after the word, GBoard only sends the keycode "13" without "isComposing" (or "isComposing" is set to false).

You can easily test this at https://w3c.github.io/uievents/tools/key-event-viewer.html

Maybe the function "handleEnter" can deal with this and actually place the caret on the correct line?

martin-leoorg avatar Nov 17 '22 08:11 martin-leoorg

class SupportGBoard {
    constructor(quill) {
        this.quill = quill;
        quill.on("editor-change", this.handle.bind(this));
    }

    handle(eventName, ...args) {
        if (eventName === "text-change") {
            let ops = args[0]["ops"];
            let oldSelection = this.quill.getSelection(true);
            let oldPos = oldSelection.index;
            let oldSelectionLength = oldSelection.length;

            let retain = undefined;
            let insert = undefined;
            for (let j = 0; j < ops.length; j++) {
                let keys = Object.keys(ops[j]);
                if (keys.includes("retain") && retain === undefined) {
                    retain = ops[j]["retain"];
                }
                if (keys.includes("insert")) {
                    insert = ops[j]["insert"];
                }
            }

            if (
                retain === undefined ||
                insert === undefined ||
                insert != "\n" ||
                oldSelectionLength > 0
            ) {
                return;
            }

            setTimeout(function () {
                try {
                    let newPos = this.quill.getSelection(true).index;
                    if (newPos === oldPos) {
                        console.log("Change selection bad pos");
                        this.quill.setSelection(
                            this.quill.getSelection(true).index + 1,
                            0
                        );
                    }
                } catch (err) {
                    this.quill.focus();
                }
            }, 30);
        }
    }
}

Quill.register("modules/supportGBoard", SupportGBoard);

you can just add this module

App10xHarshal avatar Nov 24 '22 07:11 App10xHarshal

To get this working on an Ionic App using Typescript I used the code below.

I had to add the following line otherwise bullet or numbered lists didn't work properly because pressing "Enter" at the end of the list item would create a new bullet but then jump the cursor down below the bullet/numbered list (hopefully the ops[1].attributes change won't cause any other issues):

ops[1].attributes

This is the full code that I used:

quillContentChanged(event) {

    try {
      // Resolve bug with Android 12 not setting correct cursor position when pressing "Enter" on keyboard
      const ops = event.delta.ops;
      const oldSelection = event.editor.getSelection();
      const oldPos = oldSelection ? oldSelection.index : null;
      const oldSelectionLength = oldSelection ?  oldSelection.length : 0;
      if (ops[0].retain === undefined ||
          !ops[1] ||
          !ops[1].insert ||
          !ops[1].insert ||
          ops[1].insert !== '\n' ||
          oldSelectionLength > 0 ||
          ops[1].attributes) {
          return;
      }
      setTimeout(() => {
          const newPos = event.editor.getSelection().index;
          if (newPos === oldPos) {
              console.log('Change selection bad pos');
              event.editor.setSelection(event.editor.getSelection().index + 1, 0);
          }
      }, 30);
    } catch { }
  }

<quill-editor no-blur (onContentChanged)="quillContentChanged($event)" [(ngModel)]="testing" customToolbarPosition="bottom"></quill-editor>

webfletch avatar Dec 21 '22 23:12 webfletch

class SupportGBoard { constructor(quill) { this.quill = quill; quill.on("editor-change", this.handle.bind(this)); }

handle(eventName, ...args) {
    if (eventName === "text-change") {
        let ops = args[0]["ops"];
        let oldSelection = this.quill.getSelection(true);
        let oldPos = oldSelection.index;
        let oldSelectionLength = oldSelection.length;

        let retain = undefined;
        let insert = undefined;
        for (let j = 0; j < ops.length; j++) {
            let keys = Object.keys(ops[j]);
            if (keys.includes("retain") && retain === undefined) {
                retain = ops[j]["retain"];
            }
            if (keys.includes("insert")) {
                insert = ops[j]["insert"];
            }
        }

        if (
            retain === undefined ||
            insert === undefined ||
            insert != "\n" ||
            oldSelectionLength > 0
        ) {
            return;
        }

        setTimeout(function () {
            try {
                let newPos = this.quill.getSelection(true).index;
                if (newPos === oldPos) {
                    console.log("Change selection bad pos");
                    this.quill.setSelection(
                        this.quill.getSelection(true).index + 1,
                        0
                    );
                }
            } catch (err) {
                this.quill.focus();
            }
        }, 30);
    }
}

}

Quill.register("modules/supportGBoard", SupportGBoard);

How to apply it on dynamically imported Quill? Here's my code:

import React, { useState } from "react";
import dynamic from "next/dynamic";
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });

const modules = {
    toolbar: {
        container: [
            ["bold", "italic", "underline", "link"],
        ]
    },
};

const formats = [
    "header",
    "bold",
    "italic",
    "underline",
    "link",
];

const RichTextEditor = ({ field }) => {
    return (
            <>
                <ReactQuill
                    theme="snow"
                    value={field.value}
                    formats={formats}
                    modules={modules}
                    className="flex flex-col-reverse"
                    onChange={field.onChange(field.name)}
                />
            </>
    );
};

export default RichTextEditor;

molul avatar Jan 07 '23 10:01 molul

Ok I figured out how to do it, but the provided module doesn't work (throws an error "Cannot read properties of undefined (reading 'focus')" on the line this.quill.focus.

If anyone wants to know how to register the module, here's my code: import React, { useState } from "react";


class SupportGBoard {
    constructor(quill) {
        this.quill = quill;
        quill.on("editor-change", this.handle.bind(this));
    }

    handle(eventName, ...args) {
        if (eventName === "text-change") {
            
            let ops = args[0]["ops"];
            let oldSelection = this.quill.getSelection(true);
            let oldPos = oldSelection.index;
            let oldSelectionLength = oldSelection.length;

            let retain = undefined;
            let insert = undefined;
            for (let j = 0; j < ops.length; j++) {
                let keys = Object.keys(ops[j]);
                if (keys.includes("retain") && retain === undefined) {
                    retain = ops[j]["retain"];
                }
                if (keys.includes("insert")) {
                    insert = ops[j]["insert"];
                }
            }

            if (
                retain === undefined ||
                insert === undefined ||
                insert != "\n" ||
                oldSelectionLength > 0
            ) {
                return;
            }

            setTimeout(function () {
                try {
                    let newPos = this.quill.getSelection(true).index;
                    if (newPos === oldPos) {
                        console.log("Change selection bad pos");
                        this.quill.setSelection(
                            this.quill.getSelection(true).index + 1,
                            0
                        );
                    }
                } catch (err) {
                    console.log(err);
                    this.quill.focus();
                }
            }, 30);
            
        }
    }
}

import dynamic from "next/dynamic";
//const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
const ReactQuill = dynamic(
    async () => {
        const { default: RQ } = await import("react-quill");
        RQ.Quill.register("modules/supportGBoard", SupportGBoard);
        return function forwardRef({ forwardedRef, ...props }) {
            return <RQ ref={forwardedRef} {...props} />;
        };
    },
    {
        ssr: false,
    }
);

const modules = {
    toolbar: {
        container: [
            ["bold", "italic", "underline", "link"],
        ]
    },
    supportGBoard: true,
};

const formats = [
    "header",
    "bold",
    "italic",
    "underline",
    "link",
];

const RichTextEditor = ({ field }) => {
    return (
            <>
                <ReactQuill
                    theme="snow"
                    value={field.value}
                    formats={formats}
                    modules={modules}
                    className="flex flex-col-reverse"
                    onChange={field.onChange(field.name)}
                />
            </>
    );
};

export default RichTextEditor;

molul avatar Jan 08 '23 17:01 molul

Good news, it seems that Google finally could fix the problem: https://issuetracker.google.com/issues/177757645

martinstoeckli avatar Jan 16 '23 08:01 martinstoeckli

still h

Good news, it seems that Google finally could fix the problem: https://issuetracker.google.com/issues/177757645

it still happened

Atom02 avatar Mar 24 '23 07:03 Atom02

Quill 2.0 has been released (announcement post) with many changes and fixes. If this is still an issue please create a new issue after reviewing our updated Contributing guide :pray:

quill-bot avatar Apr 17 '24 10:04 quill-bot