Viewing page 1 of 62

Added Amazon S3 Browser Widget to Texo

Written by Adam Presley on 04/18/2014 at 12:55 AM

After much work I have completed adding an Amazon S3 Browser widget to Texo CMS. Texo is the engine that I wrote which runs this blog. It is still in its infancy but this widget will certainly make writing my blog posts easier. After setting up Amazon S3 key settings and bucket information a new button shows up while editing or writing blog entries. Clicking on this brings up a widget that shows all available images and allows me to view them, select an image, delete, and even upload new images. Selecting an image will populate the correct Markdown syntax in the editor with the URL ready to go.

Selecting an image

Uploading an image

Deleting an image

JavaScript Functional Goodness Part 3

Written by Adam Presley on 04/17/2014 at 12:17 AM

More and more I am trying to stretch my "functional muscles" any time I have a chance. I've been working on an enhancement to my blog engine by adding a button to the editor I am using for editing my Markdown-based posts. This new button, when clicked, will display a dialog widget with thumbnails for all images in an Amazon S3 bucket.

S3 Browser Screenshot

When the widget is initialized it makes a call to a RESTful endpoint that is responsible for authenticating credentials and retrieving bucket contents. The result of that call returns data that looks something like this.

[
    {"url":"http://adampresley.com/image1.png", "name": "image1.png"},
    {"url":"http://adampresley.com/image2.png", "name": "image2.png"},
    {"url":"http://adampresley.com/image3.png", "name": "image3.png"},
    {"url":"http://adampresley.com/image4.png", "name": "image4.png"}
]

Originally I had the code perform an AJAX call using jQuery and then did a good old-fashioned for loop over that data. Inside that loop I was concatenating to a string the DOM necessary to create image elements to display thumbnails of each image. Then I decided to try to clean it up and go a little functional on it.

The first order of business we must address is to define a function that will create the DOM element for a single thumbnail image given a single item in the array demonstrated above. This code creates an image element and attaches the src attribute which is the URL to the image. It also stores the S3 key name in the attribute data-name for retrieval later on. Then we setup some CSS attributes to get it sized right and return our new element. That looks like this.

_createThumbnailItemDom = function(thumbnailItem) {
    return $("<img />")
        .attr("src", thumbnailItem.url)
        .attr("data-name", thumbnailItem.name)
        .addClass("s3BrowserWidgetItem")
        .css({
            width: "150px",
            height: "150px",
            margin: "15px"
        });
}

Next up we need a method to iterate over each item in our returned dataset and craft the DOM elements, then attach them to a DIV container. This problem turned out to be a one-liner, but has two real parts to it. The first part of this is transforming the current object in our array to a DOM element using our method defined above. We can use the $.map() method in jQuery to apply a function to all items in our array like so.

$.map(data, _createThumbnailItemDom)

The above line of code will return a new array with each item in the old array transformed into DOM elements. Now we need to append all those elements to a target element. So what we will do is create a method that takes a reference to the target element and the array of AJAX data. We then will iterate over that dataset using jQuery's $.each() method. As you may recall the $.each() method takes two arguments. The first is an array, and the second is a function that takes an index and a current item. In this function you do whatever you wish with the item. In our case, we want to append it to the target DOM element.

_createThumbnailsDom = function(targetEl, data) {
    $.each($.map(data, _createThumbnailItemDom), function(index, el) { targetEl.append(el); });
}

The final step is to make an AJAX call to get our data, then on successful return call our _createThumbnailsDom() method to create our thumbnails.

var onSuccess = function(response) { _createThumbnailsDom($(el), response.data); };
$.ajax({ url: "/some/endpoint/for/bucketdata" }).done(onSuccess);

And that's it! Below is the full code for the S3 Browser widget so far. I'm sure it will change over time, but this is how it looks now. Cheers, and happy coding!

