(function() { var root = this; var PD = root.PersonalDisplay = {}; PD.version = "1.4" PD.hasHTML5LocalStorage = function() { try { return 'localStorage' in window && window['localStorage'] !== null; } catch (e) { return false; } } /// ## Models PD.TimelineMarkModel = Backbone.Model.extend({ initialize: function() { _.bindAll(this, "equals"); }, equals: function(that) { return this.id == that.id; } }); PD.GTDEntryModel = Backbone.Model.extend({ initialize: function() { _.bindAll(this, "equals"); }, equals: function(that) { return this.id == that.id; } }); // ## Views PD.CurrentActivityView = Backbone.View.extend({ el: $("#current-task"), initialize: function() { _.bindAll(this, "render"); this.model.on('change', this.render, this); }, render: function() { this.$el.find(".task").text(this.model.get("mark")); this.$el.find(".task-notes").text(this.model.get("notes")); return this; } }); PD.GTDNextActionView = Backbone.View.extend({ className: "next-action", tagName: "div", ignoredProperties: ["action", "details", "id"], initialize: function() { _.bindAll(this, "render"); $("#priorities").append(this.el); this.model.on('change', this.render, this); }, render: function() { var elements = []; // Look for the "action" property first var actionEl = $(document.createElement("span")) .addClass("action") actionEl.text(this.model.get("action").toString()); elements.push(actionEl); // Add span elements for each of the other attributes. _.each(this.model.attributes, function(val, key) { if (!_.contains(this.ignoredProperties, key)) { var el = $(document.createElement("span")) .addClass(key.toString()); el.text(val); elements.push(el); } }, this); // Finally, look for the "details" property if (this.model.get("details")) { var detailEl = $(document.createElement("span")) .addClass("details"); detailEl.text(this.model.get("details").toString()); elements.push(detailEl); } // Clear the old data and add our new elements in order this.$el.empty(); _.each(elements, function(el) { this.$el.append(el); }, this); return this; } }); PD.GTDNextActionCollection = Backbone.Collection.extend({ model: PD.GTDEntryModel, initialize: function() { _.bindAll(this, "addNextActionView", "removeNextActionView"); this.views = {}; this.on('add', this.addNextActionView); this.on('remove', this.removeNextActionView); }, addNextActionView: function(entryModel) { var view = new PD.GTDNextActionView({model: entryModel}); view.render(); this.views[entryModel.get("id")] = view; }, removeNextActionView: function(entryModel) { var view = this.views[entryModel.get("id")]; view.$el.remove(); } }); PD.ConfigDialog = Backbone.View.extend({ el: $("#config-dialog"), events: { "blur .timestamper-config .password" : "tsLogin", "blur .gtd-config .password" : "gtdLogin", "change .gtd-config .category" : "addCategory", "click .remove-button" : "removeCategory", "click .save-button" : "saveAndClose" }, initialize: function() { _.bindAll(this, "show", "hide", "tsLogin", "gtdLogin", "loadTsData", "loadGtdData", "addCategory", "makeCategoryItem", "removeCategory", "saveAndClose"); }, show: function() { var $tsSection = this.$el.find(".timestamper-config"); var $gtdSection = this.$el.find(".gtd-config"); // Load TimeStamper configuration values. if (PD.tsCfg) { $tsSection.find(".username").val(PD.tsCfg.username); $tsSection.find(".password").val(PD.tsCfg.password); $tsSection.find(".host").val(PD.tsCfg.host); if (PD.tsAuth) { this.loadTsData(PD.tsCfg.host, PD.tsCfg.username); } this.$el.find('.timeline').val(PD.tsCfg.timelineId); } // Or suggest a default server. else { $tsSection.find(".host").val("timestamper.jdb-labs.com"); } // Load GTD configuration values. if (PD.gtdCfg) { $gtdSection.find(".username").val(PD.gtdCfg.username); $gtdSection.find(".password").val(PD.gtdCfg.password); $gtdSection.find(".host").val(PD.gtdCfg.host); if (PD.gtdAuth) { this.loadGtdData(PD.gtdCfg.host); } // Create the items for the selected categories $(".category-name").parent().remove(); _.forEach(PD.gtdCfg.categories, this.makeCategoryItem); } this.$el.find('.refresh-period').val( PD.refreshPeriod ? PD.refreshPeriod / 1000 : 15); this.$el.fadeIn(); }, hide: function() { this.$el.fadeOut(); }, tsLogin: function() { var username = this.$el.find(".timestamper-config .username").val(); var password = this.$el.find(".timestamper-config .password").val(); var host = this.$el.find(".timestamper-config .host").val(); if (!PD.tsCfg) { PD.tsCfg = {}; } // Hide the configuration dialog. this.$el.find(".wait-overlay span").text("Connecting to " + host); this.$el.find(".wait-overlay").fadeIn(); // Try to log in to the TimeStamper service. $.ajax({ url: "https://" + host + "/ts_api/login", xhrFields: { withCredentials: true }, processData: false, type: 'POST', async: false, data: JSON.stringify( {"username": username, "password": password}), error: function(jqXHR, textStatus, error) { if (jqXHR.status == 401) { $(".validate-tips") .text("Invalid username/password combination for " + "the TimeStamper service."); } else { $(".validate-tips").text("There was an error " + "trying to log into the TimeStamper service: " + error); } PD.tsAuth = false; }, success: function(data, textStatus, jqXHR) { PD.tsAuth = true; $(".validate-tips").text(""); // Load the user's timelines. PD.configDialog.loadTsData(host, username); } }); // Success or failure we hide the wait overlay. this.$el.find(".wait-overlay").fadeOut(); }, gtdLogin: function() { var username = this.$el.find(".gtd-config .username").val(); var password = this.$el.find(".gtd-config .password").val(); var host = this.$el.find(".gtd-config .host").val(); if (!PD.gtdCfg) { PD.gtdCfg = {}; } // Hide the configuration dialog. this.$el.find(".wait-overlay span").text("Connecting to " + host); this.$el.find(".wait-overlay").fadeIn(); // Try to log in to the GTD service. $.ajax({ url: "http://" + host + "/gtd/login", xhrFields: { withCredentials: true }, processData: false, type: 'POST', async: false, data: JSON.stringify( {"username": username, "password": password}), error: function(jqXHR, textStatus, error) { if (jqXHR.status == 401) { $(".validate-tips") .text("Invalid username/password combination for " + "the Getting Things Done service."); } else { $(".validate-tips").text("There was an error " + "trying to log into the Getting Things Done service: " + error); } PD.gtdAuth = false; }, success: function(data, textStatus, jqXHR) { PD.gtdAuth = true; $(".validate-tips").text(""); PD.configDialog.loadGtdData(host); } }); this.$el.find(".wait-overlay").fadeOut(); }, loadTsData: function(host, username) { // (Re)load the user's timelines. PD.tsCfg.timelines = JSON.parse($.ajax({ url: 'https://' + host + '/ts_api/timelines/' + username, xhrFields: { withCredentials: true }, async: false}).responseText); // Populate the available timelines list. var $timelineSelectEl = this.$el.find(".timestamper-config .timeline"); $timelineSelectEl.empty(); _.forEach(PD.tsCfg.timelines, function(timeline) { var $optionEl = $(document.createElement("option")); $optionEl.attr("value", timeline.id); $optionEl.text(timeline.description); $timelineSelectEl.append($optionEl); }); }, loadGtdData: function(host) { // Load the user's contexts PD.gtdCfg.contexts = JSON.parse($.ajax({ url: 'http://' + host + '/gtd/contexts', xhrFields: { withCredentials: true }, async: false }).responseText); // Load the user's projects PD.gtdCfg.projects = JSON.parse($.ajax({ url: 'http://' + host + '/gtd/projects', xhrFields: { withCredentials: true }, async: false }).responseText); // Populate the available contexts and projects drop-down. var $categorySelectEl = $(".gtd-config .category") $categorySelectEl.empty(); $categorySelectEl.append( ""); _.forEach(PD.gtdCfg.contexts.concat(PD.gtdCfg.projects), function(category) { var $optionEl = $(document.createElement("option")); $optionEl.attr("value", category.id); $optionEl.text(category.id); $categorySelectEl.append($optionEl); }); $categorySelectEl[0].selectedIndex = 0; }, makeCategoryItem: function(catName) { var $liEl = $( "
  • remove" + "
  • "); $liEl.find('.category-name').text(catName); this.$el.find(".gtd-config ul").append($liEl); }, addCategory: function(source) { var selectEl = source.target; var $selectEl = $(selectEl); if (selectEl.selectedIndex == 0) { return; } this.makeCategoryItem($selectEl.val()); selectEl.selectedIndex = 0; }, removeCategory: function(source) { $(source.target).parent().remove(); }, saveAndClose: function() { if (!PD.tsCfg) { PD.tsCfg = {}; } if (!PD.gtdCfg) { PD.gtdCfg = {}; } // Save TimeStamper configuration. var $tsEl = this.$el.find(".timestamper-config"); PD.tsCfg.host = $tsEl.find(".host").val(); PD.tsCfg.username = $tsEl.find(".username").val(); PD.tsCfg.password = $tsEl.find(".password").val(); PD.tsCfg.timelineId = $tsEl.find(".timeline").val(); // Save Getting Things Done configuration. var $gtdEl = this.$el.find(".gtd-config"); PD.gtdCfg.host = $gtdEl.find(".host").val(); PD.gtdCfg.username = $gtdEl.find(".username").val(); PD.gtdCfg.password = $gtdEl.find(".password").val(); PD.gtdCfg.categories = _.map( this.$el.find(".category-name"), function(span) { return $(span).text(); }); // Save global data PD.refreshPeriod = parseInt(this.$el.find(".refresh-period").val()) * 1000; if (PD.hasHTML5LocalStorage()) { localStorage.setItem("tsCfg", JSON.stringify(PD.tsCfg)); localStorage.setItem("gtdCfg", JSON.stringify(PD.gtdCfg)); localStorage.setItem("refreshPeriod", JSON.stringify(PD.refreshPeriod)); } this.hide(); } }); PD.Main = Backbone.View.extend({ el: $("body"), events: { "click a.refresh" : "refresh", "click a.pause-continue" : "toggleSync", "click a.show-config" : "showConfig", "click a.toggle-fullscreen" : "toggleFullscreen" }, initialize: function() { _.bindAll(this, "refresh", "toggleSync"); // Create our config dialog view. PD.configDialog = new PD.ConfigDialog(); // Create our initial models and views. PD.currentActivityModel = new PD.TimelineMarkModel({}); PD.currentActivityView = new PD.CurrentActivityView( {model: PD.currentActivityModel}) // Test for localStorage support if (!PD.hasHTML5LocalStorage()) { alert("Your browser does not support HTML5 localStorage." + "Without this I cannot store your preferences."); PD.configDialog.show(); } else { PD.tsCfg = JSON.parse(localStorage.getItem('tsCfg')); PD.gtdCfg = JSON.parse(localStorage.getItem('gtdCfg')); PD.refreshPeriod = JSON.parse( localStorage.getItem('refreshPeriod')); } PD.gtdNextActionCollection = new PD.GTDNextActionCollection(); // Perform the initial refresh. this.refresh(); // Schedule future refreshes. PD.refreshIntervalId = setInterval(this.refresh, PD.refreshPeriod ? PD.refreshPeriod : 15000); }, refresh: function(evt) { // If the dialog is still open we skip this sync to give the user // a chance to finish configuration. if ($("#config-dialog").is(":visible")) { return; } // Otherwise, if we do not have configuration information, open the // dialog so the user can enter it. if (!(PD.tsCfg && PD.gtdCfg)) { PD.configDialog.show(); return; } // Check that we are authenticated to the services we need. Try to // authenticate if we are not. if (!PD.tsAuth) { $.ajax({ url: "https://" + PD.tsCfg.host + "/ts_api/login", xhrFields: { withCredentials: true }, processData: false, type: "POST", async: false, data: JSON.stringify( { "username": PD.tsCfg.username, "password": PD.tsCfg.password }), error: function(jqXHR, textStatus, error) { // TODO: Handle error. PD.tsAuth=false; alert("Unable to authenticate to the TimeStamper " + "service: " + error); PD.configDialog.show(); }, success: function(data, textStatus, jqXHR) { PD.tsAuth = true; }}); } if (!PD.gtdAuth) { $.ajax({ url: "http://" + PD.gtdCfg.host + "/gtd/login", xhrFields: { withCredentials: true }, processData: false, type: "POST", async: false, data: JSON.stringify( { "username": PD.gtdCfg.username, "password": PD.gtdCfg.password }), error: function(jqXHR, textStatus, error) { // TODO: Handle error. PD.gtdAuth=false; alert("Unable to authenticate to the GTD service: " + error); PD.configDialog.show(); }, success: function(data, textStatus, jqXHR) { PD.gtdAuth = true; }}); } // Check that we have successfully authenticated to both services. // If we are not, we will skip this refresh. if (!(PD.tsAuth && PD.gtdAuth)) { return; } // Get the latest timestamp from the TimeStamper service. $.ajax({ url: "https://" + PD.tsCfg.host + "/ts_api/entries/" + PD.tsCfg.username + "/" + PD.tsCfg.timelineId, xhrFields: { withCredentials: true }, data: {"order": "asc" }, dataType: 'json', type: 'GET', async: true, error: function(jqXHR, textStatus, errorText) { if (jqXHR.status == 401) { PD.tsAuth = false; } else { alert("Unable to retrieve current timestamp: " + errorText); PD.configDialog.show(); } }, success: function(data, textStatus, jqXHR) { PD.currentActivityModel.set(data[0]); } }); // Get the list of GTD entries for each of our categories. var categories = _.reduce( PD.gtdCfg.categories, function(acc, cat) { return acc ? acc + "," + cat : cat; }, ""); $.ajax({ url: "http://" + PD.gtdCfg.host + "/gtd/next-actions/" + categories, xhrFields: { withCredentials: true }, dataType: 'json', type: 'GET', async: true, error: function(jqXHR, textStatus, errorText) { if (jqXHR.status == 401) { PD.gtdAtuh = false; } else if (jqXHR.status == 500) { return; } else { alert("Unable to retrieve next actions: " + errorText); PD.configDialog.show(); } }, success: function(data, textStatus, jqXHR) { var collection = PD.gtdNextActionCollection; // Add all the retrieved items to the collection. _.forEach(data, function(actionAttr) { // Try to find this entry in out collection. var model = collection.get(actionAttr.id); // Update it if found if (model) { model.set(actionAttr); } // Insert a new model if not found. else { collection.add( new PD.GTDEntryModel(actionAttr)); }}); // Look through our collection for entries that are no // longer in our retrieved data and remove them. collection.forEach(function(model) { if (!_.any(data, model.equals)) { collection.remove(model); }}); } }); if (evt) evt.preventDefault(); }, showConfig: function() { PD.configDialog.show(); }, toggleFullscreen: function(evt) { var $button = $(evt.target); if ($button.text() == "Go Fullscreen") { if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.documentElement.webkitRequestFullscreen) { document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else { alert ("Not supported by this browser."); } $button.text("Leave Fullscreen"); } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullscreen) { document.documentElement.mozCancelFullscreen(); } else { alert ("Not supported by this browser."); } $button.text("Go Fullscreen"); } }, toggleSync: function(evt) { if (PD.refreshIntervalId == null) { PD.refreshIntervalId = setInterval(this.refresh, PD.refreshPeriod ? PD.refreshPeriod : 15000); $('.pause-continue').text('Pause Monitoring'); } else { clearInterval(PD.refreshIntervalId); PD.refreshIntervalId = null; $('.pause-continue').text('Resume Monitoring'); } if (evt) evt.preventDefault(); } }); PD.main = new PD.Main(); }).call(this);