Wednesday, 4 January 2017

Javascript: Finding the index of an Object in an Array

Having done this more times than is wise it's documented here so that, when it's forgotten, this will act as an aide memoire.

Objects can be a bugger as they aren't equivalent unless they are actually the same thing:

console.log({"name": "Fred"} !== {"name": "Fred"});
var a = {"name": "Fred"};
var b = a;
console.log(a === b);

Meaning that the best way to check equivalence is to convert them to strings and compare them that way:

console.log(JSON.stringify({"name": "Fred"}) === JSON.stringify({"name": "Fred"}));

This string comparison is what happens in the following script. It iterates over the array of objects using the map array method and returns the index or false if the object does not exist in that position in the array. False values are filtered, and the array is counted. If the length is greater than zero then the index is again calculated and returned, otherwise -1 is returned.

var list = [
    {
        "name": "Jane"
    }, {
        "name": "John"
    }, {
        "name": "William"
    }],
    target = {
        "name": "Fred"
    },
    s = JSON.stringify,
    p = JSON.parse,
    c = a => p(s(a).replace(/(,false)/g, "").replace(/\[false\,/, "[").replace(/\,false\]/, "]").replace(/\[false\]/, "[]")),
    index = (c(list.map((o, i) => (s(o) === s(target)) && i)).length > 0) ? c(list.map((o, i) => (s(o) === s(target)) && i))[0] : -1;

console.log("index:", index);

It must be pointed out, though, that if the object is present more than once the index of the first appearance is returned.

The code is slightly tongue-in-cheek as it's stupidly minimal, please do take some time to check that you know what's going on before using it.

Thursday, 22 December 2016

Cambridge Evening News Classified Section Friday, May 3, 1974 p.8

How things have changed!
The advert reads:


NATIONAL BLOOD
TRANSFUSION SERVICE 

Long Road. 
Cambridge CB2 2PT 

BLOOD DONOR ATTENDANT

Ladies who are willing to 
travel throughout East Anglia
and work irregular hours are
required to staff mobile blood
collecting teams. Training will
be given. Adequate allowances
are payable for day and over-
night absences from Cam-
bridge. Uniform provided free.
Commencing salary £843 per
annum at age 18, rising to
£1,221 per annum commenc-
ing at £1,041 at age 21 and
over. 

PART-TIMERS 

to work 40 hour fortnight also 
required. 

Please write or telephone
Cambridge 45921

Tuesday, 13 December 2016

Extending jQuery Validation to cope with whitespace

I like formatting HTML; I go through all sorts of weird and wonderful ways of formatting HTML. One of my favourite things is to format forms so I can see things properly as I'm coding without too much horizontal scrolling, as such, I end up with forms a little like this:

<form 
  class="formTwo" 
  id="formTwo" 
  method="get" 
  action="">
    <fieldset>
        <legend>
            Test Two
        </legend>
        <p>
            <label 
              for="word2">
                Word 2 (required, at least 2 characters)
            </label>
            <input 
              id="word2" 
              name="word2" 
              type="text"/>
        </p>
        <p>
            <label 
              for="comment2">
                Comment 2 (required)
            </label>
            <textarea 
              id="comment2" 
              name="comment2">
              </textarea>
        </p>
        <p>
            <input 
              class="submit" 
              type="submit" 
              value="Submit"/>
        </p>
    </fieldset>
</form>

The thing is the Textarea ends up having a shed load of white space within it. This means that when I get around to validating that the Textarea has content using jQuery Validation I end up getting false positives as it counts whitespace as a valid input. All I'd need to do to pass the validation for the form above, with the following validation rules in place:

$("#formTwo").validate({
    "rules":{
        "word2": {
            "required": true,
            "minlength": 2
        },
        "comment2": "required"
    },
    "messages": {
        "word2": {
            "required": "Please enter a word",
            "minlength": "Your word must consist of at least 2 characters, with no spaces"
        },
        "comment2": "Don't be a prat!"
    }
});

Is to enter 2 spaces!

I've boon looking around at ways of solving this for a while, as it isn't only an issue for people who have odd indentation habits and I've come across two main ways of sorting it. One is to copy the required method, replace it and then call it again after running the replacement; the other is to extend it. I think that I prefer extending it but here are the two methods I've come across:

Copy, repalce and implement

$.validator.methods.oldRequired = $.validator.methods.required;
$.validator.addMethod("required", function(value, element, param) {
    if (value.trim().length === 0) {
        return false;
    }
    return $.validator.methods.oldRequired.call(this, value, element, param);
}, $.validator.messages.required);

Extend

$.extend($.validator.methods, {
    required: function(b, c, d) {
        if (!this.depend(d, c)) return "dependency-mismatch";
        if ("select" === c.nodeName.toLowerCase()) {
            var e = a(c).val();
            return e && e.length > 0
        }
        return this.checkable(c) ? this.getLength(b, c) > 0 : b.trim().length > 0
    }
});

