Jsdom: offsetWidth, offsetHeight, offsetTop, and offsetLeft

Created on 1 Feb 2011  ·  32Comments  ·  Source: jsdom/jsdom

I'm trying to implement a server-side renderer for HighCharts. Everything appears to work fine except the resulting SVG markup is missing x, y, width, and height values all over the place. Through some debugging I believe I've pinpointed the problem to the fact that offsetWidth, offsetHeight, offsetTop and offsetLeft are all undefined for each element.

Are these properties slated to be included in a future release? Also, is there a workaround I can implement in the time being?

css feature layout needs tests

Most helpful comment

I use this snippet for support, which works for my case. Keep in mind it is far from a proper implementation. Note that window should be your document.parentWindow.

If you need a better implementation, extend the code using the spec:
http://dev.w3.org/csswg/cssom-view/#dom-htmlelement-offsetleft

Object.defineProperties(window.HTMLElement.prototype, {
  offsetLeft: {
    get: function() { return parseFloat(window.getComputedStyle(this).marginLeft) || 0; }
  },
  offsetTop: {
    get: function() { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
  },
  offsetHeight: {
    get: function() { return parseFloat(window.getComputedStyle(this).height) || 0; }
  },
  offsetWidth: {
    get: function() { return parseFloat(window.getComputedStyle(this).width) || 0; }
  }
});

All 32 comments

I think http://www.w3.org/TR/cssom-view/, would need to be implemented.

we have started integrating cssom which should solve some of the SVG problems.

It would indeed be great if we could access the offset* properties, would need those for a canvas-related project.

How is the progress in this issue? i would also appreciate such a feature for backend sag rendering with Highcharts.

I would love to have this too :)

Do you guys have any spare cycles to help? Personally I don't need svg 1.1 but I'd be willing to help land it if you guys are fired up enough to work on it as well.

@tmpvar: I'd like to help, but I'm pretty new to those server-side JS-stuff.
Maybe you can hint me on where to start and I could try my best to get something going :)

I'm very interested in SVG 1.1 too, to enable highcharts on the server. I'd like to offer help, but first I need to get acquainted with SVG 1.0 and its current implementation in jsdom, which might take a while ;)

I think the first step here is to grab a copy of the test suite (http://www.w3.org/Graphics/SVG/WG/wiki/Test_Suite_Overview) and convert it to be headless (which may be quite an undertaking)

Then we can add a level2/svg.js and start implementing.

I'll probably get around to this eventually, but it is not high on my list of jsdom priorities right now.

@href the svg 1.0 implementation is pretty bare. It passes the tests in level1/core but doesn't really implement anything svg specific.

I think, the getBoundingCLientRect is not implement correctly.

I've this code to calculate specific svg element :

var html = '<!DOCTYPE html><html><head><title></title></head><body style="margin: 0; padding: 0;"><svg id="svg" xmlns="http://www.w3.org/2000/svg" version="1.1" style="margin: 0; padding: 0;"><rect style="" x="10" y="10" width="100" height="100" rx="0" ry="0"/></svg></body></html>';

        jsdom.env({
            html : html,
            src: [jquery],
            done: function (errors, window) {
                var $ = window.$;
                var clientBox = $("#svg").find(type)[0].getBoundingClientRect();
                console.log(clientBox);
            }
        });

And the result is :

{ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 }

But, if i test the same html in web browser, the result is this :

{ bottom: 110, height: 100, left: 10, right: 110, top: 10, width: 100 }

@throrin19: Read through #653.

So it is impossible to achieve? So far I managed to bypass this concern through phantomJS but it is extremely slow and if I have a big SVG, it crashes.

I mean, it's not impossible, but we'd have to write a whole layout engine, and nobody's had the time to do that.

I've thought about it, but then my brain spasms and I have to walk away.

Are there tests beyond acid that could be followed?

+1 on finding this a highly desirable feature

I use this snippet for support, which works for my case. Keep in mind it is far from a proper implementation. Note that window should be your document.parentWindow.

If you need a better implementation, extend the code using the spec:
http://dev.w3.org/csswg/cssom-view/#dom-htmlelement-offsetleft

Object.defineProperties(window.HTMLElement.prototype, {
  offsetLeft: {
    get: function() { return parseFloat(window.getComputedStyle(this).marginLeft) || 0; }
  },
  offsetTop: {
    get: function() { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
  },
  offsetHeight: {
    get: function() { return parseFloat(window.getComputedStyle(this).height) || 0; }
  },
  offsetWidth: {
    get: function() { return parseFloat(window.getComputedStyle(this).width) || 0; }
  }
});

Has anyone else found success with what tobyhinloopen suggested?

@tobyhinloopen: My graph background, axis, and labels show up (albeit in a skewed way) but the rest is still vey muddled. Were you able to get a complete graph?

I had a different use case where my suggested implementation worked. If it doesn't work for you, extend it. Likely OffsetHeight/width must also include the padding. I would suggest some hacky hotfixing to make it work, because a perfect implementation is difficult to implement.

The solution proposed by @tobyhinloopen seems fine but when I use it I get "cannot refined property offsetLeft"

@zallek I think you already have offsetLeft support then :) What does window.HTMLElement.prototype.offsetLeft return? If it is anything but undefined, my solution won't work

Just wanted to say thank you @tobyhinloopen, your solution was very useful to me. :)

