Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert absolute to relative CSS values using Javascript

I was wondering if it's possible and feasible to convert absolute CSS values to relative ones while maintaining proportions using Javascript. The solution must work with HTML as well as SVG markup.

Say you got this:

<div style="width:100%;height:800px">
  <svg width="1000" height="500">
    <rect width="30" height="50" x="34" y="24"/>
  </svg>
</div>

How would you achieve this?

<div style="width:100%;height:100%">
  <svg width="83%" height="62.5%">
    <rect width="3%" height="10%" x="3.4% y="4.8%"/>
  </svg>
</div>

I'd

  1. start at the root element (div)
  2. get .width() and .height() of this and the parent element
  3. calculate proportion (this_height/parent_height*100)
  4. set width and height accordingly
  5. iterate over children, set child as root, start at 1.

What are common pitfalls that may likely happen? There are probably tags that don't have a width or height set, like svg:g. Then there are non-standard size definitions like in svg:circle or attributes you don't think about at first, like svg:texts dy. Is there a way to take an educated guess about the performance hit due to massive reflows? What else?

Thank you very much!

like image 953
prayerslayer Avatar asked Jul 16 '13 18:07

prayerslayer


1 Answers

Ok. So here is what I understand that you want to do in my own words.

Loop through all DOM elements converting absolute dimensions (i.e. height/width in terms of pixels) into relative dimensions (i.e. height/width in terms of percentages). This should be done with sensitivity as to whether the dimension is set using CSS or HTML attributes so as to be usable for SVG elements.

That being said, there is one major constraint: If you want to convert absolute dimensions to relative dimensions using <x> as the root element, then <x> must have absolute dimensions. Otherwise, the relative dimensions inside the container will change size based upon their contents instead of the overall size of the parent <x>.

With that stipulation, what you are asking is fairly easy. This is the algorithm that needs to be used.

  1. Start at <body>
  2. Give <body> fixed dimensions
  3. Walk DOM tree for <body> doing the following for each element
    1. Get the element's dimensions
    2. Get the parent's dimensions
    3. Convert to relative dimensions (i.e. width / parent width).
    4. Apply relative dimensions

To test my code I used the following HTML. It has both the code you include and an instance where a child element is larger than it's parent. This is one case I identified as a boundary condition to test.

<div style="width:100%;height:800px">
  <svg width="150" height="200" style="background: red;">
    <rect width="30" height="50" x="34" y="24"/>
  </svg>

  <div style="background: green; width: 50px; height: 50px;">
      <p style="background: teal; width: 100px; height: 20px;">Content</p>
  </div>
</div>

This is the JavaScript. It is fairly well commented and provides decent console output. For production code I would recommend recommend removing the console logging.

Please keep in mind that the code does not execute immediately, but on a 2 second delay. This way you can see the way the DOM looks before it gets modified.

For those who do not know the following code will return a if it is present and not false and b otherwise.

foo = a || b;

We could rewrite like one of the two below, but this is more succinct.

foo = a ? a : b;

if(a)
    foo = a
else
    foo = b

Here is a jsFiddle.

And the code!

/** convertToRelative
 * Convert absolute width/height to relative. Should work for
 * Both HTML and SVG objects. Tries to use CSS attributes, falls
 * back on HTML attributes.
 */
function convertToRelative(element) {
    // Get Element and Parent
    element = $(element);
    parent  = element.parent();

    // If there is no valid with, we need to use attributes
    var useAttr   = !element.width();

    // Get dimensions from css or attributes
    var width     = element.width()  || element.attr('width');
    var height    = element.height() || element.attr('height');
    var pWidth    = parent.width()   || parent.attr('width');
    var pHeight   = parent.height()  || parent.attr('height');

    // Find relative dimensions
    var relWidth  = (width  / pWidth)  * 100.0;
    var relHeight = (height / pHeight) * 100.0;

    // Log info
    console.log(element);
    console.log("1: "+ parent.height() +", 2: "+ parent.attr('height'));
    console.log("Width: "+ width +", Height: "+ height);
    console.log("PWidth: "+ pWidth +", pHeight: "+ pHeight);
    console.log("relWidth: "+ relWidth +", relHeight: "+ relHeight);

    // Clear current stylings
    element.removeAttr('width');
    element.removeAttr('height');

    // Apply relative dimensions
    if (useAttr) {
        element.attr('width', relWidth +'%');
        element.attr('height', relHeight +'%');
    } else {
        element.width(relWidth +"%");
        element.height(relHeight +"%");
    }
}

/** walk
 * Walk through a DOM element and all its sub elements running
 * the convertToRelative function to convert absolute positions
 * to relative positions.
 * DOES NOT CONVERT THE FIRST ELEMENT, ONLY CHILDREN.
 */
function walk(element) {
    $(element).children().each(function() {
        convertToRelative(this);
        walk(this);
    });
}

/** fixBody
 * In order to play with relative dimensions for children of the
 * body, we need to fix the dimensions of the body. Otherwise it
 * will resize as we begin to use relative dimensions.
 */
function fixBody() {
    $('body').css('width', $('body').width() +"px");
    $('body').css('height', $('body').height() +"px");
}

// Walk the body and all children on a 2 second delay.
setTimeout(function() {
    console.log('Running Conversion');
    fixBody()
    walk('body');
    console.log('Conversion Finished');
}, 2000);

Let me know if this is not what you wanted, or you run into a problem!

For your questions

What are common pitfalls that may likely happen?

The most common pitfall is going to be an incorrect conversion. Something will go wrong in the transformation and the resulting DOM will look nothing like the input.

There are probably tags that don't have a width or height set, like svg:g. Then there are non-standard size definitions like in svg:circle or attributes you don't think about at first, like svg:texts dy.

There are tags that do not have a width/height element. However, using jQuery should give us a bit more robustness in this department. I have not worked with jQuery and SVG much, but there is a plugin for support available if we run into any problems with the more obscure elements.

Is there a way to take an educated guess about the performance hit due to massive reflows?

I believe that this code will run in O(m * n) Where m = delay for a single reflow and n = number of nodes. The Reflows should have a fairly consistent change because we are converting the largest elements before the smallest. BUT I may be proven wrong on that last statement.

What else?

Success!

like image 166
Dan Grahn Avatar answered Oct 05 '22 06:10

Dan Grahn