fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
29.03k stars 3.51k forks source link

No keyboard on Android when editing iText #6588

Open timoostrich opened 4 years ago

timoostrich commented 4 years ago

I don't know when this stopped working, but it looks like the keyboard does not show up on Android anymore, when you enter the editing mode of an iText object. I tried to set the focus on the hidden textfield as a workaround but I had no success.

So I checked the iText demos and realized it's not working there, too: http://fabricjs.com/test/misc/itext.html On iOS the keyboard shows up.

Any ideas how to fix this? It is an essential feature of my app...

I still love fabric.js and have to thank you so much for this awesome library!

jaseel001 commented 4 years ago

Same issue for me. It is not working in android chrome latest version (85). Working in all other android browsers. I think issue since last chrome update @Timoostrich Please comment if you found any solution

timoostrich commented 4 years ago

Just did some more tests. Firefox on Android (latest version) works fine - the keyboard comes up. Huawei's Browser has the same problem like chrome - no keyboard comes up.

I also tried to do focus() or click() on the hiddenTextarea, but I had no success.

jaseel001 commented 4 years ago

I found a solution for now. Modified initHiddenTextarea function like this const initHiddenTextarea = function() { this.hiddenTextarea = fabric.document.createElement('textarea'); this.hiddenTextarea.setAttribute('autocapitalize', 'off'); this.hiddenTextarea.setAttribute('autocorrect', 'off'); this.hiddenTextarea.setAttribute('autocomplete', 'off'); this.hiddenTextarea.setAttribute('spellcheck', 'false'); this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); this.hiddenTextarea.setAttribute('wrap', 'off'); var style = this._calcTextareaPosition(); // line-height: 1px; was removed from the style to fix this: // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + ' paddingーtop: ' + style.fontSize + ';'; fabric.document.body.appendChild(this.hiddenTextarea); fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); if (!this._clickHandlerInitialized && this.canvas) { fabric.util.addListener(this.canvas.upperCanvasEl, 'touchstart', this.clickFunc.bind(this)); fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.clickFunc.bind(this)); this._clickHandlerInitialized = true; } }

Extra added code is fabric.util.addListener(this.canvas.upperCanvasEl, 'touchstart', this.clickFunc.bind(this));

and extended fabric text box like this, fabric.util.object.extend(fabric.Textbox.prototype, { initHiddenTextarea: initHiddenTextarea });

asturur commented 4 years ago

i ll give it a look, i do not have an android device anymore.

timoostrich commented 4 years ago

I found a solution for now. Modified initHiddenTextarea function like this const initHiddenTextarea = function() { this.hiddenTextarea = fabric.document.createElement('textarea'); this.hiddenTextarea.setAttribute('autocapitalize', 'off'); this.hiddenTextarea.setAttribute('autocorrect', 'off'); this.hiddenTextarea.setAttribute('autocomplete', 'off'); this.hiddenTextarea.setAttribute('spellcheck', 'false'); this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); this.hiddenTextarea.setAttribute('wrap', 'off'); var style = this._calcTextareaPosition(); // line-height: 1px; was removed from the style to fix this: // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + ' paddingーtop: ' + style.fontSize + ';'; fabric.document.body.appendChild(this.hiddenTextarea); fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); if (!this._clickHandlerInitialized && this.canvas) { fabric.util.addListener(this.canvas.upperCanvasEl, 'touchstart', this.clickFunc.bind(this)); fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.clickFunc.bind(this)); this._clickHandlerInitialized = true; } }

Extra added code is fabric.util.addListener(this.canvas.upperCanvasEl, 'touchstart', this.clickFunc.bind(this));

and extended fabric text box like this, fabric.util.object.extend(fabric.Textbox.prototype, { initHiddenTextarea: initHiddenTextarea });

What does is do? Did you only remove the 1px line-height?

aldyrpl commented 4 years ago