/**
 * Class: S3Browser
 * This class provides a visual widget for viewing files in an Amazon
 * S3 bucket. A user can select images to get a full URL to the
 * S3 location. This window also allows uploading files to Amazon S3.
 *
 * This widget is based on the jQuery UI dialog widget. As such all the
 * same options available to the dialog widget are available in the S3Browser
 * widget.
 *
 * When an image is selected in the S3 Browser an event labeled 
 * *s3browser-widget.select* is fired.
 *
 * Exports:
 *    $.ui.S3Browser
 *
 * RequireJS Name:
 *    s3browser-widget
 *
 * Dependencies:
 *    jquery
 *    jqueryui
 *
 * Commands:
 *    open - Opens the S3 Browser
 *    close - Closes the S3 Browser
 *
 * Example:
 *    > require(["jquery", "s3browser-widget"], function($) {
 *    >    $("#someDiv").S3Browser();
 *    >    $("#someDiv").S3Browser("open");
 *    > });
 */
define(
    [
        "jquery", "rajo.pubsub", "jqueryui"
    ],
    function($, PubSub) {
        "use strict";

        var 
            /**
             * Fuction: _createDom
             * Method that creates the DOM for a single instance of this dialog widget.
             *
             * Parameters:
             *    getBucketListEndpoint - URL to the service endpoint to get the list of items in an S3 bucket
             *    dialogEl              - A reference to the dialog element to render to
             *    dialogElId            - ID of the dialog element to render to
             */
            _createDom = function(getBucketListEndpoint, dialogEl, dialogElId) {
                var
                    body = "<div class=\"s3BrowserWidgetItems\" style=\"width: 100%; height: auto;\"></div>",
                    el = "#" + dialogElId + " .s3BrowserWidgetItems",

                    onSuccess = function(response) { _createThumbnailsDom($(el), response.data); };

                $.ajax({ url: getBucketListEndpoint }).done(onSuccess);

                /*
                 * Add the initial body container to the dialog. The thumbnails
                 * are loaded via AJAX and attached via the _createThumbnailsDom method.
                 */
                dialogEl.html(body);

                /*
                 * Assign a click event handler to any element with a class of 
                 * "s3BrowserWidgetItem" that is a child of our container element.
                 */
                $(el).on("click", ".s3BrowserWidgetItem", function() {
                    $(el + " .s3BrowserWidgetItem").removeClass("img-thumbnail");
                    $(this).toggleClass("img-thumbnail");
                });
            },

            /**
             * Function: _createThumbnailItemDom
             * Creates an individual thumbnail DOM item and returns it.
             * 
             * Parameters:
             *    thumbnailUrl - URL to the image thumbnail
             */
            _createThumbnailItemDom = function(thumbnailItem) {
                return $("<img />")
                    .attr("src", thumbnailItem.url)
                    .attr("data-name", thumbnailItem.name)
                    .addClass("s3BrowserWidgetItem")
                    .css({
                        width: "150px",
                        height: "150px",
                        margin: "15px"
                    });
            },

            /**
             * Function: _createThumbnailsDom
             * This function creates all thumbnail DOM elements in a set of data
             * and appends them to a target DOM element. The data parameter
             * is an array of thumbnail URLs.
             *
             * Parameters:
             *    targetEl - Element to attach thumbnail DOM items to
             *    data     - Array of thumbnail URLs
             */
            _createThumbnailsDom = function(targetEl, data) {
                $.each($.map(data, _createThumbnailItemDom), function(index, el) { targetEl.append(el); });
            },

            /**
             * Function: _onDelete
             * Event handler for the *Delete* button. This will publish an event named
             * *s3browser-widget.delete* with a reference to the dialog element, the
             * URL of the image, and the S3 key name.
             */
            _onDelete = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    PubSub.publish("s3browser-widget.delete", {
                        dialogEl: dialogEl,
                        imageUrl: selectedImageEl[0].src,
                        name    : selectedImageEl[0].getAttribute("data-name")
                    });
                }
            },

            /**
             * Function: _onSelect
             * Event handler for the *Select* button. This will publish an event
             * named *s3browser-widget.select* with a reference to the dialog element, the
             * URL of the image, and the S3 key name.
             */
            _onSelect = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    PubSub.publish("s3browser-widget.select", {
                        dialogEl: dialogEl,
                        imageUrl: selectedImageEl[0].src,
                        name    : selectedImageEl[0].getAttribute("data-name")
                    });
                }
            },

            /**
             * Function: _onView
             * Event handler for the *View* button. This will open up the selected
             * image in a new tab/window.
             */
            _onView = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    window.open(selectedImageEl[0].src);
                }
            };

        /*
         * Create the widget in the "adampresley" namespace using the 
         * jQuery UI WidgetFactory.
         */
        $.widget("adampresley.S3Browser", $.ui.dialog, {
            _create: function() {
                _createDom(this.options.getBucketListEndpoint, this.element, this.element[0].id);
                this._super();
            },

            options: {
                title   : "Amazon S3 Browser",
                width   : 450,
                height  : 450,
                autoOpen: false,
                modal   : true,
                resizable: true,
                buttons : [
                    {
                        text : "Select",
                        click: function() { _onSelect(this); }
                    },
                    {
                        text : "View",
                        click: function() { _onView(this); }
                    },
                    {
                        text : "Delete",
                        click: function() { _onDelete(this); }
                    }
                ],

                getBucketListEndpoint: "/admin/ajax/s3/bucket"
            }
        });
    }
);

