quill
quill copied to clipboard
Cursor not moved when newline typed in Android
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
- Use a mobile phone with Chrome browser
- Visit https://quilljs.com/standalone/snow/
- 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
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
Have the same issue
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.
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.
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 - 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 - 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?
I wonder if the workaround mentioned here might also resolve this? https://github.com/quilljs/quill/issues/3153
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)
is there any news about it? We have the same issue
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
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 {
}
}
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
Any updates to this issue? Can confirm still occuring.
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
Personally I switched to Tiptap editor, it also built on top ProseMirror and it has formatting example
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
reproduced on version 1.3.7
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?
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
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>
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;
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;
Good news, it seems that Google finally could fix the problem: https://issuetracker.google.com/issues/177757645
still h
Good news, it seems that Google finally could fix the problem: https://issuetracker.google.com/issues/177757645
it still happened
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: