diff --git a/karma.conf.js b/karma.conf.js
index 1e63603cf2..0b79578199 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -53,7 +53,16 @@ module.exports = function(config) {
"website/public/js/filters/filters.js",
- "website/public/js/directives/directives.js",
+ "website/public/js/directives/focus-me.directive.js",
+ "website/public/js/directives/from-now.directive.js",
+ "website/public/js/directives/habitrpg-tasks.directive.js",
+ "website/public/js/directives/hrpg-sort-checklist.directive.js",
+ "website/public/js/directives/hrpg-sort-tags.directive.js",
+ "website/public/js/directives/hrpg-sort-tasks.directive.js",
+ "website/public/js/directives/popover-html-popup.directive.js",
+ "website/public/js/directives/popover-html.directive.js",
+ "website/public/js/directives/task-focus.directive.js",
+ "website/public/js/directives/when-scrolled.directive.js",
"website/public/js/controllers/authCtrl.js",
"website/public/js/controllers/notificationCtrl.js",
@@ -70,7 +79,7 @@ module.exports = function(config) {
"website/public/js/controllers/hallCtrl.js",
'test/spec/mock/**/*.js',
'test/spec/specHelper.js',
- 'test/spec/*.js'
+ 'test/spec/**/*.js'
],
// list of files / patterns to exclude
diff --git a/test/spec/directives/focus-me.directive.spec.js b/test/spec/directives/focus-me.directive.spec.js
new file mode 100644
index 0000000000..ed016949c3
--- /dev/null
+++ b/test/spec/directives/focus-me.directive.spec.js
@@ -0,0 +1,28 @@
+'use strict';
+
+describe('focusMe Directive', function() {
+ var element, scope;
+
+ beforeEach(module('habitrpg'));
+
+ beforeEach(inject(function($rootScope, $compile) {
+ scope = $rootScope.$new();
+
+ element = "";
+
+ element = $compile(element)(scope);
+ scope.$digest();
+ }));
+
+ it('focuses the element when appended to the DOM', function() {
+ inject(function($timeout) {
+ var focusSpy = sinon.spy();
+
+ element.appendTo(document.body);
+ element.on('focus', focusSpy);
+
+ $timeout.flush();
+ expect(focusSpy).to.have.been.called;
+ });
+ });
+});
diff --git a/test/spec/directives/from-now.directive.spec.js b/test/spec/directives/from-now.directive.spec.js
new file mode 100644
index 0000000000..a197f2c331
--- /dev/null
+++ b/test/spec/directives/from-now.directive.spec.js
@@ -0,0 +1,89 @@
+'use strict';
+
+describe('fromNow Directive', function() {
+ var element, scope;
+ var fromNow = 'recently';
+ var diff = 0;
+
+ beforeEach(module('habitrpg'));
+
+ beforeEach(inject(function($rootScope, $compile) {
+ scope = $rootScope.$new();
+ scope.message = {};
+
+ sinon.stub(window, 'moment').returns({
+ fromNow: function() { return fromNow },
+ diff: function() { return diff }
+ });
+
+ element = "
";
+
+ element = $compile(element)(scope);
+ scope.$digest();
+ }));
+
+ afterEach(function() {
+ window.moment.restore();
+ });
+
+ it('sets the element text to the elapsed time', function() {
+ expect(element.text()).to.eql('recently');
+ });
+
+ describe('when the elapsed time is less than an hour', function() {
+ beforeEach(inject(function($compile) {
+ fromNow = 'recently';
+ diff = 0;
+
+ element = $compile('')(scope);
+ scope.$digest();
+ }));
+
+ it('updates the elapsed time every minute', inject(function($interval) {
+ fromNow = 'later';
+
+ expect(element.text()).to.eql('recently');
+ $interval.flush(60001);
+
+ expect(element.text()).to.eql('later');
+ }));
+
+ it('moves to hourly updates after an hour', inject(function($timeout, $interval) {
+ diff = 61;
+
+ $timeout.flush();
+ $interval.flush(60001);
+
+ fromNow = 'later';
+
+ $interval.flush(60001);
+ expect(element.text()).to.eql('recently');
+
+ $interval.flush(3600000);
+ expect(element.text()).to.eql('later');
+ }));
+ });
+
+ describe('when the elapsed time is more than an hour', function() {
+ beforeEach(inject(function($compile) {
+ fromNow = 'recently';
+ diff = 65;
+
+ element = $compile('')(scope);
+ scope.$digest();
+ }));
+
+ it('updates the elapsed time every hour', inject(function($interval) {
+ fromNow = 'later';
+
+ expect(element.text()).to.eql('recently');
+
+ $interval.flush(60001);
+ expect(element.text()).to.eql('recently');
+
+ $interval.flush(3600000);
+ expect(element.text()).to.eql('later');
+ }));
+ });
+
+});
diff --git a/website/public/js/directives/directives.js b/website/public/js/directives/directives.js
deleted file mode 100644
index fae365de9e..0000000000
--- a/website/public/js/directives/directives.js
+++ /dev/null
@@ -1,197 +0,0 @@
-'use strict';
-
-/**
- * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true.
- */
-habitrpg.directive('taskFocus',
- ['$timeout',
- function($timeout) {
- return function(scope, elem, attrs) {
- scope.$watch(attrs.taskFocus, function(newval) {
- if ( newval ) {
- $timeout(function() {
- elem[0].focus();
- }, 0, false);
- }
- });
- };
- }
-]);
-
-habitrpg.directive('whenScrolled', function() {
- return function(scope, elm, attr) {
- var raw = elm[0];
-
- elm.bind('scroll', function() {
- if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
- scope.$apply(attr.whenScrolled);
- }
- });
- };
-});
-
-habitrpg
- .directive('habitrpgTasks', ['$rootScope', 'User', function($rootScope, User) {
- return {
- restrict: 'EA',
- templateUrl: 'templates/habitrpg-tasks.html',
- //transclude: true,
- //scope: {
- // main: '@', // true if it's the user's main list
- // obj: '='
- //},
- controller: ['$scope', '$rootScope', function($scope, $rootScope){
- $scope.editTask = function(task){
- task._editing = !task._editing;
- task._tags = User.user.preferences.tagsCollapsed;
- task._advanced = User.user.preferences.advancedCollapsed;
- if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false;
- };
- }],
- link: function(scope, element, attrs) {
- // $scope.obj needs to come from controllers, so we can pass by ref
- scope.main = attrs.main;
- scope.modal = attrs.modal;
- var dailiesView;
- if(User.user.preferences.dailyDueDefaultView) {
- dailiesView = "remaining";
- } else {
- dailiesView = "all";
- }
- $rootScope.lists = [
- {
- header: window.env.t('habits'),
- type: 'habit',
- placeHolder: window.env.t('newHabit'),
- placeHolderBulk: window.env.t('newHabitBulk'),
- view: "all"
- }, {
- header: window.env.t('dailies'),
- type: 'daily',
- placeHolder: window.env.t('newDaily'),
- placeHolderBulk: window.env.t('newDailyBulk'),
- view: dailiesView
- }, {
- header: window.env.t('todos'),
- type: 'todo',
- placeHolder: window.env.t('newTodo'),
- placeHolderBulk: window.env.t('newTodoBulk'),
- view: "remaining"
- }, {
- header: window.env.t('rewards'),
- type: 'reward',
- placeHolder: window.env.t('newReward'),
- placeHolderBulk: window.env.t('newRewardBulk'),
- view: "all"
- }
- ];
-
- }
- }
- }]);
-
-habitrpg.directive('fromNow', ['$interval', function($interval){
- return function(scope, element, attr){
- var updateText = function(){ element.text(moment(scope.message.timestamp).fromNow()) };
- updateText();
- // Update the counter every 60secs if was sent less than one hour ago otherwise every hour
- // OPTIMIZATION, every time the interval is run, update the interval time
- var intervalTime = moment().diff(scope.message.timestamp, 'minute') < 60 ? 60000 : 3600000;
- var interval = $interval(function(){ updateText() }, intervalTime, false);
- scope.$on('$destroy', function() {
- $interval.cancel(interval);
- });
- }
-}]);
-
-habitrpg.directive('hrpgSortTasks', ['User', function(User) {
- return function($scope, element, attrs, ngModel) {
- $(element).sortable({
- axis: "y",
- distance: 5,
- start: function (event, ui) {
- ui.item.data('startIndex', ui.item.index());
- },
- stop: function (event, ui) {
- var task = angular.element(ui.item[0]).scope().task,
- startIndex = ui.item.data('startIndex');
- User.user.ops.sortTask({ params: {id: task.id}, query: {from: startIndex, to: ui.item.index()} });
- }
- });
- }
-}]);
-
-habitrpg.directive('hrpgSortChecklist', ['User', function(User) {
- return function($scope, element, attrs, ngModel) {
- $(element).sortable({
- axis: "y",
- distance: 5,
- start: function (event, ui) {
- ui.item.data('startIndex', ui.item.index());
- },
- stop: function (event, ui) {
- var task = angular.element(ui.item[0]).scope().task,
- startIndex = ui.item.data('startIndex');
- //$scope.saveTask(task, true);
- $scope.swapChecklistItems(task, startIndex, ui.item.index());
- }
- });
- }
-}]);
-
-habitrpg.directive('hrpgSortTags', ['User', function(User) {
- return function($scope, element, attrs, ngModel) {
- $(element).sortable({
- start: function (event, ui) {
- ui.item.data('startIndex', ui.item.index());
- },
- stop: function (event, ui) {
- User.user.ops.sortTag({query:{ from: ui.item.data('startIndex'), to:ui.item.index() }});
- }
- });
- }
-}]);
-
-habitrpg
- .directive( 'popoverHtmlPopup', ['$sce', function($sce) {
- return {
- restrict: 'EA',
- replace: true,
- scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
- link: function(scope, element, attrs) {
- scope.$watch('content', function(value, oldValue) {
- scope.unsafeContent = $sce.trustAsHtml(scope.content);
- });
- },
- templateUrl: 'template/popover/popover-html.html'
- };
- }])
- .directive( 'popoverHtml', [ '$compile', '$timeout', '$parse', '$window', '$tooltip',
- function ( $compile, $timeout, $parse, $window, $tooltip ) {
- return $tooltip( 'popoverHtml', 'popover', 'click' );
- }
- ])
- .run(["$templateCache", function($templateCache) {
- $templateCache.put("template/popover/popover-html.html",
- "\n" +
- "
\n" +
- "\n" +
- "
\n" +
- "
\n" +
- "
\n" +
- "
\n" +
- "
\n");
- }]);
-
-habitrpg.directive('focusMe', ['$timeout', '$parse', function($timeout, $parse) {
- return {
- link: function(scope, element, attrs) {
- var model = $parse(attrs.focusMe);
- scope.$watch(model, function(value) {
- $timeout(function() {
- element[0].focus();
- });
- });
- }
- };
-}]);
diff --git a/website/public/js/directives/focus-me.directive.js b/website/public/js/directives/focus-me.directive.js
new file mode 100644
index 0000000000..e2fd96657c
--- /dev/null
+++ b/website/public/js/directives/focus-me.directive.js
@@ -0,0 +1,23 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('focusMe', focusMe);
+
+focusMe.$inject = [
+ '$timeout',
+ '$parse'
+];
+
+function focusMe($timeout, $parse) {
+ return {
+ link: function(scope, element, attrs) {
+ var model = $parse(attrs.focusMe);
+ scope.$watch(model, function(value) {
+ $timeout(function() {
+ element[0].focus();
+ });
+ });
+ }
+ }
+}
diff --git a/website/public/js/directives/from-now.directive.js b/website/public/js/directives/from-now.directive.js
new file mode 100644
index 0000000000..37b61a2b90
--- /dev/null
+++ b/website/public/js/directives/from-now.directive.js
@@ -0,0 +1,44 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('fromNow', fromNow);
+
+fromNow.$inject = [
+ '$interval',
+ '$timeout'
+];
+
+function fromNow($interval, $timeout) {
+ return function(scope, element, attr){
+ var interval, timeout;
+
+ var updateText = function(){
+ element.text(moment(scope.message.timestamp).fromNow());
+ };
+
+ var setupInterval = function() {
+ if(interval) $interval.cancel(interval);
+ if(timeout) $timeout.cancel(timeout);
+
+ var diff = moment().diff(scope.message.timestamp, 'minute');
+
+ if(diff < 60) {
+ // Update every minute
+ interval = $interval(updateText, 60000, false);
+ timeout = $timeout(setupInterval, diff * 60000);
+ } else {
+ // Update every hour
+ interval = $interval(updateText, 3600000, false);
+ }
+ };
+
+ updateText();
+ setupInterval();
+
+ scope.$on('$destroy', function() {
+ if(interval) $interval.cancel(interval);
+ if(timeout) $timeout.cancel(timeout);
+ });
+ }
+}
diff --git a/website/public/js/directives/habitrpg-tasks.directive.js b/website/public/js/directives/habitrpg-tasks.directive.js
new file mode 100644
index 0000000000..1ec1a5b93a
--- /dev/null
+++ b/website/public/js/directives/habitrpg-tasks.directive.js
@@ -0,0 +1,69 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('habitrpgTasks', habitrpgTasks);
+
+habitrpgTasks.$inject = [
+ '$rootScope',
+ 'User'
+];
+
+function habitrpgTasks($rootScope, User) {
+ return {
+ restrict: 'EA',
+ templateUrl: 'templates/habitrpg-tasks.html',
+ //transclude: true,
+ //scope: {
+ // main: '@', // true if it's the user's main list
+ // obj: '='
+ //},
+ controller: ['$scope', '$rootScope', function($scope, $rootScope){
+ $scope.editTask = function(task){
+ task._editing = !task._editing;
+ task._tags = User.user.preferences.tagsCollapsed;
+ task._advanced = User.user.preferences.advancedCollapsed;
+ if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false;
+ };
+ }],
+ link: function(scope, element, attrs) {
+ // $scope.obj needs to come from controllers, so we can pass by ref
+ scope.main = attrs.main;
+ scope.modal = attrs.modal;
+ var dailiesView;
+ if(User.user.preferences.dailyDueDefaultView) {
+ dailiesView = "remaining";
+ } else {
+ dailiesView = "all";
+ }
+ $rootScope.lists = [
+ {
+ header: window.env.t('habits'),
+ type: 'habit',
+ placeHolder: window.env.t('newHabit'),
+ placeHolderBulk: window.env.t('newHabitBulk'),
+ view: "all"
+ }, {
+ header: window.env.t('dailies'),
+ type: 'daily',
+ placeHolder: window.env.t('newDaily'),
+ placeHolderBulk: window.env.t('newDailyBulk'),
+ view: dailiesView
+ }, {
+ header: window.env.t('todos'),
+ type: 'todo',
+ placeHolder: window.env.t('newTodo'),
+ placeHolderBulk: window.env.t('newTodoBulk'),
+ view: "remaining"
+ }, {
+ header: window.env.t('rewards'),
+ type: 'reward',
+ placeHolder: window.env.t('newReward'),
+ placeHolderBulk: window.env.t('newRewardBulk'),
+ view: "all"
+ }
+ ];
+
+ }
+ }
+}
diff --git a/website/public/js/directives/hrpg-sort-checklist.directive.js b/website/public/js/directives/hrpg-sort-checklist.directive.js
new file mode 100644
index 0000000000..52211e8cd4
--- /dev/null
+++ b/website/public/js/directives/hrpg-sort-checklist.directive.js
@@ -0,0 +1,30 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('hrpgSortChecklist', hrpgSortChecklist);
+
+hrpgSortChecklist.$inject = [
+ 'User'
+];
+
+function hrpgSortChecklist(User) {
+ return function($scope, element, attrs, ngModel) {
+ $(element).sortable({
+ axis: "y",
+ distance: 5,
+ start: function (event, ui) {
+ ui.item.data('startIndex', ui.item.index());
+ },
+ stop: function (event, ui) {
+ var task = angular.element(ui.item[0]).scope().task;
+ var startIndex = ui.item.data('startIndex');
+ $scope.swapChecklistItems(
+ task,
+ startIndex,
+ ui.item.index()
+ );
+ }
+ });
+ }
+}
diff --git a/website/public/js/directives/hrpg-sort-tags.directive.js b/website/public/js/directives/hrpg-sort-tags.directive.js
new file mode 100644
index 0000000000..3eac5fe0a6
--- /dev/null
+++ b/website/public/js/directives/hrpg-sort-tags.directive.js
@@ -0,0 +1,27 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('hrpgSortTags', hrpgSortTags);
+
+hrpgSortTags.$inject = [
+ 'User'
+];
+
+function hrpgSortTags(User) {
+ return function($scope, element, attrs, ngModel) {
+ $(element).sortable({
+ start: function (event, ui) {
+ ui.item.data('startIndex', ui.item.index());
+ },
+ stop: function (event, ui) {
+ User.user.ops.sortTag({
+ query: {
+ from: ui.item.data('startIndex'),
+ to:ui.item.index()
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/website/public/js/directives/hrpg-sort-tasks.directive.js b/website/public/js/directives/hrpg-sort-tasks.directive.js
new file mode 100644
index 0000000000..0e18091cf2
--- /dev/null
+++ b/website/public/js/directives/hrpg-sort-tasks.directive.js
@@ -0,0 +1,32 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('hrpgSortTasks', hrpgSortTasks);
+
+hrpgSortTasks.$inject = [
+ 'User'
+];
+
+function hrpgSortTasks(User) {
+ return function($scope, element, attrs, ngModel) {
+ $(element).sortable({
+ axis: "y",
+ distance: 5,
+ start: function (event, ui) {
+ ui.item.data('startIndex', ui.item.index());
+ },
+ stop: function (event, ui) {
+ var task = angular.element(ui.item[0]).scope().task;
+ var startIndex = ui.item.data('startIndex');
+ User.user.ops.sortTask({
+ params: { id: task.id },
+ query: {
+ from: startIndex,
+ to: ui.item.index()
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/website/public/js/directives/popover-html-popup.directive.js b/website/public/js/directives/popover-html-popup.directive.js
new file mode 100644
index 0000000000..555a7019d1
--- /dev/null
+++ b/website/public/js/directives/popover-html-popup.directive.js
@@ -0,0 +1,45 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('popoverHtmlPopup', popoverHtmlPopup)
+ .run(loadPopupTemplate);
+
+popoverHtmlPopup.$inject = [
+ '$sce'
+];
+
+function popoverHtmlPopup($sce) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
+ link: function(scope, element, attrs) {
+ scope.$watch('content', function(value, oldValue) {
+ scope.unsafeContent = $sce.trustAsHtml(scope.content);
+ });
+ },
+ templateUrl: 'template/popover/popover-html.html'
+ };
+}
+
+/*
+ * TODO: Review whether it's appropriate to be seeding this into the
+ * templateCache like this. Feel like this might be an antipattern?
+ */
+
+loadPopupTemplate.$inject = [
+ '$templateCache'
+];
+
+function loadPopupTemplate($templateCache) {
+ $templateCache.put("template/popover/popover-html.html",
+ "\n" +
+ "
\n" +
+ "\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n");
+}
diff --git a/website/public/js/directives/popover-html.directive.js b/website/public/js/directives/popover-html.directive.js
new file mode 100644
index 0000000000..0462d99a31
--- /dev/null
+++ b/website/public/js/directives/popover-html.directive.js
@@ -0,0 +1,17 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('popoverHtml', popoverHtml);
+
+popoverHtml.$inject = [
+ '$compile',
+ '$timeout',
+ '$parse',
+ '$window',
+ '$tooltip'
+];
+
+function popoverHtml($compile, $timeout, $parse, $window, $tooltip) {
+ return $tooltip('popoverHtml', 'popover', 'click');
+}
diff --git a/website/public/js/directives/task-focus.directive.js b/website/public/js/directives/task-focus.directive.js
new file mode 100644
index 0000000000..391e42b7fe
--- /dev/null
+++ b/website/public/js/directives/task-focus.directive.js
@@ -0,0 +1,24 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('taskFocus', taskFocus);
+
+taskFocus.$inject = ['$timeout'];
+
+/**
+ * Directive that places focus on the element it is applied to when the
+ * expression it binds to evaluates to true.
+ */
+
+function taskFocus($timeout) {
+ return function(scope, elem, attrs) {
+ scope.$watch(attrs.taskFocus, function(newVal) {
+ if (newVal) {
+ $timeout(function() {
+ elem[0].focus();
+ }, 0, false);
+ }
+ });
+ }
+}
diff --git a/website/public/js/directives/when-scrolled.directive.js b/website/public/js/directives/when-scrolled.directive.js
new file mode 100644
index 0000000000..3a063ca439
--- /dev/null
+++ b/website/public/js/directives/when-scrolled.directive.js
@@ -0,0 +1,17 @@
+'use strict';
+
+angular
+ .module('habitrpg')
+ .directive('whenScrolled', whenScrolled);
+
+function whenScrolled() {
+ return function(scope, elm, attr) {
+ var raw = elm[0];
+
+ elm.bind('scroll', function() {
+ if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
+ scope.$apply(attr.whenScrolled);
+ }
+ });
+ };
+}
diff --git a/website/public/manifest.json b/website/public/manifest.json
index 8bb5d7d275..7436040c57 100644
--- a/website/public/manifest.json
+++ b/website/public/manifest.json
@@ -50,7 +50,16 @@
"js/filters/filters.js",
- "js/directives/directives.js",
+ "js/directives/focus-me.directive.js",
+ "js/directives/from-now.directive.js",
+ "js/directives/habitrpg-tasks.directive.js",
+ "js/directives/hrpg-sort-checklist.directive.js",
+ "js/directives/hrpg-sort-tags.directive.js",
+ "js/directives/hrpg-sort-tasks.directive.js",
+ "js/directives/popover-html-popup.directive.js",
+ "js/directives/popover-html.directive.js",
+ "js/directives/task-focus.directive.js",
+ "js/directives/when-scrolled.directive.js",
"js/controllers/authCtrl.js",
"js/controllers/notificationCtrl.js",