diff --git a/bundle.js b/bundle.js index 74b48b0..76a3c84 100644 --- a/bundle.js +++ b/bundle.js @@ -45,43 +45,39 @@ /***/ function(module, exports, __webpack_require__) { /*global app, $on */ + 'use strict'; __webpack_require__(1); __webpack_require__(5); - //require('todomvc-common'); __webpack_require__(7); __webpack_require__(8); __webpack_require__(9); __webpack_require__(10); __webpack_require__(11); __webpack_require__(12); - (function () { - 'use strict'; + /** + * Sets up a brand new Todo list. + * + * @param {string} name The name of your new to do list. + */ + function Todo(name) { + this.storage = new app.Store(name); + this.model = new app.Model(this.storage); + this.template = new app.Template(); + this.view = new app.View(this.template); + this.controller = new app.Controller(this.model, this.view); + } - /** - * Sets up a brand new Todo list. - * - * @param {string} name The name of your new to do list. - */ - function Todo(name) { - this.storage = new app.Store(name); - this.model = new app.Model(this.storage); - this.template = new app.Template(); - this.view = new app.View(this.template); - this.controller = new app.Controller(this.model, this.view); - } + var todo; - var todo; + function setView() { + todo.controller.setView(document.location.hash); + } - function setView() { - todo.controller.setView(document.location.hash); - } - - $on(window, 'load', function() { - todo = new Todo('todos-vanillajs'); - setView(); - }); - $on(window, 'hashchange', setView); - })(); + $on(window, 'load', function () { + todo = new Todo('todos-vanillajs'); + setView(); + }); + $on(window, 'hashchange', setView); /***/ }, @@ -450,224 +446,221 @@ /***/ function(module, exports, __webpack_require__) { /*global qs, qsa, $on, $parent, $delegate */ + 'use strict'; __webpack_require__(8); - (function (window) { - 'use strict'; + /** + * View that abstracts away the browser's DOM completely. + * It has two simple entry points: + * + * - bind(eventName, handler) + * Takes a todo application event and registers the handler + * - render(command, parameterObject) + * Renders the given command with the options + */ + function View(template) { + this.template = template; - /** - * View that abstracts away the browser's DOM completely. - * It has two simple entry points: - * - * - bind(eventName, handler) - * Takes a todo application event and registers the handler - * - render(command, parameterObject) - * Renders the given command with the options - */ - function View(template) { - this.template = template; + this.ENTER_KEY = 13; + this.ESCAPE_KEY = 27; - this.ENTER_KEY = 13; - this.ESCAPE_KEY = 27; + this.$todoList = qs('.todo-list'); + this.$todoItemCounter = qs('.todo-count'); + this.$clearCompleted = qs('.clear-completed'); + this.$main = qs('.main'); + this.$footer = qs('.footer'); + this.$toggleAll = qs('.toggle-all'); + this.$newTodo = qs('.new-todo'); + } - this.$todoList = qs('.todo-list'); - this.$todoItemCounter = qs('.todo-count'); - this.$clearCompleted = qs('.clear-completed'); - this.$main = qs('.main'); - this.$footer = qs('.footer'); - this.$toggleAll = qs('.toggle-all'); - this.$newTodo = qs('.new-todo'); - } + View.prototype._removeItem = function (id) { + var elem = qs('[data-id="' + id + '"]'); - View.prototype._removeItem = function (id) { - var elem = qs('[data-id="' + id + '"]'); + if (elem) { + this.$todoList.removeChild(elem); + } + }; - if (elem) { - this.$todoList.removeChild(elem); - } - }; + View.prototype._clearCompletedButton = function (completedCount, visible) { + this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount); + this.$clearCompleted.style.display = visible ? 'block' : 'none'; + }; - View.prototype._clearCompletedButton = function (completedCount, visible) { - this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount); - this.$clearCompleted.style.display = visible ? 'block' : 'none'; - }; + View.prototype._setFilter = function (currentPage) { + qs('.filters .selected').className = ''; + qs('.filters [href="#/' + currentPage + '"]').className = 'selected'; + }; - View.prototype._setFilter = function (currentPage) { - qs('.filters .selected').className = ''; - qs('.filters [href="#/' + currentPage + '"]').className = 'selected'; - }; + View.prototype._elementComplete = function (id, completed) { + var listItem = qs('[data-id="' + id + '"]'); - View.prototype._elementComplete = function (id, completed) { - var listItem = qs('[data-id="' + id + '"]'); + if (!listItem) { + return; + } - if (!listItem) { - return; - } + listItem.className = completed ? 'completed' : ''; - listItem.className = completed ? 'completed' : ''; + // In case it was toggled from an event and not by clicking the checkbox + qs('input', listItem).checked = completed; + }; - // In case it was toggled from an event and not by clicking the checkbox - qs('input', listItem).checked = completed; - }; + View.prototype._editItem = function (id, title) { + var listItem = qs('[data-id="' + id + '"]'); - View.prototype._editItem = function (id, title) { - var listItem = qs('[data-id="' + id + '"]'); + if (!listItem) { + return; + } - if (!listItem) { - return; - } + listItem.className = listItem.className + ' editing'; - listItem.className = listItem.className + ' editing'; + var input = document.createElement('input'); + input.className = 'edit'; - var input = document.createElement('input'); - input.className = 'edit'; + listItem.appendChild(input); + input.focus(); + input.value = title; + }; - listItem.appendChild(input); - input.focus(); - input.value = title; - }; + View.prototype._editItemDone = function (id, title) { + var listItem = qs('[data-id="' + id + '"]'); - View.prototype._editItemDone = function (id, title) { - var listItem = qs('[data-id="' + id + '"]'); + if (!listItem) { + return; + } - if (!listItem) { - return; - } + var input = qs('input.edit', listItem); + listItem.removeChild(input); - var input = qs('input.edit', listItem); - listItem.removeChild(input); + listItem.className = listItem.className.replace('editing', ''); - listItem.className = listItem.className.replace('editing', ''); + qsa('label', listItem).forEach(function (label) { + label.textContent = title; + }); + }; - qsa('label', listItem).forEach(function (label) { - label.textContent = title; - }); - }; + View.prototype.render = function (viewCmd, parameter) { + var that = this; + var viewCommands = { + showEntries: function () { + that.$todoList.innerHTML = that.template.show(parameter); + }, + removeItem: function () { + that._removeItem(parameter); + }, + updateElementCount: function () { + that.$todoItemCounter.innerHTML = that.template.itemCounter(parameter); + }, + clearCompletedButton: function () { + that._clearCompletedButton(parameter.completed, parameter.visible); + }, + contentBlockVisibility: function () { + that.$main.style.display = that.$footer.style.display = parameter.visible ? 'block' : 'none'; + }, + toggleAll: function () { + that.$toggleAll.checked = parameter.checked; + }, + setFilter: function () { + that._setFilter(parameter); + }, + clearNewTodo: function () { + that.$newTodo.value = ''; + }, + elementComplete: function () { + that._elementComplete(parameter.id, parameter.completed); + }, + editItem: function () { + that._editItem(parameter.id, parameter.title); + }, + editItemDone: function () { + that._editItemDone(parameter.id, parameter.title); + } + }; - View.prototype.render = function (viewCmd, parameter) { - var that = this; - var viewCommands = { - showEntries: function () { - that.$todoList.innerHTML = that.template.show(parameter); - }, - removeItem: function () { - that._removeItem(parameter); - }, - updateElementCount: function () { - that.$todoItemCounter.innerHTML = that.template.itemCounter(parameter); - }, - clearCompletedButton: function () { - that._clearCompletedButton(parameter.completed, parameter.visible); - }, - contentBlockVisibility: function () { - that.$main.style.display = that.$footer.style.display = parameter.visible ? 'block' : 'none'; - }, - toggleAll: function () { - that.$toggleAll.checked = parameter.checked; - }, - setFilter: function () { - that._setFilter(parameter); - }, - clearNewTodo: function () { - that.$newTodo.value = ''; - }, - elementComplete: function () { - that._elementComplete(parameter.id, parameter.completed); - }, - editItem: function () { - that._editItem(parameter.id, parameter.title); - }, - editItemDone: function () { - that._editItemDone(parameter.id, parameter.title); - } - }; + viewCommands[viewCmd](); + }; - viewCommands[viewCmd](); - }; + View.prototype._itemId = function (element) { + var li = $parent(element, 'li'); + return parseInt(li.dataset.id, 10); + }; - View.prototype._itemId = function (element) { - var li = $parent(element, 'li'); - return parseInt(li.dataset.id, 10); - }; + View.prototype._bindItemEditDone = function (handler) { + var that = this; + $delegate(that.$todoList, 'li .edit', 'blur', function () { + if (!this.dataset.iscanceled) { + handler({ + id: that._itemId(this), + title: this.value + }); + } + }); - View.prototype._bindItemEditDone = function (handler) { - var that = this; - $delegate(that.$todoList, 'li .edit', 'blur', function () { - if (!this.dataset.iscanceled) { - handler({ - id: that._itemId(this), - title: this.value - }); - } - }); + $delegate(that.$todoList, 'li .edit', 'keypress', function (event) { + if (event.keyCode === that.ENTER_KEY) { + // Remove the cursor from the input when you hit enter just like if it + // were a real form + this.blur(); + } + }); + }; - $delegate(that.$todoList, 'li .edit', 'keypress', function (event) { - if (event.keyCode === that.ENTER_KEY) { - // Remove the cursor from the input when you hit enter just like if it - // were a real form - this.blur(); - } - }); - }; + View.prototype._bindItemEditCancel = function (handler) { + var that = this; + $delegate(that.$todoList, 'li .edit', 'keyup', function (event) { + if (event.keyCode === that.ESCAPE_KEY) { + this.dataset.iscanceled = true; + this.blur(); - View.prototype._bindItemEditCancel = function (handler) { - var that = this; - $delegate(that.$todoList, 'li .edit', 'keyup', function (event) { - if (event.keyCode === that.ESCAPE_KEY) { - this.dataset.iscanceled = true; - this.blur(); + handler({id: that._itemId(this)}); + } + }); + }; - handler({id: that._itemId(this)}); - } - }); - }; + View.prototype.bind = function (event, handler) { + var that = this; + if (event === 'newTodo') { + $on(that.$newTodo, 'change', function () { + handler(that.$newTodo.value); + }); - View.prototype.bind = function (event, handler) { - var that = this; - if (event === 'newTodo') { - $on(that.$newTodo, 'change', function () { - handler(that.$newTodo.value); - }); + } else if (event === 'removeCompleted') { + $on(that.$clearCompleted, 'click', function () { + handler(); + }); - } else if (event === 'removeCompleted') { - $on(that.$clearCompleted, 'click', function () { - handler(); - }); + } else if (event === 'toggleAll') { + $on(that.$toggleAll, 'click', function () { + handler({completed: this.checked}); + }); - } else if (event === 'toggleAll') { - $on(that.$toggleAll, 'click', function () { - handler({completed: this.checked}); - }); + } else if (event === 'itemEdit') { + $delegate(that.$todoList, 'li label', 'dblclick', function () { + handler({id: that._itemId(this)}); + }); - } else if (event === 'itemEdit') { - $delegate(that.$todoList, 'li label', 'dblclick', function () { - handler({id: that._itemId(this)}); - }); + } else if (event === 'itemRemove') { + $delegate(that.$todoList, '.destroy', 'click', function () { + handler({id: that._itemId(this)}); + }); - } else if (event === 'itemRemove') { - $delegate(that.$todoList, '.destroy', 'click', function () { - handler({id: that._itemId(this)}); - }); + } else if (event === 'itemToggle') { + $delegate(that.$todoList, '.toggle', 'click', function () { + handler({ + id: that._itemId(this), + completed: this.checked + }); + }); - } else if (event === 'itemToggle') { - $delegate(that.$todoList, '.toggle', 'click', function () { - handler({ - id: that._itemId(this), - completed: this.checked - }); - }); + } else if (event === 'itemEditDone') { + that._bindItemEditDone(handler); - } else if (event === 'itemEditDone') { - that._bindItemEditDone(handler); + } else if (event === 'itemEditCancel') { + that._bindItemEditCancel(handler); + } + }; - } else if (event === 'itemEditCancel') { - that._bindItemEditCancel(handler); - } - }; - - // Export to window - window.app = window.app || {}; - window.app.View = View; - }(window)); + // Export to window + window.app = window.app || {}; + window.app.View = View; /***/ }, @@ -675,457 +668,450 @@ /***/ function(module, exports) { /*global NodeList */ - (function (window) { - 'use strict'; + 'use strict'; + // Get element(s) by CSS selector: + window.qs = function (selector, scope) { + return (scope || document).querySelector(selector); + }; + window.qsa = function (selector, scope) { + return (scope || document).querySelectorAll(selector); + }; - // Get element(s) by CSS selector: - window.qs = function (selector, scope) { - return (scope || document).querySelector(selector); - }; - window.qsa = function (selector, scope) { - return (scope || document).querySelectorAll(selector); - }; + // addEventListener wrapper: + window.$on = function (target, type, callback, useCapture) { + target.addEventListener(type, callback, !!useCapture); + }; - // addEventListener wrapper: - window.$on = function (target, type, callback, useCapture) { - target.addEventListener(type, callback, !!useCapture); - }; + // Attach a handler to event for all elements that match the selector, + // now or in the future, based on a root element + window.$delegate = function (target, selector, type, handler) { + function dispatchEvent(event) { + var targetElement = event.target; + var potentialElements = window.qsa(selector, target); + var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0; - // Attach a handler to event for all elements that match the selector, - // now or in the future, based on a root element - window.$delegate = function (target, selector, type, handler) { - function dispatchEvent(event) { - var targetElement = event.target; - var potentialElements = window.qsa(selector, target); - var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0; - - if (hasMatch) { - handler.call(targetElement, event); - } + if (hasMatch) { + handler.call(targetElement, event); } + } - // https://developer.mozilla.org/en-US/docs/Web/Events/blur - var useCapture = type === 'blur' || type === 'focus'; + // https://developer.mozilla.org/en-US/docs/Web/Events/blur + var useCapture = type === 'blur' || type === 'focus'; - window.$on(target, type, dispatchEvent, useCapture); - }; + window.$on(target, type, dispatchEvent, useCapture); + }; - // Find the element's parent with the given tag name: - // $parent(qs('a'), 'div'); - window.$parent = function (element, tagName) { - if (!element.parentNode) { - return; - } - if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { - return element.parentNode; - } - return window.$parent(element.parentNode, tagName); - }; + // Find the element's parent with the given tag name: + // $parent(qs('a'), 'div'); + window.$parent = function (element, tagName) { + if (!element.parentNode) { + return; + } + if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { + return element.parentNode; + } + return window.$parent(element.parentNode, tagName); + }; - // Allow for looping on nodes by chaining: - // qsa('.foo').forEach(function () {}) - NodeList.prototype.forEach = Array.prototype.forEach; - })(window); + // Allow for looping on nodes by chaining: + // qsa('.foo').forEach(function () {}) + NodeList.prototype.forEach = Array.prototype.forEach; /***/ }, /* 9 */ /***/ function(module, exports) { - (function (window) { - 'use strict'; + 'use strict'; + /** + * Takes a model and view and acts as the controller between them + * + * @constructor + * @param {object} model The model instance + * @param {object} view The view instance + */ + function Controller(model, view) { + var that = this; + that.model = model; + that.view = view; - /** - * Takes a model and view and acts as the controller between them - * - * @constructor - * @param {object} model The model instance - * @param {object} view The view instance - */ - function Controller(model, view) { - var that = this; - that.model = model; - that.view = view; + that.view.bind('newTodo', function (title) { + that.addItem(title); + }); - that.view.bind('newTodo', function (title) { - that.addItem(title); - }); + that.view.bind('itemEdit', function (item) { + that.editItem(item.id); + }); - that.view.bind('itemEdit', function (item) { - that.editItem(item.id); - }); + that.view.bind('itemEditDone', function (item) { + that.editItemSave(item.id, item.title); + }); - that.view.bind('itemEditDone', function (item) { - that.editItemSave(item.id, item.title); - }); + that.view.bind('itemEditCancel', function (item) { + that.editItemCancel(item.id); + }); - that.view.bind('itemEditCancel', function (item) { - that.editItemCancel(item.id); - }); + that.view.bind('itemRemove', function (item) { + that.removeItem(item.id); + }); - that.view.bind('itemRemove', function (item) { - that.removeItem(item.id); - }); + that.view.bind('itemToggle', function (item) { + that.toggleComplete(item.id, item.completed); + }); - that.view.bind('itemToggle', function (item) { - that.toggleComplete(item.id, item.completed); - }); + that.view.bind('removeCompleted', function () { + that.removeCompletedItems(); + }); - that.view.bind('removeCompleted', function () { - that.removeCompletedItems(); - }); + that.view.bind('toggleAll', function (status) { + that.toggleAll(status.completed); + }); + } - that.view.bind('toggleAll', function (status) { - that.toggleAll(status.completed); - }); + /** + * Loads and initialises the view + * + * @param {string} '' | 'active' | 'completed' + */ + Controller.prototype.setView = function (locationHash) { + var route = locationHash.split('/')[1]; + var page = route || ''; + this._updateFilterState(page); + }; + + /** + * An event to fire on load. Will get all items and display them in the + * todo-list + */ + Controller.prototype.showAll = function () { + var that = this; + that.model.read(function (data) { + that.view.render('showEntries', data); + }); + }; + + /** + * Renders all active tasks + */ + Controller.prototype.showActive = function () { + var that = this; + that.model.read({completed: false}, function (data) { + that.view.render('showEntries', data); + }); + }; + + /** + * Renders all completed tasks + */ + Controller.prototype.showCompleted = function () { + var that = this; + that.model.read({completed: true}, function (data) { + that.view.render('showEntries', data); + }); + }; + + /** + * An event to fire whenever you want to add an item. Simply pass in the event + * object and it'll handle the DOM insertion and saving of the new item. + */ + Controller.prototype.addItem = function (title) { + var that = this; + + if (title.trim() === '') { + return; } - /** - * Loads and initialises the view - * - * @param {string} '' | 'active' | 'completed' - */ - Controller.prototype.setView = function (locationHash) { - var route = locationHash.split('/')[1]; - var page = route || ''; - this._updateFilterState(page); - }; + that.model.create(title, function () { + that.view.render('clearNewTodo'); + that._filter(true); + }); + }; - /** - * An event to fire on load. Will get all items and display them in the - * todo-list - */ - Controller.prototype.showAll = function () { - var that = this; - that.model.read(function (data) { - that.view.render('showEntries', data); + /* + * Triggers the item editing mode. + */ + Controller.prototype.editItem = function (id) { + var that = this; + that.model.read(id, function (data) { + that.view.render('editItem', {id: id, title: data[0].title}); + }); + }; + + /* + * Finishes the item editing mode successfully. + */ + Controller.prototype.editItemSave = function (id, title) { + var that = this; + if (title.trim()) { + that.model.update(id, {title: title}, function () { + that.view.render('editItemDone', {id: id, title: title}); }); - }; + } else { + that.removeItem(id); + } + }; - /** - * Renders all active tasks - */ - Controller.prototype.showActive = function () { - var that = this; - that.model.read({ completed: false }, function (data) { - that.view.render('showEntries', data); + /* + * Cancels the item editing mode. + */ + Controller.prototype.editItemCancel = function (id) { + var that = this; + that.model.read(id, function (data) { + that.view.render('editItemDone', {id: id, title: data[0].title}); + }); + }; + + /** + * By giving it an ID it'll find the DOM element matching that ID, + * remove it from the DOM and also remove it from storage. + * + * @param {number} id The ID of the item to remove from the DOM and + * storage + */ + Controller.prototype.removeItem = function (id) { + var that = this; + that.model.remove(id, function () { + that.view.render('removeItem', id); + }); + + that._filter(); + }; + + /** + * Will remove all completed items from the DOM and storage. + */ + Controller.prototype.removeCompletedItems = function () { + var that = this; + that.model.read({completed: true}, function (data) { + data.forEach(function (item) { + that.removeItem(item.id); }); - }; + }); - /** - * Renders all completed tasks - */ - Controller.prototype.showCompleted = function () { - var that = this; - that.model.read({ completed: true }, function (data) { - that.view.render('showEntries', data); - }); - }; - - /** - * An event to fire whenever you want to add an item. Simply pass in the event - * object and it'll handle the DOM insertion and saving of the new item. - */ - Controller.prototype.addItem = function (title) { - var that = this; - - if (title.trim() === '') { - return; - } - - that.model.create(title, function () { - that.view.render('clearNewTodo'); - that._filter(true); - }); - }; - - /* - * Triggers the item editing mode. - */ - Controller.prototype.editItem = function (id) { - var that = this; - that.model.read(id, function (data) { - that.view.render('editItem', {id: id, title: data[0].title}); - }); - }; - - /* - * Finishes the item editing mode successfully. - */ - Controller.prototype.editItemSave = function (id, title) { - var that = this; - if (title.trim()) { - that.model.update(id, {title: title}, function () { - that.view.render('editItemDone', {id: id, title: title}); - }); - } else { - that.removeItem(id); - } - }; - - /* - * Cancels the item editing mode. - */ - Controller.prototype.editItemCancel = function (id) { - var that = this; - that.model.read(id, function (data) { - that.view.render('editItemDone', {id: id, title: data[0].title}); - }); - }; - - /** - * By giving it an ID it'll find the DOM element matching that ID, - * remove it from the DOM and also remove it from storage. - * - * @param {number} id The ID of the item to remove from the DOM and - * storage - */ - Controller.prototype.removeItem = function (id) { - var that = this; - that.model.remove(id, function () { - that.view.render('removeItem', id); + that._filter(); + }; + + /** + * Give it an ID of a model and a checkbox and it will update the item + * in storage based on the checkbox's state. + * + * @param {number} id The ID of the element to complete or uncomplete + * @param {object} checkbox The checkbox to check the state of complete + * or not + * @param {boolean|undefined} silent Prevent re-filtering the todo items + */ + Controller.prototype.toggleComplete = function (id, completed, silent) { + var that = this; + that.model.update(id, {completed: completed}, function () { + that.view.render('elementComplete', { + id: id, + completed: completed }); + }); + if (!silent) { that._filter(); - }; + } + }; - /** - * Will remove all completed items from the DOM and storage. - */ - Controller.prototype.removeCompletedItems = function () { - var that = this; - that.model.read({ completed: true }, function (data) { - data.forEach(function (item) { - that.removeItem(item.id); - }); + /** + * Will toggle ALL checkboxes' on/off state and completeness of models. + * Just pass in the event object. + */ + Controller.prototype.toggleAll = function (completed) { + var that = this; + that.model.read({completed: !completed}, function (data) { + data.forEach(function (item) { + that.toggleComplete(item.id, completed, true); + }); + }); + + that._filter(); + }; + + /** + * Updates the pieces of the page which change depending on the remaining + * number of todos. + */ + Controller.prototype._updateCount = function () { + var that = this; + that.model.getCount(function (todos) { + that.view.render('updateElementCount', todos.active); + that.view.render('clearCompletedButton', { + completed: todos.completed, + visible: todos.completed > 0 }); - that._filter(); - }; + that.view.render('toggleAll', {checked: todos.completed === todos.total}); + that.view.render('contentBlockVisibility', {visible: todos.total > 0}); + }); + }; - /** - * Give it an ID of a model and a checkbox and it will update the item - * in storage based on the checkbox's state. - * - * @param {number} id The ID of the element to complete or uncomplete - * @param {object} checkbox The checkbox to check the state of complete - * or not - * @param {boolean|undefined} silent Prevent re-filtering the todo items - */ - Controller.prototype.toggleComplete = function (id, completed, silent) { - var that = this; - that.model.update(id, { completed: completed }, function () { - that.view.render('elementComplete', { - id: id, - completed: completed - }); - }); + /** + * Re-filters the todo items, based on the active route. + * @param {boolean|undefined} force forces a re-painting of todo items. + */ + Controller.prototype._filter = function (force) { + var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1); - if (!silent) { - that._filter(); - } - }; + // Update the elements on the page, which change with each completed todo + this._updateCount(); - /** - * Will toggle ALL checkboxes' on/off state and completeness of models. - * Just pass in the event object. - */ - Controller.prototype.toggleAll = function (completed) { - var that = this; - that.model.read({ completed: !completed }, function (data) { - data.forEach(function (item) { - that.toggleComplete(item.id, completed, true); - }); - }); + // If the last active route isn't "All", or we're switching routes, we + // re-create the todo item elements, calling: + // this.show[All|Active|Completed](); + if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) { + this['show' + activeRoute](); + } - that._filter(); - }; + this._lastActiveRoute = activeRoute; + }; - /** - * Updates the pieces of the page which change depending on the remaining - * number of todos. - */ - Controller.prototype._updateCount = function () { - var that = this; - that.model.getCount(function (todos) { - that.view.render('updateElementCount', todos.active); - that.view.render('clearCompletedButton', { - completed: todos.completed, - visible: todos.completed > 0 - }); + /** + * Simply updates the filter nav's selected states + */ + Controller.prototype._updateFilterState = function (currentPage) { + // Store a reference to the active route, allowing us to re-filter todo + // items as they are marked complete or incomplete. + this._activeRoute = currentPage; - that.view.render('toggleAll', {checked: todos.completed === todos.total}); - that.view.render('contentBlockVisibility', {visible: todos.total > 0}); - }); - }; + if (currentPage === '') { + this._activeRoute = 'All'; + } - /** - * Re-filters the todo items, based on the active route. - * @param {boolean|undefined} force forces a re-painting of todo items. - */ - Controller.prototype._filter = function (force) { - var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1); + this._filter(); - // Update the elements on the page, which change with each completed todo - this._updateCount(); + this.view.render('setFilter', currentPage); + }; - // If the last active route isn't "All", or we're switching routes, we - // re-create the todo item elements, calling: - // this.show[All|Active|Completed](); - if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) { - this['show' + activeRoute](); - } - - this._lastActiveRoute = activeRoute; - }; - - /** - * Simply updates the filter nav's selected states - */ - Controller.prototype._updateFilterState = function (currentPage) { - // Store a reference to the active route, allowing us to re-filter todo - // items as they are marked complete or incomplete. - this._activeRoute = currentPage; - - if (currentPage === '') { - this._activeRoute = 'All'; - } - - this._filter(); - - this.view.render('setFilter', currentPage); - }; - - // Export to window - window.app = window.app || {}; - window.app.Controller = Controller; - })(window); + // Export to window + window.app = window.app || {}; + window.app.Controller = Controller; /***/ }, /* 10 */ /***/ function(module, exports) { - (function (window) { - 'use strict'; + 'use strict'; + /** + * Creates a new Model instance and hooks up the storage. + * + * @constructor + * @param {object} storage A reference to the client side storage class + */ + function Model(storage) { + this.storage = storage; + } - /** - * Creates a new Model instance and hooks up the storage. - * - * @constructor - * @param {object} storage A reference to the client side storage class - */ - function Model(storage) { - this.storage = storage; + /** + * Creates a new todo model + * + * @param {string} [title] The title of the task + * @param {function} [callback] The callback to fire after the model is created + */ + Model.prototype.create = function (title, callback) { + title = title || ''; + callback = callback || function () { + }; + + var newItem = { + title: title.trim(), + completed: false + }; + + this.storage.save(newItem, callback); + }; + + /** + * Finds and returns a model in storage. If no query is given it'll simply + * return everything. If you pass in a string or number it'll look that up as + * the ID of the model to find. Lastly, you can pass it an object to match + * against. + * + * @param {string|number|object} [query] A query to match models against + * @param {function} [callback] The callback to fire after the model is found + * + * @example + * model.read(1, func); // Will find the model with an ID of 1 + * model.read('1'); // Same as above + * //Below will find a model with foo equalling bar and hello equalling world. + * model.read({ foo: 'bar', hello: 'world' }); + */ + Model.prototype.read = function (query, callback) { + var queryType = typeof query; + callback = callback || function () { + }; + + if (queryType === 'function') { + callback = query; + return this.storage.findAll(callback); + } else if (queryType === 'string' || queryType === 'number') { + query = parseInt(query, 10); + this.storage.find({id: query}, callback); + } else { + this.storage.find(query, callback); } + }; - /** - * Creates a new todo model - * - * @param {string} [title] The title of the task - * @param {function} [callback] The callback to fire after the model is created - */ - Model.prototype.create = function (title, callback) { - title = title || ''; - callback = callback || function () {}; + /** + * Updates a model by giving it an ID, data to update, and a callback to fire when + * the update is complete. + * + * @param {number} id The id of the model to update + * @param {object} data The properties to update and their new value + * @param {function} callback The callback to fire when the update is complete. + */ + Model.prototype.update = function (id, data, callback) { + this.storage.save(data, callback, id); + }; - var newItem = { - title: title.trim(), - completed: false - }; + /** + * Removes a model from storage + * + * @param {number} id The ID of the model to remove + * @param {function} callback The callback to fire when the removal is complete. + */ + Model.prototype.remove = function (id, callback) { + this.storage.remove(id, callback); + }; - this.storage.save(newItem, callback); + /** + * WARNING: Will remove ALL data from storage. + * + * @param {function} callback The callback to fire when the storage is wiped. + */ + Model.prototype.removeAll = function (callback) { + this.storage.drop(callback); + }; + + /** + * Returns a count of all todos + */ + Model.prototype.getCount = function (callback) { + var todos = { + active: 0, + completed: 0, + total: 0 }; - /** - * Finds and returns a model in storage. If no query is given it'll simply - * return everything. If you pass in a string or number it'll look that up as - * the ID of the model to find. Lastly, you can pass it an object to match - * against. - * - * @param {string|number|object} [query] A query to match models against - * @param {function} [callback] The callback to fire after the model is found - * - * @example - * model.read(1, func); // Will find the model with an ID of 1 - * model.read('1'); // Same as above - * //Below will find a model with foo equalling bar and hello equalling world. - * model.read({ foo: 'bar', hello: 'world' }); - */ - Model.prototype.read = function (query, callback) { - var queryType = typeof query; - callback = callback || function () {}; + this.storage.findAll(function (data) { + data.forEach(function (todo) { + if (todo.completed) { + todos.completed++; + } else { + todos.active++; + } - if (queryType === 'function') { - callback = query; - return this.storage.findAll(callback); - } else if (queryType === 'string' || queryType === 'number') { - query = parseInt(query, 10); - this.storage.find({ id: query }, callback); - } else { - this.storage.find(query, callback); - } - }; - - /** - * Updates a model by giving it an ID, data to update, and a callback to fire when - * the update is complete. - * - * @param {number} id The id of the model to update - * @param {object} data The properties to update and their new value - * @param {function} callback The callback to fire when the update is complete. - */ - Model.prototype.update = function (id, data, callback) { - this.storage.save(data, callback, id); - }; - - /** - * Removes a model from storage - * - * @param {number} id The ID of the model to remove - * @param {function} callback The callback to fire when the removal is complete. - */ - Model.prototype.remove = function (id, callback) { - this.storage.remove(id, callback); - }; - - /** - * WARNING: Will remove ALL data from storage. - * - * @param {function} callback The callback to fire when the storage is wiped. - */ - Model.prototype.removeAll = function (callback) { - this.storage.drop(callback); - }; - - /** - * Returns a count of all todos - */ - Model.prototype.getCount = function (callback) { - var todos = { - active: 0, - completed: 0, - total: 0 - }; - - this.storage.findAll(function (data) { - data.forEach(function (todo) { - if (todo.completed) { - todos.completed++; - } else { - todos.active++; - } - - todos.total++; - }); - callback(todos); + todos.total++; }); - }; + callback(todos); + }); + }; - // Export to window - window.app = window.app || {}; - window.app.Model = Model; - })(window); + // Export to window + window.app = window.app || {}; + window.app.Model = Model; /***/ }, @@ -1133,145 +1119,145 @@ /***/ function(module, exports) { /*jshint eqeqeq:false */ - (function (window) { - 'use strict'; + 'use strict'; + /** + * Creates a new client side storage object and will create an empty + * collection if no collection already exists. + * + * @param {string} name The name of our DB we want to use + * @param {function} callback Our fake DB uses callbacks because in + * real life you probably would be making AJAX calls + */ + function Store(name, callback) { + callback = callback || function () { + }; - /** - * Creates a new client side storage object and will create an empty - * collection if no collection already exists. - * - * @param {string} name The name of our DB we want to use - * @param {function} callback Our fake DB uses callbacks because in - * real life you probably would be making AJAX calls - */ - function Store(name, callback) { - callback = callback || function () {}; + this._dbName = name; - this._dbName = name; + if (!localStorage[name]) { + var data = { + todos: [] + }; - if (!localStorage[name]) { - var data = { - todos: [] - }; - - localStorage[name] = JSON.stringify(data); - } - - callback.call(this, JSON.parse(localStorage[name])); + localStorage[name] = JSON.stringify(data); } - /** - * Finds items based on a query given as a JS object - * - * @param {object} query The query to match against (i.e. {foo: 'bar'}) - * @param {function} callback The callback to fire when the query has - * completed running - * - * @example - * db.find({foo: 'bar', hello: 'world'}, function (data) { + callback.call(this, JSON.parse(localStorage[name])); + } + + /** + * Finds items based on a query given as a JS object + * + * @param {object} query The query to match against (i.e. {foo: 'bar'}) + * @param {function} callback The callback to fire when the query has + * completed running + * + * @example + * db.find({foo: 'bar', hello: 'world'}, function (data) { * // data will return any items that have foo: bar and * // hello: world in their properties * }); - */ - Store.prototype.find = function (query, callback) { - if (!callback) { - return; - } + */ + Store.prototype.find = function (query, callback) { + if (!callback) { + return; + } - var todos = JSON.parse(localStorage[this._dbName]).todos; + var todos = JSON.parse(localStorage[this._dbName]).todos; - callback.call(this, todos.filter(function (todo) { - for (var q in query) { - if (query[q] !== todo[q]) { - return false; - } + callback.call(this, todos.filter(function (todo) { + for (var q in query) { + if (query[q] !== todo[q]) { + return false; } - return true; - })); - }; - - /** - * Will retrieve all data from the collection - * - * @param {function} callback The callback to fire upon retrieving data - */ - Store.prototype.findAll = function (callback) { - callback = callback || function () {}; - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); - }; - - /** - * Will save the given data to the DB. If no item exists it will create a new - * item, otherwise it'll simply update an existing item's properties - * - * @param {object} updateData The data to save back into the DB - * @param {function} callback The callback to fire after saving - * @param {number} id An optional param to enter an ID of an item to update - */ - Store.prototype.save = function (updateData, callback, id) { - var data = JSON.parse(localStorage[this._dbName]); - var todos = data.todos; - - callback = callback || function () {}; - - // If an ID was actually given, find the item and update each property - if (id) { - for (var i = 0; i < todos.length; i++) { - if (todos[i].id === id) { - for (var key in updateData) { - todos[i][key] = updateData[key]; - } - break; - } - } - - localStorage[this._dbName] = JSON.stringify(data); - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); - } else { - // Generate an ID - updateData.id = new Date().getTime(); - - todos.push(updateData); - localStorage[this._dbName] = JSON.stringify(data); - callback.call(this, [updateData]); } - }; + return true; + })); + }; - /** - * Will remove an item from the Store based on its ID - * - * @param {number} id The ID of the item you want to remove - * @param {function} callback The callback to fire after saving - */ - Store.prototype.remove = function (id, callback) { - var data = JSON.parse(localStorage[this._dbName]); - var todos = data.todos; + /** + * Will retrieve all data from the collection + * + * @param {function} callback The callback to fire upon retrieving data + */ + Store.prototype.findAll = function (callback) { + callback = callback || function () { + }; + callback.call(this, JSON.parse(localStorage[this._dbName]).todos); + }; + /** + * Will save the given data to the DB. If no item exists it will create a new + * item, otherwise it'll simply update an existing item's properties + * + * @param {object} updateData The data to save back into the DB + * @param {function} callback The callback to fire after saving + * @param {number} id An optional param to enter an ID of an item to update + */ + Store.prototype.save = function (updateData, callback, id) { + var data = JSON.parse(localStorage[this._dbName]); + var todos = data.todos; + + callback = callback || function () { + }; + + // If an ID was actually given, find the item and update each property + if (id) { for (var i = 0; i < todos.length; i++) { - if (todos[i].id == id) { - todos.splice(i, 1); + if (todos[i].id === id) { + for (var key in updateData) { + todos[i][key] = updateData[key]; + } break; } } localStorage[this._dbName] = JSON.stringify(data); callback.call(this, JSON.parse(localStorage[this._dbName]).todos); - }; + } else { + // Generate an ID + updateData.id = new Date().getTime(); - /** - * Will drop all storage and start fresh - * - * @param {function} callback The callback to fire after dropping the data - */ - Store.prototype.drop = function (callback) { - localStorage[this._dbName] = JSON.stringify({todos: []}); - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); - }; + todos.push(updateData); + localStorage[this._dbName] = JSON.stringify(data); + callback.call(this, [updateData]); + } + }; - // Export to window - window.app = window.app || {}; - window.app.Store = Store; - })(window); + /** + * Will remove an item from the Store based on its ID + * + * @param {number} id The ID of the item you want to remove + * @param {function} callback The callback to fire after saving + */ + Store.prototype.remove = function (id, callback) { + var data = JSON.parse(localStorage[this._dbName]); + var todos = data.todos; + + for (var i = 0; i < todos.length; i++) { + if (todos[i].id == id) { + todos.splice(i, 1); + break; + } + } + + localStorage[this._dbName] = JSON.stringify(data); + callback.call(this, JSON.parse(localStorage[this._dbName]).todos); + }; + + /** + * Will drop all storage and start fresh + * + * @param {function} callback The callback to fire after dropping the data + */ + Store.prototype.drop = function (callback) { + localStorage[this._dbName] = JSON.stringify({todos: []}); + callback.call(this, JSON.parse(localStorage[this._dbName]).todos); + }; + + // Export to window + window.app = window.app || {}; + window.app.Store = Store; /***/ }, @@ -1279,119 +1265,117 @@ /***/ function(module, exports) { /*jshint laxbreak:true */ - (function (window) { - 'use strict'; + 'use strict'; - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '`': '`' - }; + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '`': '`' + }; - var escapeHtmlChar = function (chr) { - return htmlEscapes[chr]; - }; + var escapeHtmlChar = function (chr) { + return htmlEscapes[chr]; + }; - var reUnescapedHtml = /[&<>"'`]/g, - reHasUnescapedHtml = new RegExp(reUnescapedHtml.source); + var reUnescapedHtml = /[&<>"'`]/g, + reHasUnescapedHtml = new RegExp(reUnescapedHtml.source); - var escape = function (string) { - return (string && reHasUnescapedHtml.test(string)) - ? string.replace(reUnescapedHtml, escapeHtmlChar) - : string; - }; + var escape = function (string) { + return (string && reHasUnescapedHtml.test(string)) + ? string.replace(reUnescapedHtml, escapeHtmlChar) + : string; + }; - /** - * Sets up defaults for all the Template methods such as a default template - * - * @constructor - */ - function Template() { - this.defaultTemplate - = '
  • ' - + '
    ' - + '' - + '' - + '' - + '
    ' - + '
  • '; - } + /** + * Sets up defaults for all the Template methods such as a default template + * + * @constructor + */ + function Template() { + this.defaultTemplate + = '
  • ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
  • '; + } - /** - * Creates an
  • HTML string and returns it for placement in your app. - * - * NOTE: In real life you should be using a templating engine such as Mustache - * or Handlebars, however, this is a vanilla JS example. - * - * @param {object} data The object containing keys you want to find in the - * template to replace. - * @returns {string} HTML String of an
  • element - * - * @example - * view.show({ + /** + * Creates an
  • HTML string and returns it for placement in your app. + * + * NOTE: In real life you should be using a templating engine such as Mustache + * or Handlebars, however, this is a vanilla JS example. + * + * @param {object} data The object containing keys you want to find in the + * template to replace. + * @returns {string} HTML String of an
  • element + * + * @example + * view.show({ * id: 1, * title: "Hello World", * completed: 0, * }); - */ - Template.prototype.show = function (data) { - var i, l; - var view = ''; + */ + Template.prototype.show = function (data) { + var i, l; + var view = ''; - for (i = 0, l = data.length; i < l; i++) { - var template = this.defaultTemplate; - var completed = ''; - var checked = ''; + for (i = 0, l = data.length; i < l; i++) { + var template = this.defaultTemplate; + var completed = ''; + var checked = ''; - if (data[i].completed) { - completed = 'completed'; - checked = 'checked'; - } - - template = template.replace('{{id}}', data[i].id); - template = template.replace('{{title}}', escape(data[i].title)); - template = template.replace('{{completed}}', completed); - template = template.replace('{{checked}}', checked); - - view = view + template; + if (data[i].completed) { + completed = 'completed'; + checked = 'checked'; } - return view; - }; + template = template.replace('{{id}}', data[i].id); + template = template.replace('{{title}}', escape(data[i].title)); + template = template.replace('{{completed}}', completed); + template = template.replace('{{checked}}', checked); - /** - * Displays a counter of how many to dos are left to complete - * - * @param {number} activeTodos The number of active todos. - * @returns {string} String containing the count - */ - Template.prototype.itemCounter = function (activeTodos) { - var plural = activeTodos === 1 ? '' : 's'; + view = view + template; + } - return '' + activeTodos + ' item' + plural + ' left'; - }; + return view; + }; - /** - * Updates the text within the "Clear completed" button - * - * @param {[type]} completedTodos The number of completed todos. - * @returns {string} String containing the count - */ - Template.prototype.clearCompletedButton = function (completedTodos) { - if (completedTodos > 0) { - return 'Clear completed'; - } else { - return ''; - } - }; + /** + * Displays a counter of how many to dos are left to complete + * + * @param {number} activeTodos The number of active todos. + * @returns {string} String containing the count + */ + Template.prototype.itemCounter = function (activeTodos) { + var plural = activeTodos === 1 ? '' : 's'; - // Export to window - window.app = window.app || {}; - window.app.Template = Template; - })(window); + return '' + activeTodos + ' item' + plural + ' left'; + }; + + /** + * Updates the text within the "Clear completed" button + * + * @param {[type]} completedTodos The number of completed todos. + * @returns {string} String containing the count + */ + Template.prototype.clearCompletedButton = function (completedTodos) { + if (completedTodos > 0) { + return 'Clear completed'; + } else { + return ''; + } + }; + + // Export to window + window.app = window.app || {}; + window.app.Template = Template; /***/ } diff --git a/karma.conf.js b/karma.conf.js index 1cf56c1..818bf8e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,3 +1,9 @@ +var path = require('path'); +var webpackConfig = require('./webpack.config'); +var entry = path.resolve(webpackConfig.context, webpackConfig.entry); +var preprocessors = {}; +preprocessors[entry] = ['webpack']; + // Karma configuration // Generated on Mon Aug 10 2015 05:47:13 GMT-0600 (MDT) @@ -14,12 +20,7 @@ module.exports = function (config) { // list of files / patterns to load in the browser - files: [ - 'test/**/*Spec.js', - 'js/helpers.js', - 'js/view.js', - 'js/controller.js' - ], + files: [entry], // list of files to exclude @@ -28,8 +29,9 @@ module.exports = function (config) { // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: {}, - + preprocessors: preprocessors, + webpack: webpackConfig, + webpackMiddleware: {noInfo: true}, // test results reporter to use // possible values: 'dots', 'progress' @@ -61,6 +63,12 @@ module.exports = function (config) { // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits - singleRun: false + singleRun: false, + + plugins: [ + require('karma-webpack'), + 'karma-jasmine', + 'karma-chrome-launcher' + ] }); }; diff --git a/package.json b/package.json index 2d75706..ec4c02b 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,22 @@ }, "devDependencies": { "babel": "5.8.21", - "babel-core": "5.8.21", + "babel-core": "5.8.22", "babel-loader": "5.3.2", "css-loader": "0.15.6", "jasmine-core": "2.3.4", + "karma": "0.13.8", + "karma-chrome-launcher": "0.2.0", + "karma-jasmine": "0.3.6", + "karma-webpack": "1.7.0", "style-loader": "0.12.3", "webpack": "1.11.0", "webpack-dev-server": "1.10.1" }, "scripts": { - "test": "karma start", - "test:single": "karma start --single-run", - "start": "webpack-dev-server" + "test": "NODE_ENV=test karma start", + "test:single": "NODE_ENV=test karma start --single-run", + "start": "webpack-dev-server", + "build": "webpack" } } diff --git a/test/ControllerSpec.js b/test/ControllerSpec.js index 78c3a67..3a953b8 100755 --- a/test/ControllerSpec.js +++ b/test/ControllerSpec.js @@ -1,4 +1,5 @@ /*global app, jasmine, describe, it, beforeEach, expect */ +require('../js/controller'); describe('controller', function () { 'use strict'; diff --git a/webpack.config.js b/webpack.config.js index 807bc4e..baf53ee 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,6 @@ var path = require('path'); -module.exports = { +var config = { entry: './app.js', output: { filename: 'bundle.js', @@ -9,11 +9,18 @@ module.exports = { context: here('js'), module: { loaders: [ - {test: /\.css$/, loaders: ['style', 'css']} + {test: /\.css$/, loader: 'style!css'} ] } }; +if (process.env.NODE_ENV === 'test') { + config.entry = './ControllerSpec.js'; + config.context = here('test'); +} + +module.exports = config; + function here(d) { return d ? path.join(__dirname, d) : __dirname; }