Merge pull request #5370 from gisikw/split-out-directives

Split out Angular directives
This commit is contained in:
Blade Barringer 2015-06-09 07:42:12 -05:00
commit dabb6244e7
15 changed files with 466 additions and 200 deletions

View file

@ -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

View file

@ -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 = "<input focus-me></input>";
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;
});
});
});

View file

@ -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 = "<p from-now></p>";
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('<p from-now></p>')(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('<p from-now></p>')(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');
}));
});
});

View file

@ -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",
"<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" +
" <div class=\"arrow\"></div>\n" +
"\n" +
" <div class=\"popover-inner\">\n" +
" <h3 class=\"popover-title\" ng-bind=\"title\" ng-show=\"title\"></h3>\n" +
" <div class=\"popover-content\" ng-bind-html=\"unsafeContent\" style=\"word-wrap: break-word\"> </div>\n" +
" </div>\n" +
"</div>\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();
});
});
}
};
}]);

View file

@ -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();
});
});
}
}
}

View file

@ -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);
});
}
}

View file

@ -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"
}
];
}
}
}

View file

@ -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()
);
}
});
}
}

View file

@ -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()
}
});
}
});
}
}

View file

@ -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()
}
});
}
});
}
}

View file

@ -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",
"<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" +
" <div class=\"arrow\"></div>\n" +
"\n" +
" <div class=\"popover-inner\">\n" +
" <h3 class=\"popover-title\" ng-bind=\"title\" ng-show=\"title\"></h3>\n" +
" <div class=\"popover-content\" ng-bind-html=\"unsafeContent\" style=\"word-wrap: break-word\"> </div>\n" +
" </div>\n" +
"</div>\n");
}

View file

@ -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');
}

View file

@ -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);
}
});
}
}

View file

@ -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);
}
});
};
}

View file

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