Friday, 19 August 2016

DataTables Search for, and order by, select value

There was a cracking question on stackoverflow the day before yesterday and it got me scratching my head.

It was to do with filtering and sorting rows when there was a select input in the row and the selected option was what was being searched or ordered by. DelightedD0D had the ordering down pat but couldn't do the search. I spent far too long thinking about an answer but my solution broke the universal search within DataTables themselves and I wanted to just add to the search not replace it. This was my first attempt:

$.fn.dataTable.ext.search.push(
    function(settings, data, dataIndex) {
        var dataLabel = table
            .row(dataIndex) //get the row to evaluate    
            .nodes()        //extract the HTML - node() does not support to$     
            .to$()          //get as jQuery object 
            .find('select') //find column with the select input
            .val();         //get the value of the select input
        // return true or false if the val matches the search parameter
        return !!~dataLabel.toLowerCase().indexOf(table.search().toLowerCase()); 
    }     
);

I felt sure that there must be a better way fo doing it though and finally I've found it, it depends upon the column having a type of "selected" so that given the following table markup:

<table id="example">
    <thead>
        <tr>
            <th>
                First name
            </th>
            <th>
                Last name
            </th>
            <th>
                Position
            </th>
            <th>
                Office
            </th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                Tiger
            </td>
            <td>
                Nixon
            </td>
            <td>
                <select>
                    <option 
                      value="">
                        Please choose
                    </option>
                    <option 
                      value="System Architect" 
                      selected="selected">
                        System Architect
                    </option>
                    <option 
                      value="Accountant">
                        Accountant
                    </option>
                    <option 
                      value="Senior Javascript Developer">
                        Senior Javascript Developer
                    </option>
                    <option 
                      value="Junior Technical Author">
                        Junior Technical Author
                    </option>
                </select>
            </td>
            <td>
                Edinburgh
            </td>
        </tr>
        <!-- more rows here -->
    </tbody>
</table>

This code will allow for ordering and filtering on the value of the select input:

(function() {
    $.fn.dataTable.ext.type.search.selected = (data) => !$(data).is("select") 
     ? '' 
        : $(data).val();
    $.fn.dataTable.ext.order['dom-select'] = function(settings, col) {
        return this.api().column(col, {
            order: 'index'
        }).nodes().map(td => $('select', td).val());
    }
})();
var table = $('#example').DataTable({
    "columnDefs": [{
        "orderDataType": "dom-select",
        "type": "selected",
        "targets": 2
    }]
});
$("#example select").on("change", function() {
    var $this = $(this),
        val = $this.val(),
        cellPosition = table.cell($this.parents("td")).index(),
        rowDate = table.row(cellPosition.row).data();
    $this.find("option").each((k, v) => ($(v).val() === val) 
     ? $(v).attr("selected", "selected") 
        : $(v).removeAttr("selected"));
    rowDate[cellPosition.column] = $this.prop("outerHTML");
    table.row(cellPosition.row).data(rowDate).draw();
});

The special stuff is in the change event of the select input as it updates the data for the row and then redraws the table, allowing the search extension to know that the table has changed.

Wednesday, 10 August 2016

Bootstrap DataTables and Modal Dialog Forms (CRUD)

So I've got this HTML snippet to generate the table above:

<div 
  class="container">
    <table 
      id="actionTabDataTable" 
      class="table table-striped table-bordered" 
      cellspacing="0" 
      width="100%"></table>
