diff --git a/applications/default/extensions/form/public/templates/form.html b/applications/default/extensions/form/public/templates/form.html
index 7c17f68..c2b1330 100644
--- a/applications/default/extensions/form/public/templates/form.html
+++ b/applications/default/extensions/form/public/templates/form.html
@@ -4,5 +4,5 @@
{{error}}
-
+
diff --git a/applications/default/extensions/form/public/templates/subform.html b/applications/default/extensions/form/public/templates/subform.html
index 5b367ec..0ef73b7 100644
--- a/applications/default/extensions/form/public/templates/subform.html
+++ b/applications/default/extensions/form/public/templates/subform.html
@@ -6,5 +6,5 @@ Edit {{element.subform.title | lowercase}}
{{error}}
-
+
diff --git a/applications/default/public/js/app.js b/applications/default/public/js/app.js
index 2e7ae9e..4ee608e 100644
--- a/applications/default/public/js/app.js
+++ b/applications/default/public/js/app.js
@@ -1,7 +1,30 @@
+/**
+ * @file Main AngularJS module for the choko application.
+ */
+
'use strict';
-// Declare app level module which depends on services, directives and filters.
-angular.module('choko', ['ngRoute', 'ngResource', 'ngSanitize', 'summernote', 'angularFileUpload', 'choko.services', 'choko.directives', 'choko.filters'])
-.config(['$locationProvider', function($locationProvider) {
- //$locationProvider.html5Mode(true);
-}]);
+// Define core choko dependencies.
+var dependencies = [
+ 'ngRoute',
+ 'ngResource',
+ 'ngSanitize',
+ 'summernote',
+ 'angularFileUpload'
+];
+
+// Declare main choko module.
+angular.module('choko', dependencies)
+
+ // Define current choko version.
+ // @todo: we should read package.json and make available not only a version
+ // value but other metadata that might be used thoughout the application.
+ .value('version', '0.0.4')
+
+ // Location/routing configuration.
+ .config(['$locationProvider', function($locationProvider) {
+
+ // Use HTML5 mode to remove "#" symbols from angular-routed pages.
+ // $locationProvider.html5Mode(true);
+
+ }]);
diff --git a/applications/default/public/js/controllers.js b/applications/default/public/js/controllers.js
index ebd35df..06bc445 100644
--- a/applications/default/public/js/controllers.js
+++ b/applications/default/public/js/controllers.js
@@ -1,3 +1,7 @@
+/**
+ * @file Choko core controllers.
+ */
+
'use strict';
function ApplicationController($scope, $location, $http, applicationState, Choko) {
@@ -101,7 +105,7 @@ function RegionController($scope, $location, applicationState, Choko) {
//RegionController.$inject = ['$scope', '$location', 'applicationState', 'Choko'];
function NavigationController($scope, $location, $window, applicationState, Choko) {
- $scope.panel.classes.unshift('nav');
+ ($scope.panel.classes = $scope.panel.classes || []).unshift('nav');
$scope.isAbsolute = function(url) {
return /^(?:[a-z]+:)?\/\//i.test(url);
diff --git a/applications/default/public/js/directives.js b/applications/default/public/js/directives.js
index bfde69b..78c1fce 100644
--- a/applications/default/public/js/directives.js
+++ b/applications/default/public/js/directives.js
@@ -1,42 +1,144 @@
+/**
+ * @file Choko core directives.
+ */
+
'use strict';
-/* Directives */
+// Append directives to main choko module.
+angular.module('choko')
-angular.module('choko.directives', [])
+ // Directive to return the application's version.
.directive('appVersion', ['version', function(version) {
return function(scope, elm, attrs) {
elm.text(version);
};
}])
- .directive('ckReplace', function($http, $compile) {
+
+ // Directive to replace any tag with overridable templates from the server.
+ .directive('ckReplace', ['$http', '$compile', function($http, $compile) {
return {
- restrict: 'E',
- scope: true,
+ restrict: 'EA',
+
+ // As this directive will replace the existing markup, it's better that
+ // we run it previously to most other directives, to avoid dumb processing.
+ priority: 100,
compile: function(element, attrs) {
return function(scope, element, attrs) {
- scope.element.template = scope.element.template || 'templates/' + scope.element.type + '.html';
- $http({method: 'GET', url: scope.element.template, cache: true}).then(function(result) {
- var template = angular.element($compile(result.data)(scope));
- element.replaceWith(template);
+
+ // Request the template content.
+ var loadTemplate = $http({
+ method: 'GET',
+ // If the directive is an element, "src" should be available.
+ url: attrs.ckReplace || attrs.src,
+ cache: true
+ });
+
+ // When ready, compile the retrieved template.
+ loadTemplate.then(function(result) {
+
+ // Compile the returned template.
+ var compiled = $compile(result.data)(scope);
+
+ // Replace old element with compiled one.
+ element.replaceWith(angular.element(compiled));
+
});
};
}
};
- })
- .directive('ckButton', function($http, $compile) {
+ }])
+
+ // A helper service to handle re-compiling of directives.
+ .factory('ckReplaceAndRecompile', ['$compile', function ($compile) {
+ /**
+ * Creates a new element from the given, copying attributes but removing
+ * the old directive to avoid running it again. Recompiles the new
+ * element and replaces the old with it.
+ */
+ return function (element, directiveToRemove, scope, newTag) {
+
+ // Handle multiple removals using a array of removing directives.
+ var directives = directiveToRemove.length ? directiveToRemove : [];
+ var tagName = element.prop('localName');
+ var replacement;
+
+ // Replace the directive, be it a tag name or attribute.
+ directives.forEach(function (directive) {
+ if (tagName == directive) {
+ replacement = angular.element(document.createElement(newTag || 'div'));
+ angular.element.each(element[0].attributes, function (i, attr) {
+ replacement.attr(attr.name, attr.value);
+ });
+ element.replaceWith(replacement);
+ element = replacement;
+ } else {
+ element.removeAttr(directive);
+ }
+ });
+
+ // Recompile the element.
+ $compile(element)(scope || {});
+ }
+ }])
+
+ // Directive to replace form elements with overridable templates from
+ // the server.
+ .directive('ckReplaceElement', ['ckReplaceAndRecompile', function(ckReplaceAndRecompile) {
+ return {
+ restrict: 'EA',
+ // This directive will insert other directives dynamically. We set
+ // priority=999 (high number) to make it run first, but nor before core
+ // Angular directives, as we need their data. (ng-repeat runs at 1000
+ // priority).
+ priority: 999,
+ terminal: true,
+ compile: function (tElement, attrs) {
+ return function(scope, element, attrs) {
+
+ // Allow for custom templates but fallback to default one based
+ // on element type.
+ scope.element.template = scope.element.template || 'templates/' + scope.element.type + '.html';
+
+ // Append the ck-replace directive.
+ element.attr('ck-replace', scope.element.template);
+
+ ckReplaceAndRecompile(element, ['ck-replace-element', 'ng-repeat'], scope);
+ }
+ }
+ };
+ }])
+
+ // Handles button or button groups for navigation bars.
+ // @todo This directive is specifically used by the navigation extension.
+ // Therefore it should be moved to this extension's directory.
+ .directive('ckButton', ['ckReplaceAndRecompile', function(ckReplaceAndRecompile) {
return {
- restrict: 'E',
+ restrict: 'EA',
scope: true,
+ // This directive will insert other directives dynamically. We set
+ // priority=999 (high number) to make it run first, but nor before core
+ // Angular directives, as we need their data. (ng-repeat runs at 1000
+ // priority).
+ priority: 999,
+ terminal: true,
compile: function(element, attrs) {
return function(scope, element, attrs) {
+
+ // Add bootstrap button class.
+ scope.item.classes = scope.item.classes || [];
+ if (scope.item.classes.indexOf('btn-default')+1) {
+ scope.item.classes.push('btn-default');
+ }
+
+ // @todo: we should probably allow for custom templates, as we do in
+ // ckReplaceElement directive above.
var template = scope.item.items ? '/templates/btn-group-dropdown.html' : '/templates/btn-group-button.html';
- scope.item.classes = scope.item.classes || ['btn-default'];
+
+ // Append the replacement directive.
+ element.attr('ck-replace', template);
- $http({method: 'GET', url: template, cache: true}).then(function(result) {
- var template = angular.element($compile(result.data)(scope));
- element.replaceWith(template);
- });
+ ckReplaceAndRecompile(element, ['ck-button', 'ng-repeat'], scope);
};
}
};
- });
\ No newline at end of file
+ }]);
diff --git a/applications/default/public/js/filters.js b/applications/default/public/js/filters.js
index 71cd7a6..28c33c9 100644
--- a/applications/default/public/js/filters.js
+++ b/applications/default/public/js/filters.js
@@ -1,18 +1,23 @@
+/**
+ * @file Choko core filters.
+ */
+
'use strict';
-/* Filters */
+angular.module('choko')
-angular.module('choko.filters', [])
- .filter('interpolate', ['version', function(version) {
- return function(text) {
- return String(text).replace(/\%VERSION\%/mg, version);
- }
- }])
+ // Filter to get an array of keys for an object.
.filter('keys', function() {
- return function(input) {
- if (!input) {
- return [];
- }
- return Object.keys(input);
+
+ /**
+ * Returns the keys of a given acceptable value/object.
+ * @param {object|array|function} input
+ * @return {array}
+ */
+ function objectKeysFilter(input) {
+ return ~['object', 'function'].indexOf(typeof input) ? Object.keys(input) : [];
}
+
+ return objectKeysFilter;
});
+
\ No newline at end of file
diff --git a/applications/default/public/js/services.js b/applications/default/public/js/services.js
index c5a0941..a99254e 100644
--- a/applications/default/public/js/services.js
+++ b/applications/default/public/js/services.js
@@ -1,29 +1,38 @@
+/**
+ * @file Choko core services.
+ */
+
'use strict';
-/* Services */
-angular.module('choko.services', [])
- // Single value service for Choko version.
- .value('version', '0.0.1')
+// Append services to main choko module.
+angular.module('choko')
+
+ // Choko main REST factory.
+ .factory('Choko', ['$resource', function($resource) {
- .factory('Choko', function($resource) {
- return $resource('/rest/:type/:key', {
+ var url = '/rest/:type/:key';
+ var defaultParams = {
type: '@type',
key: '@key'
- },
- {
+ };
+ var actions = {
'get': {
method: 'GET',
transformResponse: function (data) {
return angular.fromJson(data).data;
},
- // Data is an Object, not an Array.
+ // Server will always return an object containing at least a 'data'
+ // property to hold the actual data and a status property.
isArray: false
}
- });
- })
+ }
- // Shared server with application state.
- .factory('applicationState', function($rootScope) {
+ return $resource(url, defaultParams, actions);
+ }])
+
+ // Application state wrapper, to be shared across controllers.
+ // P.s.: States are actual scope objects.
+ .factory('applicationState', function() {
var state = {};
return {
get: function() {
@@ -31,6 +40,6 @@ angular.module('choko.services', [])
},
set: function(newState) {
return state = newState;
- },
+ }
};
});