Our markup

<!DOCTYPE html>
<html>
    <head>
    <title>Resume</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link type="text/css" href="css/bootstrap/css/bootstrap.css" media="all" rel="stylesheet" id="bootstrap"/>
    <style>
        body {
            font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
            font-size: 14px;
            line-height: 20px;
            color: rgb(51, 51, 51);
            padding-bottom:25px;
            padding-top:40px;
        }
        .navbar {
            z-index:1;
        }
        ul.nav li.dropdown {
            list-style:none;
            display: inline-table;
            padding:2px;
        }
        ul.dropdown-menu {
            display:none;
            position:relative;
            list-style:none;
            margin-left:20px;
        }
        .dropdown-menu a {
            z-index:100;
            text-decoration:none;
            color:black;
        }
        .dropdown-menu a:hover {
            font-weight:bold;
            text-decoration:underline;
        }
        .nav ul:after {
            content: '';
            display: block;
            clear: both;
        }
        .nav  {
            position:absolute;
        }
        li.dropdown:hover > ul.dropdown-menu {
            display:block;
            position:absolute;
            box-shadow:10px 10px 5px #888;
            width:120px;
            padding:8px;
            background:white;
            color:black;
            *z-index: 1000;
            zoom:1;
        }
        li.dropdown > ul.dropdown-menu :after {
            content: '';
            display: block;
            clear: both;
        }
        .dropdown-toggle {
            font-size:14px;
        }
        #footer {
            position:fixed;
            left:0px;
            bottom:0px;
            height:30px;
            width:100%;
            background:#999;
        }
        #div.report {
            margin-bottom:40px;
        }
    </style>
    <style type="text/css" id="resume-style">
            #report {
                margin-top:60px;
                margin-bottom:30px;
                margin-left:2px;
            }
            .control-dlg {
                border: solid 1px black;
                padding:6px;
            }
            .cell-border {
                border: solid 1px silver;
            }
     </style>
     <link rel="shortcut icon" href="favicon.png">
     <script type="text/javascript" src="js/local/resume.js"></script>
     <script type="text/javascript" src="js/local/dom_doc.js"></script>
     </head>
    <body>
        <div class="navbar navbar-inverse navbar-fixed-top">
            <div class="navbar-inner">
                <div class="container-fluid">
                    <div id="dlg-controls">
                        <div id="add-column-dlg" style="display:none;" class="control-dlg pull-right well">
                            <div style="float:right;"><i class="icon-remove-sign close-dlg-icon"></i></div>
                            <div>Add Column to Row</div>
                            <select id="add-column-to-row" style="width:45px;margin-top:8px;">
                                <option value="1">1</option>
                            </select>
                            <button id="add-column-to-row-button">Add</button>
                        </div>
                        <ul class="nav">
                            <li class="dropdown ">
                                <a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" href="#">Document <b class="caret"></b></a>
                                <ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
                                    <li role="presentation"><a role="menuitem" tabindex="-1" href="#" id="">Not implemented.</a></li>
                                </ul>
                            </li>
                            <li class="dropdown">
                                <a class="dropdown-toggle" id="drop5" role="button" data-toggle="dropdown" href="#">Row <b class="caret"></b></a>
                                <ul id="menu2" class="dropdown-menu" role="menu" aria-labelledby="drop5">
                                    <li role="presentation"><a role="menuitem" tabindex="-1" href="#" id="add-row-link">Add Row>/a></li>
                                </ul>
                            </li>
                            <li class="dropdown">
                                <a class="dropdown-toggle" id="drop5" role="button" data-toggle="dropdown" href="#">Column<b class="caret"></b></a>
                                <ul id="menu3" class="dropdown-menu" role="menu" aria-labelledby="drop5">
                                    <li role="presentation"><a role="menuitem" tabindex="-1" href="#" id="add-column-link">Add Column>/a></li>
                                </ul>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
        <div id ="report" class="container-fluid">
        </div>

        <footer id="footer">
            <div id="success-msg" class="alert alert-success control-dlg" style="display:none;"></div>
            <div id="error-msg" class="alert alert-error control-dlg" style="display:none;"></div>
            <div id="info-msg" class="alert alert-info control-dlg" style="display:none;"></div>
        </footer>
        <script>
            document.onreadystatechange = function () {
                if (document.readyState == "complete") {
                    g = new grid({"rowList":new rowList(),"columnList": new columnList()});
                    new AppRouter();
		}
            }
        </script>
    </body>
