March 25, 2009

Cross-browser inline style with jQuery

Currently I'm developing some web applications for internal use, porting from some old ones.

I'd love jQuery, the jQuery way to do asynchronous requests is brilliant. Then I developed a plugin for my apps. Basically, it "automagically" catches all clicks on links, sends an AJAH request and loads the response inside a container.

The problem came when the response contains <STYLE> or <LINK> tags. While using Firefox and Opera all was great. But, oh $h*t, no style was applied with IE6, IE7, Chrome or Safari. I was thinking that was my plugin's fault, but when I test the use case without it, the problem remains. No style applied.

Case test

Check this simplified case without even jQuery:

<body>
  <p id="test">This test should have border and padding!</p>
  <script type="text/javascript">
    var div = document.createElement('div');
    // <br/> added to avoid a bug in IE
    div.innerHTML = '<br /><style>#test { border:2px solid #000; padding:1ex; }</style>';
    var span = div.getElementsByTagName('span')[0];
    document.body.appendChild(div);
  </script>
</body>

I thought that jQuery have workarounds for this, but I found none, so I created a plugin to do the hard work.

Maybe I missed some jQuery functionality, I'm not a guru.

jQuery cross-browser inline style plugin

This plugin relies on detecting whether inlined styles are processed or not. If not, modify $.fn.html to move all style's and link's to document.head, mark them with a 'data-style-loader' attribute and adding a dummy link to the target. When te dummy link is removed (with the same $.fn.html function), it removes all related style's and link's from head.

Well, that's the code:

$(function($) {
  // Checks for some supported features
  (function() {
    var div = document.createElement('div');
    div.id = 'jquery-support-styled';
    document.body.appendChild(div);

    // Checks whether inlined styles applies automaticly
    // (Firefox and Opera returns true)
    div.innerHTML = '<style>#'+div.id+
        ' span { display:block; width:3px; }</style><span />';
    var span = div.getElementsByTagName('span')[0];
    $.support.inlineStyleApplies = $(span).width() == 3;

    // Checks whether inlined styles applies if next to a 'br' (for IE)
    $.support.mustPrependBrToInlineStyles =
      !$.support.inlineStyleApplies && (function(){
        div.innerHTML = '<br /><style>#'+div.id+
          ' span { display:block; width:5px; }</style><span />';
        var span = div.getElementsByTagName('span')[0];
        return ($.support.inlineStyleApplies = $(span).width() == 5);
      })();

    document.body.removeChild(div);
  })();

  // Saves old $().html function
  var old_html = $.fn.html;

  if($.support.mustPrependBrToInlineStyles) {
    // Modify old $().html to add br before styles and links
    $.fn.html = function(_value) {
      if(typeof _value !== 'string') {
        _value = _value.replace(/<style|<link/gi, function(_text) {
          return '<br style="display:none"/>'+_text
        })
      }
      return old_html.call(this, _value);
    }

  } else if(!$.support.inlineStyleApplies) {
    // Change old $().html to move link´s and style´s to the head
    $.fn.html = function(_text) {
      // Remove css-pointers and their pointees
      var $styles = this.find('link[data-style-loader]');
      if($styles.length) { clean_css($styles); }

      // Calls old $().html
      old_html.call(this, _text);

      // Promote new links and styles to document.head
      $styles = this.find('style,link[rel*=stylesheet]');
      if($styles.length) { add_styles.call(this, $styles) }
      return this;
    }

    // Remove css-pointers and their pointees
    function        clean_css(_$styles) {
      // Get pointees id
      var st = [];
      for(var i = 0; i < _$styles.length; ++i) {
        st[st.length] = '[data-style-loader='+
          $(_$styles[i]).attr('data-style-loader')+']'
      }
      // Remove all links pointed
      $('head').find(st.join(', ')).remove();
    }

    // Promote new links and styles to document.head
    function        add_styles(_$styles) {
      // Selects a pointer id
      var tm = (new Date).getTime();
      // Move to head and add attribute with pointer id
      _$styles.attr('data-style-loader', tm).appendTo('head')
      // Create a link inside the target with info to the new links and styles
      this.append('<link data-style-loader='+tm+' />')
    }
  }
})

Tests and bugs

I have tested the plugin with these cases:

  1. <p>Default, no style</p>
  2. <style type="text/css">#test { color:red; }</style>
  3. <link rel="stylesheet" href="remote.css" /> and remote.css #test { color:red; }
  4. <style type="text/css">@import url("remote.css")</style>
  5. <link rel="stylesheet" href="import.css" /> and import.css @import url("remote.css")

The plugin worked GREAT! But... with some exceptions:

  • In IE6 and IE7, case 4 fails silently.

    Surprisingly, IE5 is no problem. (WTF?)

  • In Safari and Chrome, the style isn't removed when loading 1) after 3) or 5).

    Loading 1) after 2) or 4) works ok. WTF again??

Well, this browser war is almost won...

But... HOW can I lead my JS to the total victory???

2009-12-03 Still unsolved