Note, this articles is part of a series (here). In the previous installment we implemented data persistence. In this installment we complete our "must have" functions and thus, our initial release.

Logical Functions
These are the "must have" functions added for this installment.

Other Enhancements
These are changes which enhance current functionality.

*Note. I added an option to display cell borders to help the resume author visualize the rows and columns. The cells borders are not displayed in the published document. The cell borders are only displayed during the edit process (when the user toggles cell border on).

Implementation
The issue I struggled with the most was "how to get the Twitter Bootstrap style sheets in to our published document".

Normally, the Twitter Bootstrap CSS rules are stored in an external file (or remote URL/a file located on a remote web server ). Your document then references the Bootstrap CSS rules via a <link> tag (placed in the document head section). Separating a large set of CSS rules in an external file/remote URL, is typical way to organize your document's data.

Our use case is a little different. Our goal is to produce a single html document the user can email to someone.

If the receiver of the email wants to view the resume while off-line, then a remote URL reference is not going to work. For example, someone gets on an elevator which blocks wireless signals (I.E. the person's phone is no longer connected to the internet). We want that person on the elevator to be able to view the resume with the intended layout. If our resume document references a remote URL located across the internet, our elevator rider's mobile phone won't be able to reference the CSS rules. The result is, our document is not rendered as intended (it's missing the CSS rules). Thus, a reference to an a remote URL is not going to work.

