Custom reorder table rows with jquery - not alphabetically

Hi there!

I’ve got a lot of tables displaying sizes, where the rows need sorting based on the size, e.g. X small row first, then small, medium, etc.

Not all tables have all the same sizes, and some sizes are formatted slightly differently, e.g ‘X small’ or ‘XSmall’ or ‘XS’.

The table has no classes or IDs.

The size text is the first word in the first td, first tr after thead.

Here is the link: http://site-1111.myshopify.com/products/angel-diamante-dog-t-shirt

The other thing that’s not too important, but I’d like to know if its possible to remove any text that starts with a certain string. Looking at the same link above, I would like to remove all the breed guidelines in the tables, so text that starts with ‘breed guidelines’.

I’m very much a js and jquery rookie so any help would be much appreciated!!! :slight_smile:

Thank you,

James

Hello everyone,

I know it’s quite an unusual request, but I’d love to know if this is even possible and if someone could steer me in the right direction so I can get started on implementing this.

I really hope someone can help :)))

Thanks very much in advance!

James

The reason why you may not be getting much help with this is that changes to the content are normally better achieved from the backend that’s serving the data instead.

To resolve your issue though, it seems that the most important piece of the puzzle is to come up with a function that accepts the sizing string, and returns an appropriate index number for how large it is.

So XS or XSmall or X small or x-small or x-s, or such variations would result in 0, large would be 4, and superdog 2 would be 8

I’ll take a look at what can be done with that in a few hours.

Paul that sounds great!

Thanks for replying, let me know how you get on :slight_smile:

James

Today ended up being busier than expected, but I’m back to things now.

Here’s a basic start, where we attempt to use a regex to capture common parts from the size string.


function getSizeParts(size) {
    var sizeRx = /(.*)(small|medium|large|super|s|m|l)(.*)/,
        match = sizeRx.exec(size.toLowerCase()),
        parts = {prefix: '', size: '', suffix: ''};

    if (match) {
        parts.prefix = match[1];
        parts.size = match[2];
        parts.suffix = match[3];
    }

    return parts;
}

function valueOfSize(size) {
    var parts = getSizeParts(size),
        size = 0;
    
    // magic happens here
    // ...
    size = parts.size;

    return size; // need to instead return numeric value of size
}

var sizesToTest = ['XS', 'XSmall', 'X small', 'x-small', 'x-s', 'medium', 'm', 'large', 'l', 'xl', 'xxl', 'super', 'superdog', 'super dog 1', 'superdog2'],
    i,
    size, value;
for (i = 0; i < sizesToTest.length; i += 1) {
    size = sizesToTest[i];
    value = valueOfSize(size);
    console.log('size: ', size, ', value: ', value);
}

Which mostly works, except for situations such as this:


size: X small, value: l

The reason why it’s showing the wrong size is due to regular expressions being greedy by default. The first (.*) capture group grabs the whole string, but the next capture group then has nothing, so the regex removes one character from the first capture group and looks for a match from the second capture group, which it finds with the ‘l’ character from the end of the word “small”.

That’s not how we want the regex to work. Instead of the regex being greedy, we want the first capture group to be lazy instead. We can do that by adding a question mark after the asterisk, so that we instead end up with (.*?)


var sizeRx = /(.*?)(small|medium|large|super|s|m|l)(.*)/,

Now we end up getting the right size for each test.
From here it’s just a matter of turning the size in to a numeric number for ordering, and of dealing with special situations.

We can use a simple switch statement to provide different size values:


function valueOfSize(size) {
    var parts = sizeParts(size),
        size = -1;

    switch (parts.size) {
    case 'small':
        // fall through
    case 's':
        size = 1;
        break;
    case 'medium':
        // fall through
    case 'm':
        size = 2;
        break;
    case 'large':
        // fall through
    case 'l':
        size = 3;
        break;
    case 'super':
        size = 6;
        break;
    default:
        // leave size at default value
    }

    return size;
}

Now we just need the prefix to affect the small and large ones, and the suffix to affect the super ones.

Which is done with:


var parts = sizeParts(size),
    prefixMatch = parts.prefix.match(/x/g),
    numOfXs = prefixMatch && prefixMatch.length || 0,
    suffixNumber = parseInt(parts.suffix.match(/\\d/), 10) || 0,
    size = -1;
...
case 's':
    size = 1 - Math.min(numOfXs, 1); // no more than one X for small
...
case 'l':
    size = 3 + Math.min(numOfXs, 2); // no more than two X's for large
...
case 'super':
    size = 6 + suffixNumber;

Which results in the following final code for working out the size of different sizing strings:


function sizeParts(size) {
    var sizeRx = /(.*?)(small|medium|large|super|s|m|l)(.*)/,
        match = sizeRx.exec(size.toLowerCase()),
        parts = {
            prefix: '',
            size: '',
            suffix: ''
        };

    if (match) {
        parts.prefix = match[1];
        parts.size = match[2];
        parts.suffix = match[3];
    }

    return parts;
}

function valueOfSize(size) {
    var parts = sizeParts(size),
        prefixMatch = parts.prefix.match(/x/g),
        numOfXs = prefixMatch && prefixMatch.length || 0,
        suffixNumber = parseInt(parts.suffix.match(/\\d/), 10) || 0,
        size = -1;

    switch (parts.size) {
    case 'small':
        // fall through
    case 's':
        size = 1 - Math.min(numOfXs, 1); // no more than one X for small
        break;
    case 'medium':
        // fall through
    case 'm':
        size = 2;
        break;
    case 'large':
        // fall through
    case 'l':
        size = 3 + Math.min(numOfXs, 2); // no more than two X's for large
        break;
    case 'super':
        size = 6 + suffixNumber;
        break;
    default:
        // leave size at default value
    }

    return size;
}

var sizesToTest = ['XS', 'XSmall', 'unknown', 'X small', 'x-small', 'x-s', 'small', 'medium', 'm', 'large', 'l', 'xl', 'xxl', 'super', 'superdog', 'super dog 1', 'superdog2'],
    i, size, value;
for (i = 0; i < sizesToTest.length; i += 1) {
    size = sizesToTest[i];
    value = valueOfSize(size);
    console.log('size: ', size, ', value: ', value);
}

The test code results in the following output:


size:  XS , value:  0
size:  XSmall , value:  0
size:  unknown , value:  -1
size:  X small , value:  0
size:  x-small , value:  0
size:  x-s , value:  0
size:  small , value:  1
size:  medium , value:  2
size:  m , value:  2
size:  large , value:  3
size:  l , value:  3
size:  xl , value:  4
size:  xxl , value:  5
size:  super , value:  6
size:  superdog , value:  6
size:  super dog 1 , value:  7
size:  superdog2 , value:  8

wow!! thanks so much paul, that’s amazing

I’m trying my best to understand it, thanks for explaining as you go through so I can actually try to grasp an understanding too.

Am I right in saying that this code will get the sizes to equal numbers, but it won’t actually reorder the table yet? Is there another step still to do…? Sorry for my (very) minimal understanding… :slight_smile:

sorry - you did explain what the code would output.
so would i then hide the original table… and be able to add styling to the output html, or insert it into a new table…?
thanks again

The original table can remain, for a simple way to reorder the table is to loop through those difference sizes in numerical order, from biggest to smallest, and move each row to the top of the table. That way as you work your way from the largest ones down to the smallest ones, the table ends up having the smallest ones placed on top of the larger ones below it.

So to achieve that, you would need to first gain a list of the different table rows, which can be most easily done by placing an id attribute on the table itself and using document.getElementsByTagName to get the rows.

After you have the rows, you need to get the first td value from each row, which can be done in a similar way.

So first, get the rows, then the sizes from those rows, then sort those rows based on the numeric value of the size from that row.

ok, the tables dont have IDs or classes though. can i get to the table another
way like #container table?

thanks

That’s nowhere near as effective. Is this not your page that you are using the scripting with?
Without a useful identifier on the table, the script will become more brittle and vulnerable to breaking when other future changes occur to the page.

It is my website. I have recently set up a company as a dropshipper - so I have access to all of the product data from the company who fulfil the orders.
I have copied across all of their sizing tables

Okay then, it shouldn’t be a problem then for you to add a suitable identifier to the table then, such as:


<table id="sizings">

I can do but the issue is I’ve got over 300 size charts so I was hoping to add some code just to the template page, rather than go into each product’s description and edit them individually

so although its not best practice, is it possible to use the container’s id? how would this be written? and can i add / edit size names to your code, e.g. ‘superdog’ as well as ‘super dog’.

then at least i can fix all the tables immediately, then go into each one individually later to fix them properly.

thanks!

If you want to restrict the table element to be from within the div that has a class of “container main-content” then that gets trickier when you’re needing the code to remain compatable with older web browsers.


var divs = document.getElementByTagName('div'),
    i,
    table;
for (i = 0; i < divs.length; i += 1) {
    if (divs[i].className === 'container main-content') {
        table = divs[i].getElementsByTagName('table')[0];
        break;
    }
}

That is complex though, and only works if the class names don’t change or get added to.

Instead of that, it is preferable to use the querySelector that most modern web browsers now support:


var table = document.querySelector('.container.main-content table');

You can still use querySelector in browsers that don’t support that feature, such as IE7, but you will also want to use this queryselector polyfill to add that missing querySelector functionality to web browsers that need it.

