Multiple select list results in order - cont'd


#62

I’ve been reading these for a while now and I almost feel like I’m interrupting but just wanted to let you know that I admire the amount of work that you’ve put into this series!


#63

Converting the tests to get them working with webpack and karma is what we’re doing next.

Removing unwanted scripts

After having got webpack+babel+karma+jasmine working, I now feel good enough about things that we can remove the testing scripts, and use a proper script for using karma tests instead.

package.json

  "scripts": {
    ...
    // "test:mocha": "mocha --require @babel/register test/mocha.test",
    // "test:karma": "karma start",
    // "test:webpack": "mocha-webpack test/test.js",
    // "test:sandbox": "mocha --require @babel/register test/sandbox.test"
    "test": "karma start"
  },

We can also now remove the mocha.test.js, test.js, and jasmine.test.js files.

The improved testing

Instead of using npm run test:karma, we now have a much shorter way to start the tests:

> npm test

and the tests keep on running whenever changes are saved to a file.

Migrate the tests over to webpack

The sandbox.test.js test now works, so we can start converting the other test files.

As Karma is setup to test files ending with test.js, I can rename the *.spec.js files to *.test.js as I migrate the code. That will help to keep track of what’s been converted, and what yet remains to be done.

Convert sandbox

Most of the tests rely on the sandbox code which provides a nice and consistent set of HTML code for testing against, so that’s the easiest first test to convert.

We can rename sandbox.spec.js to sandbox.test.js, which helps us to keep the other tests out of the way until we’re ready to deal with them.

The define statements are replaced with import statements instead. Because import statements are used, we don’t need the use strict declaration anymore either.

sandbox.test.js

// define(["../tests/sandbox"], function (sandbox) {
import sandbox from "../tests/sandbox";
    // "use strict";
    ...
// });

And with the sandbox code, we just need to export is so that it can be imported by the test.

sandbox.js

// define([], function () {
    // "use strict";
    ...
    // return {
    export default {
        ...
    };
// });

And the test now works properly.

Convert create-sandbox

Rename createsandbox.spec.js to createsandbox.test.js, and add the import statement:

create-sandbox.test.js

// describe("Create-sandbox", function (name) {
import sandbox from "./create-sandbox";
    // "use strict";
    ...
// });

create-sandbox.js

// define(["../tests/sandbox"], function (sandbox) {
import sandbox from "./sandbox";
//     "use strict";
// });

export default sandbox;

We then get a testing error because we are no longer using the Jasmine test runner and are now using Karma instead.

TypeError: Cannot read property 'nextSibling' of null

The code responsible for that is some debugging code that we no longer have a use for, as we’re not using the test runner anymore, so it can be removed from the test.

create-sandbox.test.js

//     afterAll(function () {
//         var reporter = document.querySelector(".jasmine_html-reporter");
//         if (reporter.nextSibling) {
//             console.log("Test has failed to clean up after itself.", reporter.nextSibling);
//         }
//     });

And the above tests now run with no problem. We can start converting other tests over too.

convert scroll

The scroll test is the next simplest one to convert.

scroll.test.js

// define(["../tests/sandbox", "scroll"], function (sandbox, scroll) {
import sandbox from "./sandbox";
import scroll from "../src/scroll";
//     "use strict";
    ...
// });

scroll.js

