Wednesday, October 11, 2017

Collapsible columns in circulation rules

This is the jQuery and CSS for the collapsible columns in the circulation fines and fees columns in Koha.

I used these three chunks of jQuery with the attendent CSS as examples of how to use jQuery in Koha at the 2017 Koha-US conference in Coeur d'Alene, Idaho, during the hack-fest sessions.  I decided that these three pieces of code were pretty good ones to look at when you're learning jQuery because each one gets more complex as you work through understanding them and adding them to Koha.

To use this code, cut and paste the jQuery between the two lines here into the IntranetUserJS system preference - between the $(document).ready(function() { and the closing }); brackets..

*************************

//Administration
  //Administration › Circulation and fine rules

    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

    //BEGIN Create button to hide left hand navigation bar
      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

*************************

This is the accompanying CSS for the third segment of the above jQuery.  To use it, cut and paste the CSS between the two lines of dashes into the IntranetUserCSS system preference.

-------------------------

/* -Administration › Circulation and fine rules- hides columns in circulation rules (requires accompanying jQuery) */
th.hiderule, td.hiderule {
    max-width: 10px;
    text-indent: -9999px;
}

-------------------------

Now here's an explanation of what each piece of this jQuery does:

  //Administration › Circulation and fine rules
    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

Anything you put in jQuery that comes after a // or between a /* and a */ is considered a comment, so the first thing you see here is a set of comments that I use at the beginning of each bit of jQuery I add to Koha.  I have my IntranetUserJS system preference organized alphabetically by Koha breadcrumbs so that when I'm trying to find something I need to modify or test, I can find it more easily.  The second line describes what the actual jQuery does in Koha.

The "//BEGIN" part of this is something I started doing early on when I started adding jQuery to Koha so I could tell what it was I was doing and where the code started and where it ended.  The breadcrumbs are something I added when I started working for NExpress because when I started here, I inherited jQuery that other staff had written over an 8 year period and almost none of it had comments of any kind.  When we were upgrading to Koha 16.11 at the beginning of 2017 there was a piece of code that no longer worked - in fact it broke something in 16.11 and I spent quite a bit of time trying to figure out which thing caused the problem.  By organizing everything based on the breadcrumbs of the page that the jQuery affected, I was able to set myself up for an easier time testing during the next upgrade.
----------

    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

Each piece of jQuery begins with a dollar sign and ends with a semicolon.  In this specific statement you will notice that there are two dollar signs and two semicolons.  That's because there is one statemnt nested inside another statement.  These indicate where the jQuery starts and where it ends.
----------

    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

Next is a set of parentheses with some kind of selector usually inside of quotation marks.  The parentheses and the quotes identify what element on the page is going to be affected by this piece of jQuery.
----------

   //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

The thing that is inside of the quotes and parenthesis is the actual selector itself.  The selectors in this case tells jQuery to look for a DIV element on the page that has the ID "default-circulation-rules" and when it finds that to look for a "tr" (table row) that contains the word "edit".

If you look at W3 schools page at https://www.w3schools.com/jquery/jquery_ref_selectors.asp, they have a good list of html selectors.  A lot of coders knock W3 schools - and rightly so (please don't pay for one of their certificate of completion scams).  This is, however, is the most concise list of jQuery selectors I've found online.
----------

    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

The next thing you see is a dot.  The dot essentially says "now that we have found what we were looking for, everything that comes next tells us what to do to that thing we just found."
----------

     //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

In this specific case, the thing that comes after the dot is a "click(function(){});" action.  This action is telling Koha that whenever it finds a table that contains a row that has the word "Edit" in it and that table is inside a DIV that has an ID of "default-circulation-rules", Koha is going to do something to that row when we click on it.
----------

    //BEGIN Click on circ rule to move it to the bottom of the table
      $("#default-circulation-rules tr:contains(Edit)").click(function() {
        $(this).insertBefore("#edit_row");
      });

And here inside of those brackets in the function part of the click action is another piece of jQuery with a beginning, a new selector, and a new action.  In this specific case this jQuery is saying "Now that we've found a row with the word "Edit" in it in a table with a DIV called "default-circulation-rules" and someone clicks on that row, we're going to take "this" row and move it so that it is just above this other thing on the page that has an ID called "edit_row."

The end result is that when you click on one of the circulation rules in the "Defining circulation and fine rules for all libraries" table, jQuery is going to move the row you've clicked on to the bottom of that table, just above the editing row.

I find that moving a row, or several rows, to the bottom of the table makes it easier for my eyes to track those rows across the table.
=====

The first one was pretty easy, so let's look at the next one which is slightly more complex.

    //BEGIN Create button to hide left hand navigation bar
      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

A lot of the parts here are the same - the dollar signs, the semicolons, the parenthesis, the quotes, and the dots.  In this case, though, this group contains two statements.

The first line

      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