</html>

Our JavaScript (dom_doc.js) :


/*** This section contains utility methods. ***/
/** Closes any dialogs who's markup contrains the "control-dlg" css class.  **/
var closeDialogControls = function() {
    var controls = document.querySelectorAll(".control-dlg");
    var len = controls.length;
    var dlg = null;
    var style = null;
    for (var nIndex = 0; nIndex < len; nIndex++) {
        dlg = controls[nIndex];
        style = dlg.style;
        style.setProperty("display", "none");
    };
};
/** We have several controls which display the current list of "rows". Rows meaning the lines in our docucment.
*   This method gets the current list of rows from our "grid" object. Then creates a list of <option> elements.
*   Injects the option elements in to a <.select> control. The select control is identifed by a css "id" attribute.
*   The element_id method paramater identifes the <select> control.
*/
var reloadSelectControl = function(element_id) {
    var rl = g.getRowList();
    var row_selection_options = rl.renderLocationSelect();
    document.querySelector(element_id).innerHTML = row_selection_options;
};
/** Success Message Box -Display
*   Displays success messages in a consistent location (footer) and theme (color, etc)
*   Clears the status bar so that only the success message is displayed in the status bar.
* */
var showSuccessMessage = function(msg) {
    var successMsgBox = document.querySelector("#success-msg");
    var style = successMsgBox.style;
    style.setProperty("display", "block");
    successMsgBox.textContent = msg;
    hideErrorMessage();
    hideInfoMessage();
};
/** Success Message Box - Hide
*   Hides the success message box.
* */
var hideSuccessMessage = function() {
    var successMsgBox = document.querySelector("#success-msg");
    var style = successMsgBox.style;
    style.setProperty("display", "none");
};
/** Error Message Box - Display
*   Displays an error message in a consistent location and theme (color, etc.).
*   Clears the status bar so that only the error message is displayed in the status bar.
*/
var showErrorMessage = function(msg) {
    var errorMsgBox = document.querySelector("#error-msg");
    errorMsgBox.textContent = msg;
    var style = errorMsgBox.style;
    style.setProperty("display", "block");
    hideSuccessMessage();
    hideInfoMessage();
};

/** Error Message Box - Hide
*   Hides the error message box.
* */
var hideErrorMessage = function() {
    var errorMsgBox = document.querySelector("#error-msg");
    var style = errorMsgBox.style;
    style.setProperty("display", "none");
};

/** Information Message Box - Display
*   Displays an information message in a consistent location and theme (color, etc.).
*   Clears the status bar so that only the information message is displayed in the status bar.
*/
var showInfoMessage = function(msg) {
    var infoMsgBox = document.querySelector("#info-msg");
    var style = infoMsgBox.style;
    style.setProperty("display", "block");
    infoMsgBox.textContent = msg;
    hideSuccessMessage();
    hideErrorMessage();
};

/** Information Message Box - Hide
*   Hides the information message box.
* */
var hideInfoMessage = function() {
    var infoMsgBox = document.querySelector("#info-msg");
    var style = infoMsgBox.style;
    style.setProperty("display", "none");
};


/** Event Listeners.
 *  Adds listeners for clicks and select control changes.
 *  Note! Considering the relative small number of controls in this application. We decided to simply load
 *  the event listeners once and retain them during the application's life. As opposed to dynamically adding and
 *  removing listeners.
 *  @constructor
*/
var AppRouter  =  function() {
        /** Responds to click on menu item : Rows->Add Row */
        document.querySelector("#add-row-link").addEventListener("click", function(event) {
            event.preventDefault();
            closeDialogControls();
            g.addBlankRow();
            var table = g.render();
            report.innerHTML = table.outerHTML;
            showSuccessMessage("Row Added.");
        });
        /** Responds to click on menu item : Columns->Add Column (to specific row) */
        document.querySelector("#add-column-link").addEventListener("click", function(event) {
            event.preventDefault();
	    event.stopPropagation();
            closeDialogControls();
	     var rowList = g.getRowList();
	      var rowCount =  rowList.list.length;
	      if (rowCount && rowCount > 0) {
		reloadSelectControl("#add-column-to-row");
		var dlg = document.querySelector("#add-column-dlg");
		var style = dlg.style;
		style.display = "block";
	    } else {
		showErrorMessage("You Must Add a Row First. Unable to process requiest.");
	    }
        });
        /** General event handler for all of the application's dialogs.
        *   Note! So once a dialog is displayed, the EventManager services events like a button or link click.
        *
        * */
        var EventManager = function() {

            /** Services the Add Column (to row) Dialog */
            this.addColumnListener = function(event) {
                event.preventDefault();
                var row_id = document.querySelector("#add-column-to-row").value;
                g.addBlankColumnToRow(row_id);
                var report = document.getElementById("report");
                var table = g.render();
                report.innerHTML = table.outerHTML;
                showSuccessMessage("Column Added.");
            };

            /** Services dialogs that are rendered in the header. Listens for a click on the close-icon.
             *  Allows the user to cancel dialogs located in the header.
             */
            this.dlgListener = function(event) {
                var tagName = event.target.tagName;
                switch (tagName) {
                    case 'I':
                        closeDialogControls();
                    break;
                }
            };
            /** Selects the appropriate dom elements and adds event listeners to them.
             *
             */
            this.init = function() {
                   document.querySelector("#add-column-to-row-button").addEventListener("click", this.addColumnListener, true);
		   document.querySelector("#dlg-controls").addEventListener("click", this.dlgListener, true);
	    }
            this.init();
        };

        try {
            var eventManager = new EventManager();
        } catch (error) {
            console.log("Error installing EventManager: " + error);
       }
};

Our JavaScript (resume.js) :

/** Row entity
 *  @property id: unique identifier (key)
 *  @property location: top of document is row location 1. We increment
 *  the location value as rows are added below.
 *  @constructor
 */
var row = function(options) {
    this.init = function(options) {
        if (options && options.hasOwnProperty("id")) {
            this.id = options.id;
        }
        if (options && options.hasOwnProperty("location")) {
         this.location = options.location;
        }
    };
    this.setId = function(id) {
        this.id = id;
    };
    this.getId = function() {
        return this.id;
    };
    this.setLocation = function(location) {
        this.location = location;
    };
    this.getLocation = function() {
        return this.location;
    };
    this.init(options);
};
/** Row Collection
 *  A simple collection of row entities.
 *  @constructor
 *  @class
 */
var rowList = function() {
    this.list = [];
    /** Add new row entity to the collection */
    this.addRow = function(row) {
        this.list.push(row);
    };
    /** Get row entity by id (key) */
    this.getRow= function(row_id) {
        var len = this.list.length;
        var row = null;
        for (var nIndex=0; nIndex < len; nIndex++) {
            row = this.list[nIndex];
            if (row_id == row.getId()) {
                break;
            }
        }
        return row;
    };
    /** Get the maximum row location value
     *  we iterate through the list because the order is not guaranteed to
     *  be in either ascending or descending location order.
     */
    this.getLastRowLocation = function() {
        var len = this.list.length;
        var last =0;
        var elem = null;
        for (var nIndex =0; nIndex < len; nIndex++) {
            elem = this.list[nIndex];
            if (elem.getLocation()> last) {
                last = elem.getLocation();
            }
        }
        return last;
    };

    /** Creates the option list for the add-column-to-row select control
    *   Sets the option value to row.id and option text to row.location.
    *   Note. We sort the list by location value
     */
    this.renderLocationSelect= function() {
        this.list.sort(function(a,b){
            return a.getLocation() - b.getLocation();
        });
        var strCache = [];
        var len = this.list.length;
        var elem = null;
        var op_start = "";
        for (var nIndex= 0; nIndex < len; nIndex++) {
            elem = this.list[nIndex];
            strCache.push(op_start+elem.getId()+op_end+elem.getLocation()+op_close);
        }
        return strCache.join();
    };
};