// return {
export default {

This test didn’t take much at all to get working.

Next steps

The remaining code tends to rely on jQuery plugins, so dealing with those will be the focus of the next post.


#64

I’ve been trying several ideas to deal with jQuery plugins, but nothing seems to work when it comes to using the jquery.isscrollable plugin code with webpack+karma.

For example, these are some of the resources that have been tried.

Maybe it’s because the isscrollable extension doesn’t load as a node module, or maybe it’s because the file is misnamed being called “jquey.is-scrollable.js” instead.

Whatever the cause (I will explore this later for the sake of learning), it’s high time to rip it out of our code, which also happens to align nicely with earlier ideas to rid our code of jQuery.

It’s not that jQuery is bad, but it’s bloated for what we use of it. Eventually we’ll be able to remove jQuery completely, helping to trim some weight from our code too.

The choices to be made

Currently there are several balls that need to be juggled at the same time.

  • most of the tests haven’t yet been converted over
  • some parts of the code haven’t been fully integrated yet
  • code is being changed with no good way to tell if things still work

I don’t like making big changes without tests to tell us if the code is still working.

I have a few different options for progress from here.

  • bash my head further against the seemingly brick-wall of integrating jquery plugins into webpack & karma
  • just remove the plugins no tests as a safety net
  • remove jquery plugins from previous working code, then migrate over the updated code

Somehow I find that the latter option is the more pleasing one to consider.

Currently the jquery plugins are a blocker to further progress. Either I get them working, ignore them potentially at my peril, or remove them.

If I do suceed at getting them working, I then plan to remove them. So removing them is the best option here.

The trouble is though, that I can’t reliably go ahead with making such changes without a good set of tests to ensure that nothing breaks.

What I can do instead is to go back to previous code which has working tests, and use that to remove the jQuery plugins, testing all the while to ensure that nothing breaks.

Stop using the isscrollable plugin

I’ve created a separate sr-beta\ folder that contains an earlier version of the code with working tests. I can monitor those tests while I remove the jQuery plugins.

With that earlier set of code, we can update the code so that only the element is passed instead:

scroll.js

        // var $container = getScrollContainer($el);
        var $container = getScrollContainer($el.get(0));
...
    // function getScrollContainer($el) {
    function getScrollContainer(el) {
        // return $el.scrollableparent();
        return $(el).scrollableparent();
    }

We can now use a local function that finds out if the element is scrollable or not.

scroll.js

    function isScrollable(el) {
        var style = window.getComputedStyle(el);
        return (
            style.overflow === "scroll" ||
            style.overflow === "auto" ||
            style.overflowX === "scroll" ||
            style.overflowX === "auto" ||
            style.overflowY === "scroll" ||
            style.overflowY === "auto"
        );
    }
    function getScrollContainer(el) {
        var parent = el.parentNode;
        while (!isScrollable(parent) && parent.nodeName !== "HTML") {
            parent = parent.parentNode;
        }
        return $(parent);
    }

Prevent other code from passing jQuery objects to getScrollContainer

Everything else that calls getScrollContainer needs to only pass in the non-jquery element too.

To ensure that I catch everything, I’m going to break out of the code when a jQuery object is noticed being passed to the function.

scroll.js

    function getScrollContainer(el) {
        if (el.constructor.name === "jQuery") {
            debugger;
        }

That way, I can just rerun the tests and when the code breaks into the debugger, I can tell from the callstack exactly where the next update needs to occur.

scrollmanager.js

    function hasScrollContainerAsParent($el) {
        var el = $($el).get(0);
        // var scrollContainer = scroll.getScrollContainer($el);
        var scrollContainer = scroll.getScrollContainer(el);
    ...
    function getPageUpItem($el) {
        var el = $($el).get(0);
        // var $container = scroll.getScrollContainer($el);
        var $container = scroll.getScrollContainer(el);
    ...
    function getPageDownItem($el) {
        var el = $($el).get(0);
        // var $container = scroll.getScrollContainer($el);
        var $container = scroll.getScrollContainer(el);

scroll.spec.js

            // var scrollContainer = scroll.getScrollContainer($(item));
            var scrollContainer = scroll.getScrollContainer($(item).get(0));

And lastly, there is the getContainerChild function:

scrollmanager.js

            // isScrollParent = scroll.getScrollContainer($($el).get(0)).is($el.parent());
            isScrollParent = scroll.getScrollContainer($el).is($el.parent());

The getScrollContainer function can now return just a normal element, which we’ll convert back that back to a jQuery one when we call the function:

scroll.js

    function getScrollContainer(el) {
        ...
        // return $(parent);
        return parent;
    }
        // var $container = getScrollContainer($el.get(0));
        var $container = $(getScrollContainer($el.get(0)));

scrollmanager.js

        // var scrollContainer = scroll.getScrollContainer(el);
        var scrollContainer = $(scroll.getScrollContainer(el));
        ...
            // if (scroll.getScrollContainer($($el).get(0)).is($el.parent())) {
            if ($(scroll.getScrollContainer($($el).get(0))).is($el.parent())) {
        ...
        // var $container = scroll.getScrollContainer(el);
        var $container = $(scroll.getScrollContainer(el));

Remove the last traces of the jquery.isscrollable plugin

The above changes have resulted in jQuery being pushed up to the getContainerChild function, and we can now remove jquery.isscrollable from the code:

// define(["jquery", "jquery.isscrollable"], function ($) {
define(["jquery"], function ($) {

It’s not needed now in the main.js files too.

tests/main.js and src/main.js

    paths: {
        ...
        // "jquery.isscrollable": "../lib/jquery.isscrollable",
        ...
    },
    shim: {
        ...
        "jquery.isscrollable": ["jquery"],
        ...
    }

Next steps

I noticed earlier that there is a good opportunity with the getContainerChild function to explore what needs to be done when removing jQuery entirely. I’ll explore that next, before moving on to removing other jQuery plugins.


#65

I intend to eventually remove jQuery from all of our code, for it’s a huge library that our code doesn’t benefit from.

Trust me on this, it’ll be worth it.

Remove jQuery from getContainerChild

Here’s the initial function as it currently stands:

scrollmanager.js

    function getContainerChild($el) {
        var isScrollParent = false;
        do {
            $el = $el.parent();
            isScrollParent = $(scroll.getScrollContainer($($el).get(0))).is($el.parent());
        } while (!isScrollParent && !$el.is("html"));
        return $el;
    }

This is a more complex example because of the loop. Most of the time it’s as easy as just stopping jQuery coming in and going out, then converting the rest of the function.

Here’s how we remove the need for jQuery from this function. First I make sure that things keep working when only HTML elements are coming into the function:

scrollmanager.js

    function getContainerChild(el) {
        var $el = $(el);

and I must ensure that other functions that use getContainerChild only pass normal elements without jQuery.

scrollmanager.js

    function createInsideAboveFilter($el, scrollHeight) {
        // $el = getContainerChild($el);
        $el = getContainerChild($($el).get(0));
...
    function createInsideBelowFilter($el, scrollHeight) {
        // $el = getContainerChild($el);
        $el = getContainerChild($($el).get(0));

If I just used $el.get(0) then that would fail if $el was an element, so by turning it into a jQuery object then getting an element from it, I can then later on stop other functions from giving jQuery objects to this function and then replace that whole conversion with just el.

My focus must not be on these filter functions though. For now it’s on the getContainerChild function.

I can now simplify things in the getContainerChild function by replacing the do…while loop with a simple while loop:

scrollmanager.js

        // do {
        while (!isScrollParent && $el.is("html")) {
            $el = $el.parent();
            isScrollParent = $(scroll.getScrollContainer($($el).get(0))).is($el.parent());
        // } while (!isScrollParent && !$el.is("html"));
        }

The tests still pass which helps to reassure me that the updated loop is still correct.

I can then simplify things even further by using an isScrollParent function instead:

scrollmanager.js

    function isScrollParent(el) {
        var container = scroll.getScrollContainer(el);
        return container === el.parentNode;
    }
    function getContainerChild(el) {
        // var isScrollParent = false;
        while (!isScrollParent($($el).get(0)) && !$el.is("html")) {
            $el = $el.parent();
            // isScrollParent = $(scroll.getScrollContainer($($el).get(0))).is($el.parent());
        }

I can now convert $el statements to just el, and use normal element handling techniques with the remainder of the code:

scrollmanager.js

        // var $el = $(el);
        // while (!scrollParent($($el).get(0)) && !$el.is("html")) {
            // $el = $el.parent();
        }
        // return $el;
        // return $(el);
        while (!scrollParent(el) && el.nodeName !== "HTML") {
            el = el.parentNode;
        }
        return el;

After a few more refinements we end up with the following final code:

function getContainerChild(item) {
    var container = item;
    while (!isScrollParent(container) && container.nodeName !== "HTML") {
        container = container.parentNode;
    }
    return container;
}

Let’s compare that improved code with the original function that used the jQuery library:

scrollmanager.js

    function getContainerChild($el) {
        var isScrollParent = false;
        do {
            $el = $el.parent();
            isScrollParent = $(scroll.getScrollContainer($($el).get(0))).is($el.parent());
        } while (!isScrollParent && !$el.is("html"));
        return $el;
    }

That jQuery code is now seen to be not as good as the updated code just above it. Admittedly, even though it might be possible with more work for the jQuery code to do just as good a job, it’s not worth the bloat.

Next steps

By a gradual process of removing jQuery from functions and pushing it out to other functions that call them, we’ll eventually be able to remove jQuery completely.

More of that can wait though until after the other jQuery plugins have been removed, which I’ll do in the next post.


#66

Remove jquery.focusable

The key place where the jquery.focusable plugin is used is in the setFocused function.

scrollmanager.js

    function setFocused($el) {
        if ($el.focusable().length > 0) {
            $el = $el.focusable();
        }
        $el.focus();
    }

The focusable plugin doesn’t tell you if an element is focusable.
Instead, it searches for any children that are focusable.

We can achieve the same thing with the following code:

scrollmanager.js

    function getFocusable(parent) {
        return parent.querySelectorAll(
            "button, [href], input, select, textarea, " +
            "[tabindex]:not([tabindex='-1'])"
        );
    }

So first, we prevent other code from sending jQuery objects to the setFocused function:

scrollmanager.js

    // function setFocused($el) {
    function setFocused(el) {
        var $el = $(el);
        ...
    }
    function moveTo(el) {
        ...
        // setFocused($el);
        setFocused($($el).get(0));
    }

We can now replace the jQuery-specific code in the setFocused function:

scrollmanager.js

        // var $el = $(el);
        // if ($el.focusable().length > 0) {
        //     $el = $el.focusable();
        // }
        // $el.focus();
        var focusable = getFocusable(el);
        if (focusable) {
            focusable[0].focus();
        }

And we’re done! We can now remove the jquery.focusable plugin

scrollmanager.js

// define(["jquery", "scroll", "jquery.focusable"], function ($, scroll) {
define(["jquery", "scroll"], function ($, scroll) {

src/main.js and tests/main.js

    paths: {
        ...
        // "jquery.focusable": "../lib/jquery.focusable",
        ...
    },
    shim: {
        // "jquery.focusable": ["jquery"]
    }

Migrate code

We should now be ready to migrate the updated code. The files that we’ve changed are:

  • scroll.js
  • scroll.spec.js
  • scrollmanager.js
  • tests/main.js
  • src/main.js

The main files aren’t of any relevance to webpack, so back with the webpack code I can rename the scroll and scrollmanager files to scroll-old.js, scroll.test-old.js, and scrollmanager-old.js, and copy over the updated scroll and scrollmanager code.

And Karma successfully runs the scroll tests on the scroll code, I can now remove the old version of those updated files, and carry on with converting the test files.

Next steps

Before converting code so that jQuery isn’t used, I want all of the tests to be in place so that we can easily watch for problems. So next time, I’ll get the other tests working with webpack+karma too.


#67

Now that the complicating jQuery plugins aren’t being used, I want all of the tests to be properly working so that we can easily watch for problems as we work on the code.

Converting tests

The order of converting the tests seems to be best achieved in the following order:

  1. scrollmanager.test.js
  2. checkboxlist.test.js
  3. resultscontroller.test.js
  4. filtercheckboxesbytext.test.js
  5. searchresults.test.js
  6. createcategories.test.js
  7. createresults.test.js

Converting the tests

The conversion process is a simple task, where node_modules have no path, local files have ./ for their path, and other files are accessed by reference from the test folder, such as with ../src/

scrollmanager.test.js

// define(["../tests/sandbox", "scrollmanager"], function (sandbox, scrollManager) {
import sandbox from "./create-sandbox";
import scrollManager from "../src/scrollmanager";
    // "use strict";
    ...
// });

I can modify the karma config to individually check that the tests are working:

karma.config.js

    files: [
      // "test/**/*test.js",
      "test/scrollmanager.test.js",
      ...
    ],

which confirms that the scrollmanager test is all working.

Repeat for all tests

Repeating the above on all of the other tests has them all working, and I can now put the karma config back to what it normally should be:

karma.config.js

    files: [
      // "test/createresults.test.js",
      "test/**/*test.js",
      ...
    ],

and all of the tests now work in the webpack+karma environment!

We’re making good progress on the TODO list:

  • Get code working :white_check_mark:
  • Remove requirejs :white_check_mark:
  • Convert to import/export :white_check_mark:
  • Remove aliases :white_check_mark:
  • Get tests working with webpack :white_check_mark:
  • Remove global jQuery

Next steps

Next time, we get rid of jQuery.


#68

jQuery has been useful at giving us a few easy ways to do things, but it comes at the cost of its enormous size.

Removing jQuery helps to give us a dramatically smaller size for our code.

Removing jQuery from scrollmanager

When it comes to removing the need for jQuery code, it helps if other code doesn’t expect a jQuery object from scrollmanager. When checking for this, I find that that none of the code that calls scrollManager saves any information from it, so we don’t have to worry about that particular issue.

The other issue is passing information to scrollmanager. I want no jQuery objects being given to the scrollmanager code either. We can instead convert them to normal elements, before we convert things further.

checkboxlist.js

function checkboxClickHandler(evt, afterClick) {
    ...
    // scrollManager.moveTo($(el));
    scrollManager.moveTo(el);
    ...
}

scrollmanager.js

// function moveTo($item) {
function moveTo(item) {
    $item = $(item);
    ...
}

Fortunately we have the tests that rapidly tell us when things stop working properly, which allows me to either undo back to working code, or keep me informed about the next simple thing that needs to be fixed.

Removing jQuery ins and outs with hasScrollContainerAsParent

Starting from the top of the code, we have

scrollmanager.js

function hasScrollContainerAsParent($el) {
    var $scrollContainer = $(scroll.getScrollContainer($el));
    var $parent = $el.parent();
    return $scrollContainer.is($parent);
}

First we investigate function’s return value and input parameters. It has a boolean return so we don’t need to worry about how other code handles the return value.

The function parameter though needs to be an element instead of a jQuery object. Fortunately, I made sure to prefix all jQuery variables with a dollar symbol, helping us to easily recognise them.

The code that calls hasScrollContainerAsParent needs to be updated so that it just gives an element instead.

scrollmanager.js

function hasScrollContainerAsParent(el) {
    var $el = $(el);
    ...
}
    // while (!hasScrollContainerAsParent($el) && !$el.is("html")) {
    while (!hasScrollContainerAsParent($el.get(0)) && !$el.is("html")) {

Removing jQuery from calls to the scroll code

We call scroll.getScrollContainer with a jQuery object. We should do that with a normal element, adjusting both scrollmanager and scroll with a minimum of changes so that they both continue to work:

scrollmanager.js

function hasScrollContainerAsParent($el) {
    // var scrollContainer = $(scroll.getScrollContainer($el));
    var $scrollContainer = $(scroll.getScrollContainer($el));
    ...
}

scroll.js

function getScrollContainer(item) {
    $item = $(item);
    ...
}

We achieve a clean separation by removing jQuery from the ins and outs of the function, , and can now focus on converting the scrollmanager code without having to worry about how it impacts other parts of the code.

Completely removing jQuery from hasScrollContainerAsParent

We now have no jQuery objects coming in or going out of the function, so we can now happily convert it to work with normal elements.

scrollmanager.js

function hasScrollContainerAsParent(el) {
    // var $el = $(el);
    // var $scrollContainer = $(scroll.getScrollContainer($el));
    var scrollContainer = scroll.getScrollContainer(el);
    // var $parent = $el.parent();
    var parent = el.parentNode;
    // return $scrollContainer.is($parent);
    return scrollContainer === parent;
}

We can even move that parentNode line down to the return statement:

scrollmanager.js

function hasScrollContainerAsParent(el) {
    var scrollContainer = scroll.getScrollContainer(el);
    // var parent = el.parentNode;
    return scrollContainer === el.parentNode;
}

And that function is now completely free of jQuery.

Repeat ad-nauseum in scrollmanager code

It’s just a matter of doing that to the rest of scrollmanager functions now, for which I’ll spare you the details.

About the only addition that was needed was a way to get sibling elements, for which I added the following code:

scrollmanager.js

function getSiblings(el, type) {
    var siblings = [];
    var siblingType = (type === "prev")
        ? "previousElementSibling"
        : "nextElementSibling";
    el = el[siblingType];
    while (el) {
        siblings.push(el);
        el = el[siblingType];
    }
    return siblings;
}

The scrollmanager code without jQuery looks remarkably like what we were using with jQuery. Here’s the full scrollmanager code.

scrollmanager.js

/*jslint browser */
import scroll from "./scroll";

function getSiblings(el, type) {
    var siblings = [];
    var siblingType = (type === "prev")
        ? "previousElementSibling"
        : "nextElementSibling";
    el = el[siblingType];
    while (el) {
        siblings.push(el);
        el = el[siblingType];
    }
    return siblings;
}
function hasScrollContainerAsParent(item) {
    var scrollContainer = scroll.getScrollContainer(item);
    return scrollContainer === item.parentNode;
}
function upToScrollChild(item) {
    var container = item;
    while (!hasScrollContainerAsParent(container) && container.nodeName !== "HTML") {
        container = container.parentNode;
    }
    return container;
}
function scrollTo(item) {
    scroll.intoView(item);
}
function getFocusableFrom(container) {
    return container.querySelectorAll(
        "button, [href], input, select, textarea, " +
        "[tabindex]:not([tabindex='-1'])"
    );
}
function setFocused(item) {
    var focusable = getFocusableFrom(item);
    if (focusable) {
        focusable[0].focus();
    }
}
function moveTo(item) {
    item = upToScrollChild(item);
    scrollTo(item);
    setFocused(item);
}
function moveToGivenDirection(item, getElFunc) {
    var scrollChild = item;
    if (!hasScrollContainerAsParent(item)) {
        scrollChild = upToScrollChild(item);
    }
    scrollChild = getElFunc(scrollChild);
    if (!scrollChild) {
        scrollChild = item;
    }
    moveTo(scrollChild);
}
function moveToPrevious(item) {
    moveToGivenDirection(item, function getPrev(item) {
        return visibleSiblings(item, "prev")[0];
    });
}
function moveToNext(item) {
    return moveToGivenDirection(item, function getNext(item) {
        return visibleSiblings(item, "next")[0];
    });
}
function isScrollParent(el) {
    var scrollParent = scroll.getScrollContainer(el);
    return el.parentNode === scrollParent;
}
function getContainerChild(item) {
    var container = item;
    while (!isScrollParent(container) && container.nodeName !== "HTML") {
        container = container.parentNode;
    }
    return container;
}
function createInsideAboveFilter(item, scrollHeight) {
    var container = getContainerChild(item);
    return function scrollViewFilter(child) {
        var dist = scroll.outerDistanceBetween(container, child);
        return dist > 0 && dist < scrollHeight;
    };
}
function createInsideBelowFilter(item, scrollHeight) {
    var container = getContainerChild(item);
    return function scrollViewFilter(child) {
        var dist = scroll.outerDistanceBetween(child, container);
        return dist > 0 && dist < scrollHeight;
    };
}
function getPageUpItem(item) {
    var container = scroll.getScrollContainer(item);
    var items = Array.from(container.children);
    var scrollHeight = scroll.innerHeight(container);
    var scrollViewFilter = createInsideAboveFilter(item, scrollHeight);
    var filteredItems = items.filter(scrollViewFilter);
    return filteredItems[0];
}
function getPageDownItem(item) {
    var container = scroll.getScrollContainer(item);
    var items = Array.from(container.children);
    var scrollHeight = scroll.innerHeight(container);
    var scrollViewFilter = createInsideBelowFilter(item, scrollHeight);
    var filteredItems = items.filter(scrollViewFilter);
    return filteredItems.pop();
}
function pageUpFrom(item) {
    item = getPageUpItem(item);
    return moveTo(item);
}
function pageDownFrom(item) {
    item = getPageDownItem(item);
    return moveTo(item);
}
function moveToStart(item) {
    var scrollChild;
    if (!hasScrollContainerAsParent(item)) {
        scrollChild = upToScrollChild(item);
    }
    scrollChild = visibleSiblings(scrollChild, "prev").pop();
    if (!scrollChild) {
        scrollChild = item;
    }
    return moveTo(scrollChild);
}
function moveToEnd(item) {
    var scrollChild;
    if (!hasScrollContainerAsParent(item)) {
        scrollChild = upToScrollChild(item);
    }
    scrollChild = visibleSiblings(scrollChild, "next").pop();
    if (!scrollChild) {
        scrollChild = item;
    }
    return moveTo(scrollChild);
}
function keydownHandler(evt) {
    var item = evt.target;
    if (evt.key === "PageUp") {
        pageUpFrom(item);
        evt.preventDefault();
    }
    if (evt.key === "PageDown") {
        pageDownFrom(item);
        evt.preventDefault();
    }
    if (evt.key === "End") {
        moveToEnd(item);
        evt.preventDefault();
    }
    if (evt.key === "Home") {
        moveToStart(item);
        evt.preventDefault();
    }
    if (evt.key === "ArrowUp") {
        moveToPrevious(item);
        evt.preventDefault();
    }
    if (evt.key === "ArrowDown") {
        moveToNext(item);
        evt.preventDefault();
    }
}
export default {
    moveTo: moveTo,
    keydownHandler: keydownHandler
};

Next steps

I know that I’ll need that visibleSiblings in other code, but will wait until it’s duplicated in at least one other place yet, before moving it out to a utils import.

We’ve made a good start at removing our reliance on jQuery, and can now move on with removing it from the other sets of code.

  • Remove aliases :white_check_mark:
  • Get tests working with webpack :white_check_mark:
  • Remove jQuery from:
    • scrollmanager.js :white_check_mark:
    • scroll.js
    • checkboxlist.js
    • filtercheckboxesbytext.js
    • resultscontroller.js
    • searchresults.js
    • filtercheckboxesbytext.test.js
    • resultscontroller.test.js
  • Remove global jQuery

I like think of this as being similar to removing twitch from the garden. After doing the above tasks jQuery will be completely removed, won’t need it there any more, and we can remove its gigantic weight from our project.


#69

The above process to remove jQuery was followed with the scroll code, which just needed a simple innerHeight function:

scroll.js

function innerHeight(el) {
    var style = window.getComputedStyle(el);
    return parseInt(style.height);
}

and the rest of it was easy to convert, resulting in the following code:

scroll.js

/*jslint browser */

function innerHeight(item) {
    var style = window.getComputedStyle(item);
    return parseInt(style.height);
}
function scrollUpDifference(item, container) {
    var offsetFromContainerTop = item.offsetTop - container.offsetTop;
    var desiredOffset = innerHeight(container) - item.offsetHeight;
    return offsetFromContainerTop - desiredOffset;
}
function scrollUpTo(item, container) {
    var offsetDifference = scrollUpDifference(item, container);
    container.scrollTop = container.scrollTop + offsetDifference;
}
function scrollDownDifference(item, container) {
    var containerScroll = container.scrollTop + container.offsetTop;
    return item.offsetTop - containerScroll;
}
function scrollDownTo(item, container) {
    var offsetDifference = scrollDownDifference(item, container);
    container.scrollTop = container.scrollTop + offsetDifference;
}
function isScrollable(item) {
    var style = window.getComputedStyle(item);
    return (
        style.overflow === "scroll" ||
        style.overflow === "auto" ||
        style.overflowX === "scroll" ||
        style.overflowX === "auto" ||
        style.overflowY === "scroll" ||
        style.overflowY === "auto"
    );
}
function getScrollContainer(item) {
    var parent = item.parentNode;
    while (!isScrollable(parent) && parent.nodeName !== "HTML") {
        parent = parent.parentNode;
    }
    return parent;
}
function outerHeight(item) {
    return item.offsetHeight;
}
function scrollIntoView(item) {
    var container = getScrollContainer(item);
    if (scrollUpDifference(item, container) > 0) {
        scrollUpTo(item, container);
    }
    if (scrollDownDifference(item, container) < 0) {
        scrollDownTo(item, container);
    }
}
function distanceBetween(item1, item2) {
    return item1.offsetTop - item2.offsetTop;
}
function outerDistanceBetween(item1, item2) {
    var dist = distanceBetween(item1, item2);
    dist += (dist >= 0)
        ? outerHeight(item1)
        : outerHeight(item2);
    return dist;
}
export default {
    intoView: scrollIntoView,
    outerDistanceBetween: outerDistanceBetween,
    innerHeight: innerHeight,
    getScrollContainer: getScrollContainer
};

jQuery was also removed from Checkboxlist very easily, and found that I was duplicating some functions so they’ve been put in to a separate utils file:

utils.js

function isVisible(el) {
    return el.offsetParent !== null;
}
function getVisible(els) {
    return Array.from(els).filter(isVisible);
}
export default {
    isVisible,
    getVisible
};

checkboxlist.js

/*jslint browser */
import utils from "./utils";
import scrollManager from "./scrollmanager";

var container;
var clickHandler;

function getCheckboxByValue(value) {
    return document.querySelector("input[value=" + value + "]");
}
function uncheckByValue(value) {
    var checkbox = getCheckboxByValue(value);
    if (checkbox.checked) {
        checkbox.click();
    }
}
// We can't just set the checkbox checked value to true.
// Other things also need to occur that are triggered by the click event.
// Because of that, we need to click on the checkbox instead.
function checkFirstCheckbox() {
    var checkboxes = container.querySelectorAll("input[type=checkbox]");
    var firstCheckbox = utils.getVisible(checkboxes)[0];
    if (!firstCheckbox.checked) {
        firstCheckbox.click();
    }
    firstCheckbox.focus();
}
function reset() {
    var inputs = container.querySelectorAll("input[type=checkbox]");
    var checkedInputs = Array.from(inputs).filter((input) => input.checked);
    checkedInputs.forEach((input) => input.click());
}
function checkboxClickHandler(evt, afterClick) {
    var el = evt.target;
    scrollManager.moveTo(el);
    afterClick(el);
}
function clickWrapper(callback) {
    return function wrappedClickHandler(evt) {
        checkboxClickHandler(evt, callback);
    };
}
function init(opts) {
    container = opts.container;
    clickHandler = clickWrapper(opts.afterClick);
    container.addEventListener("click", function (evt) {
        var el = evt.target;
        if (el.type === "checkbox") {
            return clickHandler(evt);
        }
    });
    container.addEventListener("keydown", scrollManager.keydownHandler);
}
export default  {
    uncheckByValue: uncheckByValue,
    checkFirst: checkFirstCheckbox,
    reset: reset,
    init: init
};

The comment that I left in the above code reminds me that I also need to revisit the checkbox click handler code. It would be nice if it could store and run a series of events from an array instead.

Next steps

The process of removing jQuery is progressing smoothly, and at this pace shouldn’t take long to complete.

  • Remove jQuery from:
    • scrollmanager.js :white_check_mark:
    • scroll.js :white_check_mark:
    • checkboxlist.js :white_check_mark:
    • filtercheckboxesbytext.js
    • resultscontroller.js
    • searchresults.js
    • filtercheckboxesbytext.test.js
    • resultscontroller.test.js
  • Remove global jQuery
  • Use click handler events array