</div>
<!-- Modal -->
<div 
  class="modal fade" 
  id="myModal" 
  tabindex="-1" 
  role="dialog" 
  aria-labelledby="myModalLabel">
    <div 
      class="modal-dialog" 
      role="document">
        <div 
          class="modal-content">
            <div 
              class="modal-header">
                <button 
                  type="button" 
                  class="close" 
                  data-dismiss="modal" 
                  aria-label="Close">
                    <span 
                      aria-hidden="true">
                        &times;
                    </span>
                </button>
                <h4 
                  class="modal-title" 
                  id="myModalLabel">
                    Please tell us about the job 
                    <span 
                      id="name"></span> 
                    has:
                </h4>
            </div>
            <div 
              class="modal-body">
                <form>
                    <div 
                      class="form-group">
                        <label 
                          for="job">
                            Job title
                        </label>
                        <input 
                          type="text" 
                          class="form-control" 
                          id="job" 
                          name="job" />
                    </div>
                </form>
            </div>
            <div 
              class="modal-footer">
                <button 
                  type="button" 
                  class="btn btn-default" 
                  data-dismiss="modal">
                    Close
                </button>
                <button 
                  type="button" 
                  class="btn btn-primary">
                    Add
                </button>
            </div>
        </div>
    </div>
</div>

And I want to be able to interact with the underlying data which is an array of objects generated from a form further back in the mists of time... or at least earlier in the process anyway. So I use the following snippet of JavaScript:

var data = [{
    "name": "John Smith",
    "jobs": [
        "Bottle Washer",
        "Bus Boy"
    ]
}, {
    "name": "Jane Smith",
    "jobs": [
        "Head Chef",
        "Barmaid"
    ]
}, {
    "name": "Barry Smith"
}];
$(function(){
    var table = $("#actionTabDataTable").DataTable({
        "data": data,
        "columns": [{
            "title": "Name",
            "data": "name"
        }, {
            "title": "Jobs",
            "orderable": false,
            "data": "jobs",
            "render": function(d) {
                if (d) {
                    return $("<ul></ul>", {
                        "class": "list-group"
                    }).append(function() {
                        var lis = [];
                        for (var i = 0; i < d.length; i++) {
                            lis.push($("<li></li>", {
                                "text": d[i],
                                "class": "list-group-item"
                            }).append($("<i></i>", {
                                "class": "glyphicon glyphicon-remove"
                            })).append($("<i></i>", {
                                "class": "glyphicon glyphicon-edit",
                                "data-toggle": "modal",
                                "data-target": "#myModal"
                            })));
                        }
                        return lis;
                    }).prop("outerHTML");
                } else {
                    return "No jobs";
                }
            }
        }, {
            "title": "Action",
            "orderable": false,
            "render": function() {
                return $("<button></button>", {
                    "class": "btn btn-primary",
                    "text": "Add",
                    "data-toggle": "modal",
                    "data-target": "#myModal"
                }).append($("<i></i>", {
                    "class": "glyphicon glyphicon-plus"
                })).prop("outerHTML");
            }
        }]
    });

    $("#actionTabDataTable tbody").on("click", ".glyphicon-remove", function() {
        var d = table.row($(this).parents("tr")).data();
        var job = $(this).parents("li").text();
        $.each(data, function(k, v) {
            if (v.name === d.name) {
                console.log(v.jobs.length);
                for (var i = 0; i < v.jobs.length; i++) {
                    if (v.jobs[i] === job) {
                        v.jobs.splice(i, 1);
                        !v.jobs.length && delete v.jobs;
                        break;
                    }
                }
            }
        });
        table.clear().rows.add(data).draw();
    }).on("click", ".glyphicon-edit", function() {
        var d = table.row($(this).parents("tr")).data();
        var job = $(this).parents("li").text();
        $("#name").text(d.name);
        $("#job").val(job);
        $("#myModal").data({
            "original": d,
            "job": job
        }).find(".btn-primary").text("Update");
    }).on("click", ".btn-primary", function() {
        var d = table.row($(this).parents("tr")).data();
        $("#myModal").data("original", d);
        $("#name").text(d.name);
    });
    $("#myModal").on("click", ".btn-primary", function() {
        var d = $("#myModal").data("original");
        var j = $("#myModal").data("job");
        $.each(data, function(k, v) {
            if (v.name === d.name) {
                if ($("#myModal").find(".btn-primary").text() === "Update") {
                    $.each(v.jobs, function(a, b) {
                        if (b === j) {
                            v.jobs[a] = $("#job").val();
                        }
                    });
                } else {
                    if (v.hasOwnProperty("jobs") && Array.isArray(v.jobs)) {
                        v.jobs.push($("#job").val());
                    } else {
                        v.jobs = [$("#job").val()];
                    }
                }
            }
        });
        table.clear().rows.add(data).draw();
        $("#myModal").modal("hide");
    }).on("hidden.bs.modal", function() {
        $("#job").val("");
        $("#myModal")
         .removeData("original")
            .removeData("job")
            .find(".btn-primary")
             .text("Add");
    });
});