Both methods add a check on the trimmed value passed to the method; ensuring that whitespace is removed before the value is tested.

I think I prefer the extension method, it was inspired by this answer, whereas the first method was stolen from Craig Stuntz.

It might be less robust, though, especially if the plugin undergoes a radical change and the required method alters a great deal, but I think that that's a small price to pay for elegance.

Wednesday, 30 November 2016

Transforming an object with arrays into an array of objects with Javascript

This question was a lot of fun and I spent a happy 10 minutes figuring out how to transform the data. This seems to do the job:

var obj = {
    "pharmacy": [
        "Walmart",
        "Safeway",
        "Kroger Pharmacy"
    ],
    "price": [
        58.14,
        65.45,
        66.76
    ]
}, data = [];

for(var i = 0; i < obj[Object.keys(obj)[0]].length; i++){
    var tempObject = {};
    for(var key in obj){
        tempObject[key] = obj[key][i];
    }
    data.push(tempObject)
}

console.log(data);

I'm only putting it up here so I don't forget how to do it again if I need to do so. It's also worthwhile noting that the initial arrays need to be the same length or things could go really quite wrong.

Saturday, 5 November 2016

DataTable Row Re-order

I got a little stumped by a Question on stackoverflow. The poster asked about swapping rows within a DataTable and I've never had a need to do that so I got to thinking about how it might be done. The poster also got into a wee bit of tizzy over the use of this so that was the first thing I corrected. Afterwards, it kept playing on my mind as an interesting challenge so this morning I decided to scratch that itch. I clocked that I needed an artificial index so added a hidden column to the table upon which to sort the rows:

<div class="container">
    <table cellpadding="0" cellspacing="0" border="0" class="table" id="example">
        <thead>
            <tr>
                <th>Index</th>
                <th>Rendering engine</th>
                <th>Browser</th>
                <th>Platform(s)</th>
                <th>Swap Down</th>
                <th>Swap Up</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th>0</th>
                <td>Trident</td>
                <td>Internet Explorer 4.0</td>
                <td>Win 95+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>1</th>
                <td>Trident</td>
                <td>Internet Explorer 5.0</td>
                <td>Win 95+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>2</th>
                <td>Trident</td>
                <td>Internet Explorer 5.5</td>
                <td>Win 95+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>3</th>
                <td>Trident</td>
                <td>Internet Explorer 6</td>
                <td>Win 98+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>4</th>
                <td>Trident</td>
                <td>Internet Explorer 7</td>
                <td>Win XP SP2+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>5</th>
                <td>Trident</td>
                <td>AOL browser (AOL desktop)</td>
                <td>Win XP</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>6</th>
                <td>Gecko</td>
                <td>Firefox 1.0</td>
                <td>Win 98+ / OSX.2+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>7</th>
                <td>Gecko</td>
                <td>Firefox 1.5</td>
                <td>Win 98+ / OSX.2+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>8</th>
                <td>Gecko</td>
                <td>Firefox 2.0</td>
                <td>Win 98+ / OSX.2+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>9</th>
                <td>Gecko</td>
                <td>Firefox 3.0</td>
                <td>Win 2k+ / OSX.3+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>10</th>
                <td>Gecko</td>
                <td>Camino 1.0</td>
                <td>OSX.2+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>11</th>
                <td>Gecko</td>
                <td>Camino 1.5</td>
                <td>OSX.3+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>12</th>
                <td>Gecko</td>
                <td>Netscape 7.2</td>
                <td>Win 95+ / Mac OS 8.6-9.2</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>13</th>
                <td>Gecko</td>
                <td>Netscape Browser 8</td>
                <td>Win 98SE+</td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <th>14</th>
                <td>Misc</td>
                <td>Lynx</td>
                <td>Text only</td>
                <td></td>
                <td></td>
            </tr>
        </tbody>
    </table>
</div>

This JS seems to do the trick in terms of swapping rows:

