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; - }, + } }; });