is creating the button to hide the left hand navigation bar.  It's telling Koha that every time it sees a DIV called "navmenu" inside of the DIV called "admin_smart-rules" to find that DIV's parent (this is what the .parent() action does) and then to stick some other piece of HTML (i.e. "prepend") in front of that parent DIV.  In this case the HTML that Koha will prepend in front of the navmenu is a button called "navhide" and the text that will appear on that button is "Hide admin column."

That's all that this one line does is create a button and sticks it at the top of the left hand navigation column on the page with the breadcrumbs "Administration › Circulation and fine rules."

It's the next section that actually tells Koha what to do when someone clicks on the button.
----------

      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

The first line is a lot like the first line in the earlier jQuery we looked at - it is telling Koha that every time the new button is clicked, do some different things.
----------

      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

The next three lines actually tell Koha the three things jQuery is supposed to do when the button is clicked.
----------

      $("#admin_smart-rules #navmenu").parent().prepend("<button id='navhide' type='button'>Hide admin column</button>");
      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

The first jQuery says, essentially, "now that you've clicked on this new button you just created, make that very same button disappear."
----------

      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em");
      });

The second jQuery says, "while you're at it, make that whole column of stuff underneath that button disappear too."
----------

      $("#navhide").click( function() {
        $("#navhide").hide();
        $("#navmenu").hide();
        $(".yui-t1 #yui-main .yui-b").css("margin-left", "1em"); 
      });

And the third line says, "now that we've made all of that stuff on the left hand side of the screen disapper, let's change the margins on this page so that the rules table can move to the left side of the screen."

Since the "Defining circulation and fine rules for all libraries" table is 7 miles wide, clearing the left hand side of the screen helps make that table easier to scroll that table by making some more room on the screen.
=====

Finally, there is this bigger piece that requires some CSS also:

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

The first line here does kind of the same thing that the "prepend" part of the last example just instead of putting something before something else, it's adding something after the th elements in the rules table.  Once this is in place, it's going to add the words

Hide
Column

below the descripton of each column in the rules table.
----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
       });

And this part should look farmilliar now too.  It's telling Koha that when you click on one of those columns, to do something.  But what it's telling Koha to do is much more complex this time.
----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

This line is telling Koha that when the column head is clicked to create a Java variable called "index" and that the value of this variable is the value of the "index number" of that cell plus one.  Essentially Koha is going to ask the question "Counting from left to right, what's the number of the cell in the table head that was clicked on?" and then it's adding one to that number.  (The + 1 will be explained in a just a moment.)
----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
       });

This tells Koha to go through the table and look at all of the rows and to find the "nth-child" (i.e. cell number X in that row) where the cell number is the same as the "index" number we just got in the last line of this piece of code.  The reason that we don't add the "+1" here, though is that while "index" starts counting at 0, the "nth-child" selector starts counting at 1.  (Why does "index" start counting at 0 and nth-child start counting at 1?  Who knows?  Knowing why is probably not as important as knowing how to deal with the situation.)
----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

This line says, now that we've got got this "cells" variable figured out, we're going to add or remove a "class" element to each cell that is in that column and we're going to call that class "hiderule."  If the cell already has this class, the toggle will remove it.  If the cell doesn't have this class, the toggle will add it.  This is also where the CSS that corresponds with this whole set of commands comes into play.
----------

The corresponding CSS for this process is:

/* -Administration › Circulation and fine rules- hides columns in circulation rules (requires accompanying jQuery) */
th.hiderule, td.hiderule {
    max-width: 10px;
    text-indent: -9999px;
}

and this CSS is telling Koha that when an item has this "hiderule" class added to it to change the width to 10 pixels wide and to take the contents of the cell and move them 9999 pixels to the left.  Since 9999 pixels to the left is about 4 feet to the left of your monitor, adding this class to one of these columns in the circulation rules quickly renders that column invisible for all intents and purposes.
----------

    //BEGIN Hide unneeded columns in circulation rules by clicking on header (--requires corresponding css--)
      $("#default-circulation-rules thead th").append("<br /><br /><span>Hide<br />Column</span>");
      $('#default-circulation-rules thead th').click(function() {
        var index = (this.cellIndex + 1);
        var cells = $('#default-circulation-rules tr > :nth-child(' + index + ')');
        cells.toggleClass('hiderule');
        if ($(this).hasClass('hiderule')) {
          $(this).find('span').html('+');
        }
        else {
          $(this).find('span').html('Hide<br />Column');
        }
      });

The final section is a simple if-then statement.  It's saying, if the selector that this function is attached to (in this case the column header) has the "hiderule" class, to replace the "Hide Column" text with a "+" sign, but if it doesn't have that class, go ahead and keep the "Hide Column" in that space.
----------

The end result is that when you look at your circulation rules, the text "Hide Column" or a "+" sign will be added to the header row and when you click on that cell in the header, that column will collapse or expand as needed.

Between adding the ability to move the rows to the bottom of the table, the ability to make the navigation menu on the left disappear, and the ability to collapse the columns you don't need, all three of these pieces of jQuery and their corresponding piece of CSS work together to make the really wide "Defining circulation and fine rules for all libraries" table easier to manage.