@tobyhinloopen Thanks very much for posting the solution! It's very helpful and does work so long as I use a variable to set the value of offset and exclude this. this is coming out undefined, which kind of makes sense given that the context is not the actual class. Is that what you'd expect?

@jordantomax this is bound to the element when the function is called, not when the function is defined. ‘this’ should be the element when you’re calling it from any html element.

If it doesn’t, I like to see an example.

@tobyhinloopen Thanks for the quick response. That's what I would expect. When I try to reproduce my issue in codesandbox using real browser environment, it works correctly. But when using jest, I'm seeing this as undefined. I created a small sample repo to show you what I'm experiencing, maybe my setup is wrong.

https://github.com/jordantomax/jest-htmlelement-prototype-sandbox

Related, I'm also having problems overriding methods like scrollIntoView using jest and jsdom. Is there a recommended way to do it?

Thanks so much for your help!

I think it is because you use get: () => {} - The () => {} function syntax messes with your this. If you use get() {} (recommended) or get: function() {} (alternative) instead, it will work. @jordantomax

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_separate_this

You also use => in your test suite's it/describe's. I don't know about JEST, but in MOCHA it isn't recommended, since this in your it/test/describe has all kinds of utilities you cannot access if you use the arrow functions. For example, mocha has things like this.slow(), this.timeout(4000) etc.

https://mochajs.org/#arrow-functions

Related, I'm also having problems overriding methods like scrollIntoView using jest and jsdom. Is there a recommended way to do it?

Usually there is some kind of polyfill available on MDN. Otherwise, just attempt to implement it based on the spec in a way that suits your needs. If all fails, just use a different testing environment. JSDom has its place, but at some point you might want to consider a real browser instead. Many testing environments allow testing in real browsers (even multiples at the same time) while also reporting back realtime to your development machine. IIRC Karma does this -> https://karma-runner.github.io/latest/index.html

I have had multiple browsers open (firefox chrome safari internet explorer edge + mobile ones in simulators) all at once on my machine, and they all reported back realtime using Karma + Mocha.

@tobyhinloopen Right you are on all accounts! Thank you!! I'm still a little unclear about why getComputedStyle is necessary, and is it possible to write a setter given that method. For me this seems to work:

Object.defineProperties(window.HTMLElement.prototype, {
  offsetTop: {
    get () {
      return this.marginTop
    },
    set (offset) {
      this.marginTop = offset
    }
  }
})

Thanks again 🙏

@jordantomax getComputedStyle fetches the real computed style for all CSS properties, no matter where it was applied (stylesheet, style attribute or from JS). I'm not sure if this.marginTop does the same, but I guess it does (but only for margin obviously, not for all CSS props)

@tobyhinloopen Ah, got it. Thank you!

Thank you @tobyhinloopen . I use an even shorter version. I think getComputedStyle is not really needed since I personally never add any css to my unit tests unless it's inline, which is why .style is good enough and faster.

Object.defineProperties(window.HTMLElement.prototype, {
  offsetLeft: {
    get () { return parseFloat(this.style.marginLeft) || 0 }
  },
  offsetTop: {
    get () { return parseFloat(this.style.marginTop) || 0 }
  },
  offsetHeight: {
    get () { return parseFloat(this.style.height) || 0 }
  },
  offsetWidth: {
    get () { return parseFloat(this.style.width) || 0 }
  }
})

Has this been fixed, I am getting offsetWidth = 0

   const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
    let document = dom.window.document
    let span = document.createElement("span");
    span.innerHtml = 'test
    span.style.fontSize = 150 + 'px';
    span.style.fontFamily = 'Arial'
    document.body.appendChild(span)
    console.log(span.offsetWidth) // 0

Has this been fixed, I am getting offsetWidth = 0

@petran it's not realistic to expect jsdom the calculate the width of your SPAN, since it requires knowledge of the size of your font and all inherited CSS properties. I don't think that's the goal of JSDOM. If you want a more accurate simulation of a browser, consider using an actual browser.

Was this page helpful?
0 / 5 - 0 ratings