yuku / textcomplete

Autocomplete for HTMLTextAreaElement and more.

Home Page:https://yuku.takahashi.coffee/textcomplete/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TypeError: Cannot assign to read only property 'left' of object '#<ClientRect>'

metamatt opened this issue · comments

I don't have 100% understanding of this bug yet but I'm curious if anyone else has seen it.

If I use jquery-textcomplete with a strategy that completes asynchronously (has to go to the server to fetch a list of results, and then my search callback calls jquery-textcomplete's callback with the results a little later), and if the user types the trigger sequence and then backspaces over it quickly, then occasionally I get an exception with this call stack:

TypeError: Cannot assign to read only property 'left' of object '#<ClientRect>'
    at b._getCaretRelativePosition (jquery.textcomplete.js:1273)
    at b.getCaretPosition (jquery.textcomplete.js:998)
    at jquery.textcomplete.js:331
    at (my code that calls the search callback)

I can repro this in versions 1.3.3 and 1.8.0 of jquery-textcomplete, using Chrome 56, at least.

This line of code is throwing the exception:

      var $node = $(node);
      var position = $node.offset();
      position.left -= this.$el.offset().left;

I confirmed this happens if my async search callback completes after deactivate got called. It seems that if we try to show a new dropdown too soon after hiding the previous one, the jQuery $offset() call returns a frozen object.

My version of jquery has this for offset():

		rect = elem.getBoundingClientRect();

		// Make sure element is not hidden (display: none)
		if ( rect.width || rect.height ) {
			doc = elem.ownerDocument;
			win = getWindow( doc );
			docElem = doc.documentElement;

			return {
				top: rect.top + win.pageYOffset - docElem.clientTop,
				left: rect.left + win.pageXOffset - docElem.clientLeft
			};
		}

		// Return zeros for disconnected and hidden elements (gh-2310)
		return rect;

That is, it calls HTMLElement.getBoundingClientRect() and maybe returns the result of that directly (if width and height are 0, for a hidden element), or maybe returns a fresh object with only left and top properties.

In my version of Chrome, the object returned by HTMLElement.getBoundingClientRect() is a ClientRect object that is not writable -- though it does not look frozen:

r=$0.getBoundingClientRect()
ClientRect {top: 451, right: 934, bottom: 471, left: 402, width: 532…}
r.constructor.name
"ClientRect"
Object.isFrozen(r)
false
Object.getOwnPropertyDescriptors(r)
Object {}
r.left
402
r.left = 0
0
r.left
402
(function() { 'use strict'; r.left = 0; })()
VM16171:1 Uncaught TypeError: Cannot assign to read only property 'left' of object '#<ClientRect>'
    at <anonymous>:1:36
    at <anonymous>:1:43

(Note that Object.isFrozen says it is not frozen, and it has no special property descriptors, but an attempt to write to it from the normal debugger REPL is silently ignored, and an attempt to write to it from a strict mode function throws an exception.)

Arguably that's a jQuery bug and it shouldn't sometimes return a native browser object that is not writable, but, we could work around this in jquery-textcomplete by not writing to the object returned from jqNode.offset(). But either way, if I do that, I avoid this crash, but something is still unhappy in the case that the menu is dismissed before the search callback provides the data: if I type some text that triggers textcomplete, then delete it before the search callback happens, then the search callback does happen: then the textcomplete dropdown appears and is not dismissed, and pressing Enter triggers this different error:

TypeError: Failed to execute 'setStartAfter' on 'Range': parameter 1 is not of type 'Node'.
    at ContentEditable.select (jquery.textcomplete.js:1250)
    at Completer.select (jquery.textcomplete.js:287)
    at Dropdown._enter (jquery.textcomplete.js:707)
    at Dropdown._onKeydown (jquery.textcomplete.js:652)
    at HTMLDivElement.proxy (jquery.js:502)
    at HTMLDivElement.dispatch (jquery.js:5201)
    at HTMLDivElement.elemData.handle (jquery.js:5009)