4

I have a problem. I have a bunch webpage that makes heavy use of multiple css classes.

<div class="class1 class2 class3">foo</div>

Unfortunately, I have a "browser" (for lack of a better term) that can not handle multiple css classes in that manner.

I can identify all the elements with multiple classes but now I need to create new classes that merge them. First attempt was to inline all the styles into the style attribute, however that was far too slow, and bloated the document needlessly.

What I now want to do is find an element with multiple classes. Create a new class which is a combination, and replace the elements class with the newly created one, as well as any other elements with the same class combination.

Any thoughts on how best to approach this.

Community
  • 1
  • 1
CaffGeek
  • 21,856
  • 17
  • 100
  • 184
  • 1
    And why are you doing this on the client and not the server?? – epascarello Oct 04 '11 at 20:03
  • 5
    Do you expect that "browser" to handle jQuery if it cannot do such elementary styling? – Lekensteyn Oct 04 '11 at 20:04
  • @epascarello, because the page has been initially rendered in a real browser, and then heavily modified on the client. The issue is in an export to another "browser" on the client. It can't be done on the server. I just need to modify the classes in the html prior to the export. – CaffGeek Oct 04 '11 at 20:08
  • @Lekensteyn, yes because the browser executing the jQuery is a real browser that passes it's html to another "browser". Unfortunately, that second browser can't handle multiple classes. – CaffGeek Oct 04 '11 at 20:08
  • 1
    Really? A downvote? Why? This question meets all the guidelines, and isn't a dupe. – CaffGeek Oct 04 '11 at 20:09
  • @Lekensteyn, and IE6 would have this specific issue where it would run jQuery but not support multiple classnames. This isn't my situation, but it's not that far fetched. – CaffGeek Oct 04 '11 at 20:16
  • IE6 does support multiple class names on an object. – jfriend00 Oct 04 '11 at 21:07
  • You should mention in the question that the target "browser" you are targetting is Excel and that you are using IE7/8 as an intermediary step. (according to your comments on @jfriend00's answer) – Davy8 Oct 04 '11 at 21:14
  • @jfriend00, good point, my memory of IE6 and multiple class support was fuzzy. But, it wasn't my situation anyhow. – CaffGeek Oct 04 '11 at 21:21
  • @Davy8, does the target "browser" really matter? It's one that doesn't support multiple classnames. What the browser actually is changes nothing. – CaffGeek Oct 04 '11 at 21:22

3 Answers3

2

Loop through all tags. Split the class names into an array. Sort it to get it into a predictable order. Join the string back together.

$(document).ready(function() {
    var classList = {};
    $("*").each(function() {
        var temp;
        if (this.className) {
            temp = this.className.split(" "); // split into array
            temp.sort();                      // put in predictable order
            this.className = temp.join("");   // put class name back without spaces
            classList[this.className] = true; // add to list
        }
    });
    // classList object contains full list of all classNames used
});

FYI, it seems really odd that you'd have a browser that supports jQuery, but doesn't support CSS styles for multiple class names. You do realize that you are going to have to supply completely different stylesheets that work off the concatenated names, right? And, if you can change the stylesheets, it makes me wonder why you can't change the HTML.

Working implementation: http://jsfiddle.net/jfriend00/uPET7/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I want to create the new style sheets on the fly too. And IE6 would have this issue. This isn't my situation, but it isn't that strange where this could happen. – CaffGeek Oct 04 '11 at 20:15
  • My understanding of IE6 is different than yours. It will handle multiple class names on an object. I won't handle multiple class names in the style sheet. It has problems with this: `p.red.blue { color: magenta; }`, but not with `class="first second"`. Are you sure you're trying to solve the right problem? – jfriend00 Oct 04 '11 at 20:30
  • ah, true, but yes, I am trying to solve the right problem, ie6 isn't my issue. I know exactly what the issue is and reducing to one class in the element solves it. – CaffGeek Oct 04 '11 at 20:33
  • it gets me half way. But I need to create the new stylesheet on the fly as well. – CaffGeek Oct 04 '11 at 20:45
  • Your question doesn't make it clear that you want code to merge style sheets too. What browser is this for? – jfriend00 Oct 04 '11 at 20:48
  • The generating code will be run in IE7 or IE8. The target "browser" that only supports single class names is excel. – CaffGeek Oct 04 '11 at 20:56
1

Summary: This function returns an ordered list of all duplicate class names, which can easily be used to merge classes.

To start off, get a useful list of duplicates:

var multi = {};

$("*[class]").each(function(){
    var class = this.className.replace(/^\s+|\s+$/g,"").replace(/\s+/g,".");
    if(!/\./.test(class)) return; //Ignore single classes
    if(multi[class]){
        multi[class]++;
    } else {
        multi[class] = 1;
    }
});

//Now, merge duplicates, because .class1.class2 == .class2.class1
var multi_nodup = {};
for(var classes in multi){
    var a_classes = classes.split(".");
    var a_classes = a_classes.sort();
    var a_classes = a_classes.join(".");
    if(multi_nodup[a_classes]){
        multi_nodup[a_classes] += multi[classes];
    } else {
        multi_nodup[a_classes] = multi[classes]
    }
}
//Now, multi_npdup is a map of all duplicate classnames

var array_multi = [];
for(var classes in multi_nodup){
    array_multi.push([multi_nodup[classes], classes]);
}
array_multi.sort(function(x,y){return y[0]-x[0]});
//array_multi is an array which looks like [["class1.class2.class2", 33],
//             ["class3.class4", 30], ...]
// = A list, consisting of multiple class names, where multiple classnames
// are shown, together with the nuber of occurences, sorted according to
// the frequence

Execute my function, and output variable array_multi. This will show you a map of multiple class names, so that you can replace multiple classnames, accordingly.

Because of the special way I stored the class names, you can use $("." + array_multi[n][0]) to access all elements which have a set of classname which equals to the set as described at the nth position in array_multi.

Example of readable output:

//Overwrites current document!
var list = "";
for(var i=0; i<array_multi.length; i++) list += array_multi[i][0] + "\t" + array_multi[i][1];
document.open();
document.write("<pre>"+list+"</pre>")
document.close();

Automatic conversion

A way to automate the merging of the classnames i by adding all separate class properties to a JavaScript string, and add it to an object. This is the most reliable way to get the exact CSS properties, because attempting to get the classnames through the document.styleSheets object can produce slightly different results. Example:

var classStyle = {};
classStyle["class1"] = "border:1px solid #000;";
classStyle["class2"] = "color:red";

//Make sure that each declaration ends with a semicolon:
for(var i in classStyle) if(!/;$/.test(classStyle[i])) classStyle[i] += ";";

//Initialise
var all_styles = {};
for(var i=0; i<array_multi.length; i++){
    all_styles[array_multi[i][1]] = "";
}

//This loop takes definition precedence into account
for(var currentCName in classStyle){
    var currentClass = new RegExp("(?:^|\\.)" + currentCName + "(?:\\.|$)");

    // Rare occasion of failure: url("data:image/png,base64;....")
    var separateProps = classStyle[currentCName].split(";");
    var prop_RE = {};
    for(var p=0; p<separateProps.length; p++){
        var cssProperty = separateProps[p];
        if(!/:/.test(cssProperty)) continue; //Invalid CSS property
        prop_RE[cssProperty] = new RegExp("(^|;)\\s*" + cssProperty.match(/(\S+)\s*:/gi)[1] + "\\s*:[^;]+;?", "gi");
    }

    for(var class in all_styles){
        if(currentClass.test(class)){
            for(var k in prop_RE){
                all_styles[class] = all_styles[class].replace(prop_RE[k],"$1") + k;
            }
        }
    }
}

//To finish off:
var allClassesToString = "";
for(var class in all_styles){
    var newClass = class.replace(/\./g, "_");
    $("."+class).each(function(){
        this.className = newClass;
    });
    allClassesToString += "."+newClass + "{" + all_styles[class] + "}\n";
}

// allClassesToString <------- This variable now holds a string of all duplicate CSS classes!
//Example:
var style = $("<style>");
style.text(allClassesToString);
style.appendTo($("head:first"));
Rob W
  • 341,306
  • 83
  • 791
  • 678
0

Does not seem to crazy to accomplish this,

Loop through every element that has more than 1 class. Sort the classes (doesn't matter how as long as it is consistent) then merge them together to create the new class. Keep a list of all new css classes and check against them in case of duplicates.

To get all the styles from an element see here

Community
  • 1
  • 1
Andrew
  • 13,757
  • 13
  • 66
  • 84