ractivejs / ractive

Next-generation DOM manipulation
http://ractive.js.org
MIT License
5.94k stars 396 forks source link

<input> on Safari, caret position moves when updateStringValue(...) is not caused by twoway #3281

Closed giovannipiller closed 6 years ago

giovannipiller commented 6 years ago

Description:

Title might need some improvement I know. Anyway, I noticed this issue that is Safari specific, but might be interesting to work-around internally.

Steps:

  1. have an input with twoway="false": <input type="text" twoway="false" value="{{who}}">
  2. attach an event that manages the "twoway", by listening for input and setting this.set({ who }); when appropriate
  3. load Safari, and start typing at the beginning of the input

Result: As soon as you type the first key, the caret will be moved at the end of the input. See attached GIF: caret-position-change-safari

Versions affected:

1.0.1, likely prior as well

Platforms affected:

Reproduced on Safari 12, 11

Reproduction:

JSFiddle here.

const r = window.r = new Ractive({
  el: '#main',
  template: `
  <h1>Hello</h1>
  <div>
    <p>Start typing <strong>BEFORE</strong> "World".</p>
    <input type="text" on-input="edit-who" twoway="false" value="{{who}}">
  </div>
  <p style="color: red">{{error}}</p>
  `,

  data: {
    who: 'World'
  },

  on: {
    'edit-who': function(context) {
      const { event } = context;
      const who = event.target.value;

      const originalCaretPosition = event.target.selectionStart;

      this.set({ who });

      const finalCaretPosition = event.target.selectionStart;

      console.log({
        originalCaretPosition,
        finalCaretPosition
      })

      if (originalCaretPosition !== finalCaretPosition) {
        this.set({
          error: 'Caret has been moved to the end of the input.'
        });
      } else {
        this.set({
          error: null
        });
      }
    },
  },
});

Fix (aka workaround):

Looks like Safari moves caret position whenever the value of the focused input is changed, as long as this change is not directly performed by a user action (ex. normal user typing). Which is why twoway="true" would work just fine.

Example: input.value = input.value; moves the caret at the end in Safari. Not in Chrome/Firefox.

By setting the value after the input phase, Ractive will rightfully attempt to update input's value during one of its update calls, using the updateStringValue(..) function.

An internal workaround might be to check if input.value need to be updated at all. For example, by changing updateStringValue to something like this:

// excerpt from src/view/items/element/attribute/getUpdateDelegate.js
function updateStringValue(reset) {
  if (!this.locked) {
    if (reset) {
      this.node._ractive.value = '';
      this.node.removeAttribute('value');
    } else {
      const value = this.getValue();

      this.node._ractive.value = value;

      const safeValue = safeToStringValue(value);

      // FIX: prevents Safari from moving caret position when replacing a value with the same value
      if (this.node.value !== safeValue) {
        this.node.value = safeToStringValue(value);
      }

      this.node.setAttribute('value', safeToStringValue(value));
    }
  }
}

TODO

I still have to check WebKit's Bugzilla for this behaviour. In the meantime I'm opening this issue to start the discussion. WebKit issue: https://bugs.webkit.org/show_bug.cgi?id=191255

TL;DR

input.value = input.value; moves the caret at the end of the input in Safari. Not in Chrome/Firefox. Might be worth to add a workaround for edge cases.

giovannipiller commented 6 years ago

Created an issue in WebKit Bugzilla: https://bugs.webkit.org/show_bug.cgi?id=191255

giovannipiller commented 6 years ago

Just in case somebody needs a workaround, you can store caret position and restore after the set(...). Something like this:

// store caret position
const originalCaretPosition = event.target.selectionStart;

this.set({ who });

// restores caret position
event.target.setSelectionRange(originalCaretPosition, originalCaretPosition);
evs-chris commented 6 years ago

I think the check on this.node.value !== safeValue is good. Since you've already solved it would you like to do a PR for credit? If not, no worries, I'll get it in a bit. There's probably not a good way to write a test for this, since the CI is all chrome-based and I don't have access to Safari for manual testing, so a new test case isn't even necessary.