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",