this happened to me too :(, any solution?

timoostrich commented 4 years ago

My quick workaround is an additional textarea which is synced to the hiddentextarea of fabric: https://lettering.org/lettering-generator/

`//Sync hiddenTextarea and input in containter function syncTextToInput(){ var $textarea = document.querySelector('textarea[data-fabric-hiddentextarea]'); var $input = document.querySelector(".js-synced-text"); $input.value = $textarea.value; }

function syncTextToCanvas(){ var $textarea = document.querySelector('textarea[data-fabric-hiddentextarea]'); var $input = document.querySelector(".js-synced-text"); $textarea.value = $input.value; var event = new Event('input'); $textarea.dispatchEvent(event); }`

havlicekp commented 3 years ago

Same issue here, Chrome 92 on Android 10 - tapping on a text box doesn't start edit. The most current Firefox for Android is fine.

@Timoostrich Does your workaround work? I've tried https://lettering.org/lettering-generator/ in Android Chrome and seems the issue is still there. Very nice project, though!

timoostrich commented 3 years ago

Same issue here, Chrome 92 on Android 10 - tapping on a text box doesn't start edit. The most current Firefox for Android is fine.

@Timoostrich Does your workaround work? I've tried https://lettering.org/lettering-generator/ in Android Chrome and seems the issue is still there. Very nice project, though!

Thank you! Well, there should appear some kind of layer whenever you want to edit a textbox. It's just an additional text input which is synced to the hidden input of fabric.

KevinShea1990 commented 3 years ago

same issue, chromium 92.0.4515.131, Samsung Note Android 10. No keyboard when tapping on itext box. Tried the demo page: http://fabricjs.com/test/misc/itext.html It's a serious bug and it makes fabricjs useless on android phones. Please fix it asap.

asturur commented 3 years ago

i don't have any android device anymore where i can test those kind of things

KevinShea1990 commented 3 years ago

I've found the root cause of this bug. For Chrome on android, single touch will wrongly trigger touchmove event, while chrome on ios doesn't behave like this. The fix is to record the point of touchstart event, and compare the saved point with current touch point in touchmove event, if they are too close, then we know it's not a touch move event, and this touchmove event is ignored. fix.zip In the attached fix.zip, it contains a diff and fixed version of fabric.js v4.6.0.

asturur commented 3 years ago

single touch should trigger both touchStart and touchMove. If touchStart isn't triggered, this is a chrome bug and i m not sure why fabricJS should fix it

KevinShea1990 commented 3 years ago

touchstart is triggered on single touch. But touchmove is not supposed to be triggered for single touch if there is no dragging. I think it's a bug in chrome on android. My fix is a workaround to make editing of itext work in chome on android.

natschz commented 2 years ago

So are there any updates regarding this? Becase as is, this is not usable on android phones. Someone got a workaround at least?

hanna-becker commented 1 year ago

Inspired by @KevinShea1990's fix, I built a workaround for the latest Fabric version (6.0.0-beta10). I'm subscribing to the 'touchstart' and 'touchmove' events. If it's Android and the active item on 'touchstart' is an IText item, I'm locking its position, so it cannot be moved on the canvas. Then, in the 'touchmove' event, if the distance is large enough from the 'touchstart' coordinates, I'm unlocking the object again.

This way, entering text edit mode on Android works reliably every time, and not only on ~1/10 attempts like before.

const isAndroid = /Android/.test(navigator.userAgent);

let touchStartX = 0;
let touchStartY = 0;

canvas.getSelectionElement().addEventListener('touchstart', (e) => {
    if (isAndroid) {
        const touch = e.touches[0] || e.changedTouches[0];
        touchStartX = touch.pageX;
        touchStartY = touch.pageY;
        const active = canvas.getActiveObject();
        if (active?.isType('i-text')) {
            (active as fabric.IText).lockMovementX = true;
            (active as fabric.IText).lockMovementY = true;
        }
    }
});

canvas.getSelectionElement().addEventListener('touchmove', (e) => {
    if (isAndroid) {
        const active = canvas.getActiveObject();
        if (active?.isType('i-text')) {
            const touch = e.touches[0] || e.changedTouches[0];
            if (Math.abs(touch.pageX - touchStartX) > 1.5 && Math.abs(touch.pageY - touchStartY) > 1.5) {
                (active as fabric.IText).lockMovementX = false;
                (active as fabric.IText).lockMovementY = false;
            }
        }
    }
});

You can find a minimal example where I'm using this in an Angular app here: https://codesandbox.io/p/sandbox/strange-burnell-4l5dxt

If this behavior is a bug in Chrome for Android, has anyone found or filed an issue for it, yet?

Alatr commented 1 year ago

Hi, the bug still reproduces. I'm not sure about the initial description but seems it's about the same problem, but I have one more cause for reproducing.

I'm using the tablet Samsung Galaxy Tab S7, the last Chrome 114 version.

Steps:

  1. open link http://fabricjs.com/test/misc/itext.html.

  2. Click on the input and start the edit

  3. Click the button to hide the keyboard image

  4. Click the input again

Expect:

  1. The keyboard is open

Actual:

  1. The keyboard is hidden

add the video https://github.com/fabricjs/fabric.js/assets/42271438/92273c13-c30b-46e1-8a0d-9ca07c9c42ce

@asturur Is it a known issue? (if exit from the editing and enter editing again, the keyboard is shown) I tried to set the focus on the hidden text field as a workaround but I had no success.

ShaMan123 commented 1 year ago

From your description it sounds that we should listen to an event that triggers when the keyboard minimizes. Is there such a thing? Does the textarea blur when the keyboard minimizes? If so maybe trying to override the blur hook will fix this.

Alatr commented 1 year ago

From your description it sounds that we should listen to an event that triggers when the keyboard minimizes. Is there such a thing?

Seems we don't have info about this event, therefore we cannot track hide the keyboard

Does the textarea blur when the keyboard minimizes?

No textarea is always in the focus

what’s interesting is the simple click on the focused input must show up the keyboard but not in this case Even if we will be dispatch click event on the textarea. Might textarea have some interceptors

asturur commented 1 year ago

does this works for normal forms after you hide the keyboard? if you have some text input on html without fabric, does the keyboard behave as you want? i don't have an android device to test.

natschz commented 1 year ago

I think we worked around the issue, by just having input we managed ourselfs.

Something like this:

import React, {useState} from "react";

const [hasSelectedObject, setHasSelectedObject] = useState(false)
const [hasSelectedTextObject, setHasSelectedTextObject] = useState(false)
const [selectedTextObjectText, setSelectedTextObjectText] = useState("")

const currentCanvas = new fabric.Canvas(canvasRef.current);

currentCanvas.on('selection:created', (options) => {
  setHasSelectedObject(true)
  setHasSelectedTextObject(options.selected.some(activeObject => activeObject.type == "i-text"))
  setSelectedTextObjectText(options.selected.find(activeObject => activeObject.type == "i-text")?.text)
});

currentCanvas.on('selection:updated', (options) => {
  setHasSelectedObject(true)
  setHasSelectedTextObject(options.selected.some(activeObject => activeObject.type == "i-text"))
  setSelectedTextObjectText(options.selected.find(activeObject => activeObject.type == "i-text")?.text)
});

currentCanvas.on('selection:cleared', (options) => {
  setHasSelectedObject(false)
  setHasSelectedTextObject(false)
  setSelectedTextObjectText("")
});

const onTextInputChange = (e) => {
  setSelectedTextObjectText(e.target.value)

  if (hasSelectedObject) {
    const textObject = canvas.getActiveObjects().find(activeObject => activeObject.type == "i-text")
    textObject.text = e.target.value
    canvas.renderAll();
  }
}

return <>
  <canvas
    ref={canvasRef}
    className={"fabric-canvas"}
  />

  {hasSelectedTextObject && <>
    <label htmlFor="text-input">
       LABEL
    </label>
    <input
      id={"text-input"}
      className={"text-input"}
      value={selectedTextObjectText}
      type="text"
      onChange={onTextInputChange}
    />
  </>}
</>
asturur commented 1 year ago

but i don't see anything in particular for the onFocus on that input that would help. Is Android discarding the input because is not on view? If you don't hide the keyboard with the button, does it react to input changes? If you click a different text object, does the keyobard come up anyway?

If we are not triggering any focus events when clicking again on text i m not sure is going to do anything.

natschz commented 1 year ago

This way you have full control over the text input field, so you can do whatever you desire I would say, even though it's just a workaround. It's also quite some time ago since I implemented it, so I don't know about the details anymore.

Alatr commented 1 year ago

does this works for normal forms after you hide the keyboard? if you have some text input on html without fabric, does the keyboard behave as you want?

I tried so many ways to solve this but without success.

Also, I tested the behavior with pure HTML textarea and manually triggered the event via JS, absolutely did not work and the keyboard did not come up.

Seems this cause is sensitive for Android, and probably the system expects an event with the key isTrusted equal to true or something like this (with real clicks all works fine)

asturur commented 1 year ago

I don't get a full picture of the issue tbh. The keyboard does not come up after you manually minimize it. How does it behave on normal html input fields outside of fabricJS? What happens if you switch from a fabricJS textbox to another? There are so many combination possible knowing which one do not work could help understanding where the unhappy path is

timoostrich commented 1 year ago

The basic problem is that the keyboard simply does not show up on chrome android when you try to edit the text of an itext object.

Normal (visible) input fields do work without any problems.

hanna-becker commented 1 year ago

For me, it does show up with my above workaround. (Chrome on Android seems to erroneously emit a touchmove event, even when just tapping on the screen.) However, when hiding it, unlike for normal input fields, it does not reappear when tapping onto the same itext item again, even though tapping again will move the cursor, indicating we're still in editing mode.

I am not using the hidden textarea workaround, though.

ShaMan123 commented 1 year ago

The basic problem is that the keyboard simply does not show up on chrome android when you try to edit the text of an itext object.

Normal (visible) input fields do work without any problems.

I that case I wonder... what happens if you change the css and remove the opacity/z-index values? Will that fix the issue? Will setting color: transparent act as a valid workaround?

asturur commented 1 year ago

or what happens if you touch another textbox?

Alatr commented 1 year ago

The basic problem is that the keyboard simply does not show up on chrome android when you try to edit the text of an itext object. Normal (visible) input fields do work without any problems.

I that case I wonder... what happens if you change the css and remove the opacity/z-index values? Will that fix the issue? Will setting color: transparent act as a valid workaround?

changing styles is not working

Alatr commented 1 year ago

or what happens if you touch another textbox?

new focus leads to showing the keyboard

I think it's not a fabric issue, the issue is in the Android system, when we minimized the keyboard we couldn't show it again while using a manual trigger js event

the minimized keyboard we can show only with real touch on the input/textarea

asturur commented 1 year ago

It is an implementation issue with android and the many version of it for sure. If we can work around it great. One example could be that every click, even if the textbox is the same, we destroy and re-create the textarea. Maybe optionally since that could bother other flows with languages that are not english.

We should make a prototype for a quick test and need to rely on someone that can reproduce the bug easily. Is not something we are going to do before 6.0.0 release, but if you want to try and you understood what i mean you are free to spin a test.

Alatr commented 1 year ago

It is an implementation issue with android and the many version of it for sure. If we can work around it great. One example could be that every click, even if the textbox is the same, we destroy and re-create the textarea. Maybe optionally since that could bother other flows with languages that are not english.

We should make a prototype for a quick test and need to rely on someone that can reproduce the bug easily. Is not something we are going to do before 6.0.0 release, but if you want to try and you understood what i mean you are free to spin a test.

It sounds pretty hacky, but it's might be working. I'll think about it