Click Event Scope in jQuery UI Widget Factory

Written by Adam Presley on 04/09/2014 at 09:42 PM

A little tidbit that I figured out today regarding the scope of this in a jQuery UI dialog button. Actually I feel foolish for not checking the actual value of this sooner, but you can't win them all. With the creation of the WidgetFactory I've been looking at writing my UI widgets using this tool. In fact the search widget on this blog uses the jQuery UI WidgetFactory. Originally I thought I had an issue with scope, but turns out I was mistaken. You see the search widget extends the jQuery UI dialog object and has a button on it. Usually when an event handler is called the scope of this references the DOM element that fired the event. So I assumed that this would reference the button being clicked. Fortunately I was wrong.

When jQuery UI handles a click event on buttons defined in your options object it actually changes the scope of this to the dialog element itself. This is a good thing. Why? Because you may be initializing your widget against multiple matched DOM elements in your jQuery selector, so you need to know which dialog/widget you are working with. So for anyone who may have wondered, there you have it.

Blog Engine - Update 2

Written by Adam Presley on 03/25/2014 at 12:43 AM

Back in November 2013 I released my blog site under a new engine written by me. Before that I had run my site on Blogger for a while, but I grew tired of how sluggish it had become. I felt I could get better performance. So I set out to write my own using Python and lots of JavaScript.

Initially I wrote this with my site being fully AJAX driven. When you hit a URL you were hitting URI fragments which would hit a JavaScript controller I had written to route you to the correct blog entry or page of entries. This worked fairly well performance wise, but suffered from one major problem that I was ignorant of. It became a giant pain in the butt to index my site with Google. After researching the issue I did find that Google offers a solution for indexing AJAX-heavy sites, but it required me to come up with clever, convoluted solutions to create static renders of each blog post and page of posts. This turned out to be more trouble than it's really worth.

I decided to refactor a bit, though it has been a slow process. I also wanted to expand on the administrator I had built for myself and turn this into a proper CMS engine, or at least start down that path. Tonight I have released the newest increment of my site using the latest version of my code. I am calling my CMS engine Texo CMS. Of course it only really does blog posts right now, and doesn't really deserve the title of CMS, but I'm working on it.

Screenshot

My next steps are to continue development on this application, and make a home for it on Github. More on that soon.

JavaScript Functional Goodness Part 2

Written by Adam Presley on 11/15/2013 at 08:21 AM

A couple of days ago I posted an entry on some basic functional-style programming constructs that I've been slowly learning. I am back to post part 2. Here's the refresher.

Here is the JSON data that I was working with. It was being used to feed a line chart.

{
   "seriesTotals": [
      3,
      77,
      54,
      65,
      64,
      56,
      41,
      14,
      63,
      99,
      63,
      30,
      41,
      24,
      12,
      42,
      53,
      51,
      50,
      35,
      27,
      22,
      58,
      65,
      40,
      51,
      44,
      27,
      12,
      66,
      64
   ],
   "labels":    [
      "Oct 13",
      "Oct 14",
      "Oct 15",
      "Oct 16",
      "Oct 17",
      "Oct 18",
      "Oct 19",
      "Oct 20",
      "Oct 21",
      "Oct 22",
      "Oct 23",
      "Oct 24",
      "Oct 25",
      "Oct 26",
      "Oct 27",
      "Oct 28",
      "Oct 29",
      "Oct 30",
      "Oct 31",
      "Nov 1",
      "Nov 2",
      "Nov 3",
      "Nov 4",
      "Nov 5",
      "Nov 6",
      "Nov 7",
      "Nov 8",
      "Nov 9",
      "Nov 10",
      "Nov 11",
      "Nov 12"
   ],
   "dayNames":    [
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday",
      "Monday",
      "Tuesday"
   ]
}