Basically this allows us to interact with the underlying data used to generate the table without having to worry about rendering the data again. It's up and running here.

I've had to tweak the CSS a little as well, here it is:

.list-group {
    text-align: left;
    margin-bottom:0;
}
.glyphicon {
    padding: 0 0 0 10px;
    cursor: pointer;
    float:right;
}

Monday, 18 July 2016

Painlessly upload a huge files in Salesforce Using a Visualforce Page

This article (Upload a File as Large as 2GB in Salesforce Using a Visualforce Page) is a brilliant read and well worth a squint if you're asked to facilitate attachments within a SalesForce VisualForce page. There are a couple of things that might warrant changing though. The <chatter:feed entityId="[ID of the custom object record]" /> bit brings in a shed load of CSS that may well not play nicely with your layout so please remember to disabled all CSS files with the class of user:

$(".user").attr("disabled", "disabled");

Of course, you might not mind, but I prefer having a wee bit more control. I prefer to have more control over the blocking behaviour as well so it's also worth including this in the script that's triggered by your uploading a file:

// Hide the chatter:feed file upload guff
setTimeout(function(){ 
    $("#uploadProgressDialog").addClass("hidden");
    $(".shadowDiv").addClass("hidden");
    $(".overlayBackground").addClass("hidden");
}, 100);

Should you want to listen to the return from the upload there is a really nice way of doing so detailed here. We use an awful lot of jQuery so being able to listen to other AJAX requests made from within jQuery is a really rather useful tool. Simply listening to the onReadyStateChangeReplacement and seeing if the readyState is 4 means that you can trigger your own events.

/*
 * This is so we can listen to ajax calls made outside of jQuery.
 */
var open = window.XMLHttpRequest.prototype.open,
    send = window.XMLHttpRequest.prototype.send,
    onReadyStateChange;
function openReplacement(method, url, async, user, password) {
    var syncMode = async !== false ? 'async' : 'sync';
    return open.apply(this, arguments);
}
function sendReplacement(data) {
    if(this.onreadystatechange) {
        this._onreadystatechange = this.onreadystatechange;
    }
    this.onreadystatechange = onReadyStateChangeReplacement;
    return send.apply(this, arguments);
}
function onReadyStateChangeReplacement() {
    // If the ajax is finished
    if(~~this.readyState === 4){
        // repopulate the fake file list
        setTimeout(function(){ 
            /* a setTimeout is probably not required but we 
             * might need to reference something that the 
             * original component has done to the DOM
             */
            // YOUR CODE HERE 
        }, 500);
    }
    if(this._onreadystatechange) {
        return this._onreadystatechange.apply(this, arguments);
    }
}
window.XMLHttpRequest.prototype.open = openReplacement;
window.XMLHttpRequest.prototype.send = sendReplacement;

We use this to check whether the number of attachments has changed, and if it has we re-draw our list.

Tuesday, 21 June 2016

Spring Clean by Proxy review

Spring-Clean by ProxySpring-Clean by Proxy by Robert Bayley
My rating: 4 of 5 stars

A little like The Girl with all the Gifts but with less hope. Slightly, and I mean ever-so-slightly, disjointed at times but on the whole a thoroughly engaging read with a male doing a really rather marvellous female perspective. This is something I've been thinking about for a while and I really couldn't tell that the author was male, not that one should with good writing as both "voices" are human, but I'm not sure I'd be brave enough to try. I do so look forward to reading his next works as he can only get better!