/** Column entity.
 *  @property id: unique identifier (key)
 *  @property content: text or html. Holds the actual document cell value.
 *
 *  Note! We originally thought a column might be rendered in several locations.
 *  Thus we set the column's location in the row_column entity. Enabling a one to many locations
 *  relationship for columns.
 *  @constructor
 */
var column = function(options) {
    this.init= function(options) {
        if (options && options.hasOwnProperty("id")) {
            this.id = options.id;
        }
        if (options && options.hasOwnProperty("content")) {
            this.content = options.content;
        }
    };
    this.getId = function() {
        return this.id;
    };
    this.setId = function(id) {
        this.id = id;
    };
    this.getContent = function() {
        return this.content;
    };
    this.setContent = function(content) {
        this.content = content;
    };
    this.init(options);
};
/** Collection of column entities
 *
 *  @constructor
 */
var columnList = function() {
    this.list = [];
    /** Add column entity to the collection */
    this.addColumn= function(col) {
        this.list.push(col);
    };

    /** Get column entity by id value (key). */
    this.getColumn= function(column_id) {
        var len = this.list.length;
        var col = null;
        for (var nIndex =0; nIndex < len; nIndex++) {
           col = this.list[nIndex];
           if (column_id == col.getId()) {
                break;
           }
        }
        return col;
    };
};
/** Cell entity.
 *  Note! This should be refactored to read "cell" instead of row_column.
 *  @property id: unique indentifier (key)
 *  @property row_id: foreign key reference to related row entity
 *  @property colum_id: foreign key reference to related column entity
 *  @property location: defines left to right location within the containing row. The left most
 *  cell is defined as location 1. location values are incremented as cells are added to the
 *  right (within a specific row).
 *  css_class: value should be a css class definition located in any of the document's css files.
 *  Note! this application makes heavy use of Twitter Bootstap. Thus, the default css_class value is span1.
 *  There are 12 span values to a row. See the Twitter Boostrap docs for more detailed info.
 *  @constructor
 */
var row_column = function(options) {
    this.init = function(options) {
        if (options && options.hasOwnProperty("id")) {
            this.id = options.id;
        }
        if (options && options.hasOwnProperty("row_id")) {
            this.row_id = options.row_id;
        }
        if (options && options.hasOwnProperty("column_id")) {
            this.column_id = options.column_id;
        }
        if (options && options.hasOwnProperty("location")) {
            this.location = options.location;
        }
        if (options && options.hasOwnProperty("css_class")) {
            this.css_class = options.css_class;
        }
    };
    this.getId = function() {
        return this.id;
    };
    this.setId = function(id) {
        this.id = id;
    };
    this.getRowId=function() {
        return this.row_id;
    };
    this.setRowId = function(id) {
        this.row_id = row_id;
    };
    this.getColumnId = function() {
        return this.column_id;
    };
    this.setColumnId=function(column_id){
        this.column_id = column_id;
    };
    this.getLocation = function() {
        return this.location;
    };
    this.setLocation = function(location) {
        this.location = location;
    };
    this.getCssClass=function() {
        return this.css_class;
    }
    this.setCssClass=function(css_class) {
        this.css_class = css_class;
    }
    this.init(options);
};
/** Grid represents the resume document.
 *  Similar to a "worksheet". Puts the row,columns and cells together
 *  to form the resume document.
 *  @property list: collection of row_column entitys (cells)
 *  @property rowList: instance or row_list
 *  @property columnList: instance of column_list
 *  @property useBorders: boolean determines outline view on or off
 *  @property useLink: boolean determine whether cell edit links are on or off
 *  @property title: string stores the document's title value. Defaults to "Resume", the user
 *  has the option to set a custom tile (E.G. their name).
 *  @class
 */