In the last post I wanted to sum up the series totals. In this post I want to determine which day of the week has the highest number. Each number in the series is associated with a day of the week, and I needed to determine which day of the week has the highest summed value.

This can be done in a typical set of loops and sums like so.

var totalsByDay = {};
var day = "";

for (var index = 0; index < data.seriesTotals.length; index++) {
    day = data.dayNames[index];
    if (!totalsByDay.hasOwnIndex(day)) totalsByDay[day] = 0;

    totalsByDay[day] += data.seriesTotals[index];
}

var maxDay = "";
var maxDayValue = 0;

for (var key in totalsByDay) {
    if (totalsByDay.hasOwnProperty(key)) {
        if (totalsByDay[key] > maxDayValue) {
            maxDayValue = totalsByDay[key];
            maxDay = key;
        }
    }
}

// maxDay == Tuesday
// maxDayValue = 335

Once again there is nothing specifically wrong with this code (as far as I know as I didn't actually test it). It first loops over the series total values and compiles an object where each key is a day of the week with a value of the total number of items for that day. It then performs another loop over this new structure and determines which item has the highest value.

To try a functional approach I decided to use map/reduce functions and a max function. The map/reduce will create an array of object where the key is the day of the week and the value is a number from the series totals. The reduce part of the function will combine each day of the week object into a single key/value pair representing the total items for each given day. In other words the map function will produce something like this.

[
    { key: "Sunday", value: 3 },
    { key: "Monday", value: 77 },

    ...

    { key: "Sunday", value: 14 }

    ...
]

And so on. The reduce function will sum up each day into a single object for each day like so.

{
    "Sunday": 60,
    "Monday": 229,

    ...
}

And so on. Here's what that code looks like looks like.

/*
 * Sum up each day. The items array will be filled with objects that line
 * up items per day with the appropriate day of the week.
 */
var mapIdx = 0; // keep up with the day of the week
var totalsByDay = Util.reduce(
    {},
    Util.map(data.seriesTotals, function(item) { var r = { key: data.dayNames[mapIdx], value: item }; mapIdx++; return r; }),
    function(a, b) {
        if (!a.hasOwnProperty(b.key)) a[b.key] = 0;
        a[b.key] += b.value;
        return a;
    }
);

Much like last time the reduce function takes three arguments. The first is the starting value. In our case that is a blank object. The second is an array of items to iterate over, and the third is the function that takes two items and returns a "combined" item.

In this example the array of items is generated using the map() function. It takes in the seriesTotals array and returns a new array of objects who's key is the day of the week and the value from seriesTotals. The reduce combine function basically ensures that each time it returns an object it has the day of the week as a key (and ensures it exists), and incrementally adds each series value to the previous value. The result is a structure that looks like this.

{
    "Monday": 229,
    "Tuesday": 335,
    "Wednesday": 282,
    "Thursday": 258,
    "Friday": 176,
    "Saturday": 119
}

Now armed with this data the last piece is to determine which day has the highest value. We saw how we can do that with a couple of tracking variables and a loop. Here is one way using a max() function to do the same thing. The max() function takes three arguments. The first is a starting value, the second is an array or object (object in our case), and the third is a function that will compare two items and return which item is the "largest".

/*
 * Get the day with the most number of items
 */
var topDay = Util.max({ key: "", value: 0 }, totalsByDay, function(a, b) {
    return (((a.value > b.value) ? a : (a.value === b.value) ? a : b));
});

Whew that was a lot of info. Again I must provide a disclaimer. I am a total n00b at this, so if you see discussion points here I'd love to hear about it. Mostly I am enjoying sharing the ride of learning new stuff!

Cheers, and happy coding!

Tags