Referencing a local external file, also conflicts with our use case. Our resume author would have to email 2 files. The 2 files would need to always be in the same location (on the recipient's computer/mobile device). That scenario is way too unpredictable, it's just not practical. Therefore, referencing a second, local external file is not an option.

Thus, I decided to copy the Twitter Bootstrap CSS rules in to the published resume html document (I.E. in the html head section).

We still had one more issue. How do we get the Twitter Bootstrap CSS rules in to our published resume document. This is actually more of a user experience (UX) issue. We don't want the resume author, to have to locate, and then select, the proper CSS file. We want to limit the number of steps the resume author has to take.

The challenge requires a little explanation.

Our program can not simply open a file from the local file system. The user must locate and select a file, using the browser's file dialog. The program can not select the file. The user must select the file.

To complicate matters, our program can not set the default location of the file dialog. In other words, the file dialog could start anywhere on the file system.

Remember, our resume author at the end of the publishing process, is going to have to select a destination location and optionally a destination file name. If in addition, the resume author has to select the CSS files, that adds up to, way too many steps.

Thus, I applied the same solution to our main program listing. Instead of storing the Twitter Bootstrap CSS rules in an external file, I store them in the head section of our webapp.html file (our main program file listing). That eliminates any file dialog requirements to obtain the CSS rules. A document can access it's own contents.

Thus, our application's file organization is little unusual, but it works. Sometimes, just getting it to work is good enough :)

Here is the main logic which creates the html document in memory (AKA blob). We then create a link to the html blob.

var downloadFile = function() {
    var container = document.querySelector('#container');
    var output = container.querySelector('output');
    var MIME_TYPE = 'text/html';

    window.URL = window.webkitURL || window.URL;

    var prevLink = output.querySelector('a');
    if (prevLink) {
        window.URL.revokeObjectURL(prevLink.href);
        output.innerHTML = '';
    }

    // construct our new document
    var charset = '<meta charset="utf-8"></meta>\n';
    var viewPort = '<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">\n';
    var doc = document.createElement("html");
    var body = document.createElement('body');
    var head = document.createElement("head");
    var title = document.createElement("title");
    title.innerText = g.getTitle();
    head.innerHTML = title.outerHTML;
    head.innerHTML += charset;
    head.innerHTML += viewPort;
    // clone the style or we loose of formatting in our application
    var style = document.querySelector('#bootstrap').cloneNode(true);
    head.appendChild(style);
    var resume_style = document.querySelector("#resume-style");
    head.appendChild(resume_style);
    var report = g.render(false);
    var docTypeStr = "<!DOCTYPE html>\n";
    body.innerHTML = report.outerHTML;
    doc.appendChild(head);
    doc.appendChild(body);
    var bb = new Blob([docTypeStr, doc.outerHTML], {type: MIME_TYPE});
    var a = document.createElement('a');
    a.download = container.querySelector('input[type="text"]').value;
    a.href = window.URL.createObjectURL(bb);
    a.textContent = 'Download ready';

    a.dataset.downloadurl = [MIME_TYPE, a.download, a.href].join(':');

    output.appendChild(a);

    a.onclick = function(e) {
        if ('disabled' in this.dataset) {
            return false;
        }
        cleanUp(this);
    };
};

Adding your own CSS rules
In addition to the Twitter Bootstrap CSS rules, I added a <style> set identified by "id=resume-style". Any rules inside of this tag set are added to the published resume document. Thus, folks can add there own CSS rules to the "resume-style" tag set, for further customization.

Templates
Many folks find it easier to start a document with a template (versus a blank document). A template, meaning, a boiler plate document. Most word processor's provide templates.

Our application does not have the sophisticated editing experience of full blown word processor. Using our application, you have to explicitly issue commands like "add row", or "add column". If you were using a word processor, you would simply start typing.

While trying to add new content to a template, I quickly realized we needed a "move row" function. I found myself adding a few rows to the bottom of the template, then moving the rows up. Or, if the template had a footer section, I found myself moving the footer section down.

Implementing the "Move Row" function took a little planning. The row locations values, are a little counter intuitive. The words "highest" and "lowest" confuse the issue. The row at the bottom of the document contains the greatest location value. For example, a document of 20 rows. The row located at the bottom has a location value of 20. The row located at the top of the document has a location value of 1. The bottom rows have "higher" location values. The top rows have "lower" location values.

I've included an explanation of the implementation in the source code comments. The following method is a member function of our "grid" entity.

/** Resets row locations.
    *   Logically, the row identified by origin_row_id is inserted after the row
    *   identified by destination_row_id. We determine the direction of the change.
    *   If we are moving a row up, any rows which currently are above (have a row position
    *   who's location value is greater than the origin row's location value), have their locations
    *   reduced. In other words, those rows are pushed down.
    *
    *   If the move direction is down. Then any rows below (rows who's location values are less than the location
    *   value of the origin row are incremented) are pushed up.
    *
    *   If the user selects the same row for both origin and destination, this method performs no action. Simply returns.
    * */
    this.changeRowPosition=function(origin_row_id, destination_row_id){
        var origin_row = this.rowList.getRow(origin_row_id);
        var destination_row = this.rowList.getRow(destination_row_id);
        var o_location = origin_row.getLocation();
        var d_location = destination_row.getLocation();
        if ( o_location == d_location) {
            return;
        }

        var direction = (o_location > d_location) ? 'down' : 'up';
        var elem = null;
        var len = this.rowList.list.length;
        var elem_loc = null;
        switch(direction) {
            case 'down':
                for (var nIndex=0; nIndex < len; nIndex++){
                    elem = this.rowList.list[nIndex];
                    elem_loc = elem.getLocation();
                    if (elem_loc > d_location && elem_loc < o_location) {
                        elem_loc +=1;
                        elem.setLocation(elem_loc);
                    }
                    if (elem_loc == o_location) {
                        // if we found the destination element
                        destination_row.setLocation((d_location +1));
                        origin_row.setLocation(d_location);
                        this.sortByRowLocation();
                        break;
                    }
                }
            break;
            case 'up':
                for (var nIndex=0; nIndex < len; nIndex++){
                    elem = this.rowList.list[nIndex];
                    elem_loc = elem.getLocation();
                    if (elem_loc > o_location && elem_loc < d_location) {
                        elem_loc -=1;
                        elem.setLocation(elem_loc);
                    }
                    if (elem_loc == d_location) {
                        // if we found the destination element
                        destination_row.setLocation((d_location -1));
                        origin_row.setLocation(d_location);
                        this.sortByRowLocation();
                        break;
                    }
                }
            break;
        }
    };

I tested the application by generating a few template documents. I'll use those in our distribution kit. It's easier for folks to modify an existing document rather than build one from scratch.

All of the latest source code is located in the Git Hub repository here.

About the Author:
Lorin M Klugman is an experienced developer focused on new technology.
Open for work (contract or hire).
Drop Lorin a note. Click here for address.
Please no recruiters :)
- Home