I'm reticent to go into more of the story but he seems to share Stephen Baxter's joy in destruction and it's effects on the people observing the chaos. The effects of the changing physical environment and the changing mental states of the characters are perfectly captured in the prose and I caught myself re-reading sections to try and figure out how he'd captured the feeling not only in the words but in their structure - an excellent and engaging read!

View all my reviews

Saturday, 23 April 2016

Recover images Snippet

As a side project for the archery club I'm a member of I've been looking at saving some stuff to Google Drive as a backup so I've been trawling the interweb for examples of how to upload backups weekly. I did find an old but interesting article but the images were broken :-(.

Then I got to thinking about how to recover them and clocked that the full size images which they linked to still existed so I cracked open Chrome's snippet pane and entered this as a new snippet:

Array.prototype.forEach.call(document.querySelectorAll(".no-line"), function(el, i){
    var img = el.querySelector("img");
    img.setAttribute("src", el.getAttribute("href"));
    img.setAttribute("width","100%");
});

This way of sorting stuff is just so unbelievably powerful and is probably why my codeivate score is as low as it is... Why bother with a full on IDE when you can do most, if not all, of your work in the browser - all without worrying that your changes will affect anyone else?

Sunday, 17 April 2016

It's been an odd couple of days

I was working from home Friday afternoon when the phone rang. It was a teacher from #3’s school who I’d met once or twice and who had read a lovely poem at his funeral. It would’ve been the year he left school after doing his GCSEs this year so she wanted my permission to put something in this years Year Book; a photo and a poem or something. I said that was fine and forwarded her email to his Mum so she could also give permission.


That school has been lovely! I think that there’s some sort of memorial to him in the grounds somewhere but I’ve not been to see it. ‘Er indoors has and said it’s lovely, I think she might’ve even taken a picture and tried to show it to me but I turned away before I could see it so I can’t really remember.


Then Google showed me some pictures from 7 years ago that I’d taken whilst on we’d been on holiday in Tenby. We’d spent the day going to Caldey Island where we’d wandered around and I think I’ve got some aftershave from there - the number of times I shave it’ll probably outlast me!


That was odd as I was on the verge of slinging out a mug with World’s Greatest Dad on it that the boys had bought me while we were there. Seeing the utter failure I’ve been as a Dad meant it simply had to go, thankfully ‘er indoors persuaded me not to. There was a picture on the fridge that I also moved. It was of #2, #3, ‘er indoors and me at our wedding and I remember the photo being taken as I’d just had a go at #3 for carrying on alarming as he was tired. I’m not trying to remember him with rose-tinted glasses or anything, I just don’t want to remember telling him off.


He was a love but could also be a bugger! Goodness me, I miss him so much!


In the meantime I’ve got things growing in the car…


The picture above is from when I was leaving work on Friday afternoon, I thought I’d run into a bush but upon opening the door I looked into the gap and saw this:


I’m guessing it’s fine though. Life always find a way and it’s not down to me to remove it.

Thursday, 24 March 2016

JavaScript: split sentence and add line breaks

I've been using Semantic UIs form validation nigh on constantly over the past few months and apart from the ability to write my own validation rules (which is a brilliant thing by the way) I'm also impressed with the messages that get slung into the UI. The on ly problem I've had though is if the message is particularly long then it can often mess up the layout of the form. After some experimentation I discovered that adding <br/> tags added line breaks within the message. I'm using this an awful lot so I decided to write a simple function which takes a string and splits it on words. It's a wee bit fuzzy so perhaps could do with a wee bit of tidying, but it works for my purposes:

function addBr(sentence, letters, seperator){
    letters = letters || 25;
    seperator = seperator || "<br/>"
    words = sentence.trim().split(" ");
    var returnString = "";
    var line = "";
    while(words.length > 0){
        if(line.length < letters){
            if(~~line.length){
                line += " " + words.shift();
            }else{
                line += words.shift();
            }
        }else{
            returnString += line + seperator;
            line = "";
        }
    }
    if(line !== ""){
        returnString += line;
    }    
    return returnString;
}

Here's a working JSFiddle.