Another alternative without an identifier, assuming that it’s the first or only table on the web page, is to use getElementsByTagName to get the table itself:


var table = document.getElementsByTagName('table')[0];

but be wary of using the above, for it’s guaranteed to break if any table appears on the web page before the one that you’re wanting to target.

With an identifier it would as simple as using the getElementById method:


var table = document.getElementById('sizings');

That’s why using a consistent identifier allows you to very easily target the sizings table from the script.

You will see in the test code that I used before, that both of those situations result in the correct behaviour. It can handle superdog with and without spaces, and with a number suffix too.

paul you’ve been an incredible help!
i feel like i owe you something! :slight_smile:

the table is the only one on the page, so to ensure the code works in IE7 do you recommend using this

var table = document.getElementsByTagName('table')[0];

so if i just add all of the code from before, then … aaagh sorry i feel like an idiot but can you just help me connect the two so i can get it all working. otherwise i know i would stay up all night trying to figure out the last bit (i know its as good as complete in your mind lol)

thanks again!!

There’s a hell of a lot more that’s to be done yet in terms of getting the rows, gathering up the sizes, working out what order they should be in, and actually ordering the rows of the table.
If you don’t know how to write code and want someone else to do the work for you instead, then my time that I volunteer here is too limited to allow me to do all of that work for you.

If on the other hand you can put together an attempt at solving those problems - when you have any technical issues with them we can help to educate you about such things.

as i said im a js newbie, especially as it gets more complicated.
not asking anyone to do all the work for me… i’ll give it the best shot i can

Actually, I should be able to take the time to go through this, due to the rest of it being made fairly simple by combining some built-in techniques.

Starting with the table content, we want to get the tbody rows and sort them.


var tbody = document.querySelector('.main-content tbody'),
    rows = tbody.getElementsByTagName('tr');

sortRows(rows, compareSizes);

In fact, that can be made even simpler, by using querySelectorAll to get the rows themself.


var rows = document.querySelectorAll('.main-content tbody tr');

sortRows(rows, compareSizes);

There are a number of different ways to sort things but in this case, given that they are an HTML collection, the easiest way to sort them is to create an array from those HTML elements by using the Array’s slice method to convert non-array items in to an array. That way the array sort method can be used to sort the rows in that array.

Then, we can then loop through the sorted array and move each row to the bottom. The sortRows function that results in the rows all being nicely sorted, is:


function sortRows(rows, compare) {
    var rowsArr = Array.prototype.slice.call(rows, 0);

    rowsArr.sort(compare);
    rowsArr.forEach(function (row) {
        row.parentNode.appendChild(row);
    });
}

If you also need to support web browsers that don’t know about the forEach array method, you can add support for that with the following code:


// Array.forEach polyfill, from https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
if ( !Array.prototype.forEach ) {
  Array.prototype.forEach = function(fn, scope) {
    for(var i = 0, len = this.length; i < len; ++i) {
      fn.call(scope, this[i], i, this);
    }
  }
}

The sort method that arrays have is what we’ll use to sort the rows, so that compareSizes function that we give to the sort method accepts two items being compared, and returns either -1/0/+1 or false/true to indicate which one is larger than the other.


function compareSizes(a, b) {
    var aSize = a.querySelector('td').innerHTML,
        bSize = b.querySelector('td').innerHTML;

    return valueOfSize(aSize) > valueOfSize(bSize);
}

And that’s all that you should need.

Keeping things nice and easy, we can create a globally accessible function called tableSizes, which accepts a selector for where to get the rows to be sorted.
It could be called as:


window.tableSizes.sort('.main-content');

Or if you’re feeling lazy, as just:


tableSizes.sort('.main-content');

A nice way to create that is with:


window.tableSizes = (function () {
    'use strict';

    // functions and code in here
    // ...

    return {
        sort: function (sourceSelector) {
            var rows = document.querySelector(sourceSelector + ' tbody tr');

            sortRows(rows, compareSizes);
        }
    };
}());

Some example code for the above can be seen at http://jsfiddle.net/pmw57/yZrcr/

To help demonstrate things in action, I’ve added some code to randomize things too, to http://jsfiddle.net/pmw57/yZrcr/2/

hi paul - thanks so much for doing this, it all looks great once its come together

ive just copied and pasted the jsfiddle code onto a page that i can test it on, then make it match the IDs and classes on my page, and tweak anything, etc, but surprisingly even a straight copy / paste doesn’t work.

ive checked there are no ID or class conflicts (just had to change the class ‘main-content’), but not sure why its running on jsfiddle but not on my page. perhaps a jquery conflict i dont know… really sorry i cant figure it out - i have been trying and ill keep looking. here is the link http://site-1111.myshopify.com/pages/new