var grid = function(options) {
    this.list = [];
    this.init = function(options) {
        if (options && options.hasOwnProperty("list")) {
            this.list = options.list;
        }

        if (options && options.hasOwnProperty("rowList")) {
            this.rowList = options.rowList;
        }

        if (options && options.hasOwnProperty("columnList")) {
            this.columnList = options.columnList;
        }

        if (options && options.hasOwnProperty("useBorders")) {
            this.userBorders = options.userBorders;
        } else {
            this.userBorders = false; // default
        }

        if (options && options.hasOwnProperty("useLinks")) {
            this.useLinks = options.useLinks;
            inks;
        } else {
            this.useLinks = true; // default
        }

        if (options && options.hasOwnProperty("title")) {
            this.title = options.title;
        } else {
            this.title = 'Resume'; // default
        }
    };
    this.setTitle=function(title){
        this.title = title;
    };
    this.getTitle=function() {
        return this.title;
    };
    this.setUseLinks=function(useLinks) {
        this.useLinks = useLinks;
    };
    this.getUseLinks=function(){
        return this.useLinks;
    };
    this.setUseBorders=function(userBorders) {
        this.userBorders = userBorders;
    };
    this.getUseBorders = function() {
        return this.userBorders;
    };
    this.toggleBorders=function() {
        if (this.userBorders === true) {
            this.userBorders = false;
        } else {
            this.userBorders= true;
        }
    };
    /** Add row_column cell entity to the list collection */
    this.addRowColumn= function(row_column) {
        this.list.push(row_column);
    };

    /** Get a row_column cell entity */
    this.getRowColumn = function(row_id, column_id){
        var row_column = null;
        var len = this.list.length;
        for (var nIndex=0;nIndex < len; nIndex++) {
            row_column = this.list[nIndex];
            if (row_id == row_column.getRowId() && column_id == row_column.getColumnId()  ){
                break;
            }
        }
        return row_column;
    };
    /** Get this instance of rowList **/
    this.getRowList = function() {
        return this.rowList;
    }
    /** Get this instance of columnList. */
    this.getColumnList = function() {
        return this.columnList;
    };

    /** Gets the greatest value for column location in the set of row columns identified by row_id.
    *   We use this method after a user chooses to delete a column. If there are no columns remaining in the
    *   row, we remove the row also.
    *   @param row_id identifies the row_column stored in this row_column collect list. The row_column is identified by
    *   where row_column.row_id field equals the row_id parameter value.
    * */
    this.getLastColumnLocation= function(row_id) {
        var last = 0;
        row_id = parseInt(row_id);
        var len = this.list.length;
        var elem  = null;
        for (var nIndex=0; nIndex < len; nIndex++) {
            elem = this.list[nIndex];
            if (elem.getRowId()  == row_id && elem.getLocation() > last) {
                last = elem.getLocation();
            }
        }
        return last;
    };
    /** Adds a new row. Internally we add a new row entity, column entity and a row_column cell */
    this.addBlankRow = function() {
        // allows the user to add a logical row -- internally a new
        // row, column (content filled with default/placeholder) and row_column
        var self = this;
        var rows = this.getRowList();
        var last_row_location  =  rows.getLastRowLocation();
        last_row_location +=1;
        var id = new Date().getTime();
        this.rowList.addRow( new row( {"id":id,"location":last_row_location}));
        this.columnList.addColumn( new column({"id":id,"content":"Blank Column"}));
        this.addRowColumn(new row_column({"row_id":id,"column_id":id, "id":id,"location":1,"css_class":"span2"}));
    };
    /** Add a new column to the row identified by row_id. Internally we add a new column entity and row_column cell to relate the
     *  column to the row.
    */
     this.addBlankColumnToRow=function(row_id) {
        var id = new Date().getTime();
        var last_column_location = this.getLastColumnLocation(row_id);
        last_column_location +=1;
        this.columnList.addColumn( new column({"id":id,"content":"Blank Column"}));
        this.addRowColumn(new row_column({"row_id":row_id,"column_id":id, "id":id,"location":last_column_location,"css_class":"span2"}));
        this.sortByRowLocation();

    };
    /** Sorts this row_column collection list by row location in ascending order
    *
    */
    this.sortByRowLocation=function() {
         var row_a, row_b;
         var row_result;
         var result = null;
         var self = this;
         this.list.sort(function(a,b){
            row_a = self.rowList.getRow(a.getRowId());
            row_b = self.rowList.getRow(b.getRowId());
            row_result = row_a.getLocation() - row_b.getLocation();
            if (row_result === 0) {
                result = a.getLocation() - b.getLocation();
            } else {
                result = row_result;
            }
            return result;
        });
     };

    /** Creates a document cell. The render_links_arg parameter determines whether
     *  we add a parent link around the cell. A parent link allows the user to click
     *  on the cell to invoke the edit cell dialog.
     *
     *  If the grid's useBorders value is set to true, this method adds additional editing
     *  hints to the display. This method adds a border to each cell as well as a clickable
     *  icon. The "edit icon" allows the user to select cells which have either no content or
     *  a non-displayable content such as  .
     *
     *  Npte! This method logically renders the user's resume document. Thus, this method is
     *  used by the "export to HTML" function.
     *
     *  @param render_links_arg sets whether "edit links" are rendered around each document cell. Note!
     *  these links are only visible for cells that contain content. To allow the user to edit cells whose content
     *  is empty, set the grid.userBorders property to true.
     */
    this.render = function(render_links_arg) {
        var render_links = (render_links_arg === false) ? false : true;
        var table = document.createElement("div");
        table.setAttribute("class","container-fluid");
        var headerfrag = document.createDocumentFragment();
        var tr = document.createElement("div");
        tr.setAttribute("class","row-fluid");
        var docfrag = document.createDocumentFragment();
        var len = this.list.length;
        var row_column = null;
        var row = null;
        var column = null;
        var current_row_id  = null;
        var first_row = this.list[0];
        var td = null;
        var link = null;

        var edit_icon_div = document.createElement("div");
        var icon_elem = document.createElement("i");
        edit_icon_div.style.setProperty("float","right");
        icon_elem.classList.add('icon-pencil');
        edit_icon_div.appendChild(icon_elem);
        current_row_id = first_row.getRowId();
        for (var nIndex =0; nIndex < len; nIndex++) {
            row_column = this.list[nIndex];
            row = this.rowList.getRow(row_column.getRowId());
            column = this.columnList.getColumn(row_column.getColumnId());
            if (current_row_id !== row.getId()) {
                docfrag.appendChild(tr);
                current_row_id = row.getId();
                tr = document.createElement("div");
                tr.setAttribute("class","row-fluid");
            }
            td = document.createElement("div");
            td.setAttribute("cell","true");
            td.setAttribute("class",row_column.getCssClass());
            if (render_links) {
                link = document.createElement("a");
                link.setAttribute("href", "#op/row/"+current_row_id+"/column/"+column.getId());
                link.setAttribute("class","internal");
                link.appendChild(edit_icon_div.cloneNode(true));
                td.appendChild(link);
             }
            td.innerHTML += column.getContent();
            if (this.userBorders === true) {
                td.classList.add("cell-border");
            }
            tr.appendChild(td);
        }
        docfrag.appendChild(tr);
        table.appendChild(docfrag);
        return table;
    };
    this.init(options);
};
- Home