$(function() {
    var table = $("#example").DataTable({
        "order": [
            [0, 'asc']
        ],
        "paging": true,
        "columns": [{
            "visible": false
        }, {
            "orderable": false
        }, {
            "orderable": false
        }, {
            "orderable": false
        }, {
            "render": function(d) {
                return $("<i/>", {
                    "class": "fa fa-caret-down swapable swapDown"
                }).prop("outerHTML");
            },
            "orderable": false
        }, {
            "render": function(d) {
                return $("<i/>", {
                    "class": "fa fa-caret-up swapable swapUp"
                }).prop("outerHTML");
            },
            "orderable": false
        }]
    });
    $('#example tbody').on('click', 'td', function(event) {
        // We're only interested in cells with a class of swapable
        if ($(this).find(".swapable")) {
            // Helper variable
            var _this = $(this).find(".swapable");
            // Total number of rows (including hidden rows (zero based index))
            var tableRows = table.data().length - 1;
            // Index of current row
            var rowIndex = table.row(this).index();
            // Data of current row
            var rowData = table.row(this).data();
            // Index value of row (artifical because it's ours) - also tempval
            var artificalIndex = ~~rowData[0];
            /*
             * If we're on the bottom row of the table and the direction of travel is downwards  or if we're
             * on the top row and the direction of travel is upwards then we need to just swap the top and
             * bottom rows
             */
            if(
                    (_this.hasClass("swapDown") && artificalIndex === tableRows)
                    ||
                    (_this.hasClass("swapUp") && artificalIndex === 0)
            ){
                var topIndex, bottomIndex;
                table.rows().every(function(rowIdx, tableLoop, rowLoop) {
                    var data = this.data();
                    if(~~data[0] === 0){
                        topIndex = rowIdx;
                    }
                    if(~~data[0] === tableRows){
                        bottomIndex = rowIdx;
                    }
                });
                table.cell(topIndex, 0).data(tableRows);
                table.cell(bottomIndex, 0).data(0);
            }else{
                var movingIndex, tempData;
                table.rows().every(function(rowIdx, tableLoop, rowLoop) {
                    var data = this.data();
                    // Moving down
                    if (_this.hasClass("swapDown") && ~~data[0] === artificalIndex + 1) {
                        movingIndex = rowIdx;
                        tempData = data[0];
                    }
                    // Moving up
                    if (_this.hasClass("swapUp") && ~~data[0] === artificalIndex - 1) {
                        movingIndex = rowIdx;
                        tempData = data[0];
                    }
                });
                table.cell(rowIndex, 0).data(tempData);
                table.cell(movingIndex, 0).data(artificalIndex);
            }
            table.draw(false);
        }
    });
});

I'm happy that it works but it does seem a little inefficient and I'm interested in how it might be improved if anyone can come up with any ideas? The working JSFiddle is here so please use that as a base upon which to work.

One of my main issues was figuring out how to cope with people clicking the up icon on the top row or the down icon on the bottom row, then I clocked that it merely meant that the top and bottom rows needed to be swapped. I also ran into difficulty figuring out the index of rows as I'm pretty sure it isn't updated after a draw as I kept running into issues... this is the reason I introduced my own, artificial, index in the hidden first column.

Thursday, 27 October 2016

JavaScript accordion sans jQuery

Doing some work where using jQuery was verboten this (Ridiculously simple accordion without the jQuery UI library) was found.

It still used jQuery though so rolled my own. This markup:

<div id="accordion">
    <h4 class="accordion-toggle">Accordion 1</h4>
    <div class="accordion-content default">
        <p>Cras malesuada ultrices augue molestie risus.</p>
    </div>
    <h4 class="accordion-toggle">Accordion 2</h4>
    <div class="accordion-content">
        <p>Lorem ipsum dolor sit amet mauris eu turpis.</p>
    </div>
    <h4 class="accordion-toggle">Accordion 3</h4>
    <div class="accordion-content">
        <p>Vivamus facilisisnibh scelerisque laoreet.</p>
    </div>
</div>

With this CSS:

.accordion-toggle {
    cursor: pointer;
}
.accordion-content {
    display: none;
}
.accordion-content.default {
    display: block;
}

And with this Javascript (Polyfill added):

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Polyfill
if (!Array.prototype.filter) {
    Array.prototype.filter = function(fun/*, thisArg*/) {
        'use strict';

        if (this === void 0 || this === null) {
            throw new TypeError();
        }

        var t = Object(this);
        var len = t.length >>> 0;
        if (typeof fun !== 'function') {
            throw new TypeError();
        }

        var res = [];
        var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
        for (var i = 0; i < len; i++) {
            if (i in t) {
                var val = t[i];

                // NOTE: Technically this should Object.defineProperty at
                //       the next index, as push can be affected by
                //       properties on Object.prototype and Array.prototype.
                //       But that method's new, and collisions should be
                //       rare, so use the more-compatible alternative.
                if (fun.call(thisArg, val, i, t)) {
                    res.push(val);
                }
            }
        }

        return res;
    };
}

(function() {
    "use strict";
    // Stolen from: https://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/
    var forEach = function(array, callback, scope) {
        for (var i = 0; i < array.length; i++) {
            callback.call(scope, i, array[i]);
        }
    },
        accordion = document.getElementById("accordion"),
        toggles = accordion.querySelectorAll(".accordion-toggle"),
        greedy = false;
    forEach(toggles, function(i, v) {
        v.addEventListener("click", function() {
            var content = this.nextElementSibling;
            var classes = content.className.split(" ");
            if (classes.indexOf("default") !== -1) {
                content.className = classes.filter(function(value){
                    return value !== "default"
                }).join(" ");
            } else {
                if (greedy) {
                    forEach(toggles, function(i, toggle) {
                        var ct = toggle.nextElementSibling;
                        var cs = ct.className.split(" ");
                        ct.className = cs.filter(function(value){
                            return value !== "default"
                        }).join(" ");
                    });
                }
                content.className += " default";
            }
        }, false);
    });
})();

Does all that I need except for the animation - but I'll get there yet!

Working example here.