diff --git a/frontend/src/core/helpers.js b/frontend/src/core/helpers.js index ec882aafb3..0765d47a7e 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -1,413 +1,336 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Handlebars = require('handlebars'); - var Origin = require('core/origin'); - var moment = require('moment'); - - var helpers = { - console: function(context) { - return console.log(context); - }, - lowerCase: function(text) { - return text.toLowerCase(); - }, - numbers: function(index) { - return index +1; - }, - capitalise: function(text) { - return text.charAt(0).toUpperCase() + text.slice(1); - }, - odd: function (index) { - return (index +1) % 2 === 0 ? 'even' : 'odd'; - }, - stringToClassName: function(text) { - if (!text) { - return; - } - // Check if first character is an underscore and remove - // Normally used for attribute with '_'s - if (text.slice(1) === '_') { - text = text.slice(1); - } - // Remove _ and spaces with dashes - return text.replace(/_| /g, "-").toLowerCase(); - }, - keyToTitleString: function(key) { - if (!key) { - return; - } - // Take in key value and remove all _'s and capitalise - var string = key.replace(/_/g, "").toLowerCase(); - return this.capitalise(string); - }, - formatDate: function(timestamp, noZero) { - var noDisplay = '-'; - // 2014-02-17T17:00:34.196Z - if (typeof(timestamp) !== 'undefined') { - var date = new Date(timestamp); - - // optionally use noDisplay char if 0 dates are to be interpreted as such - if (noZero && 0 === date.valueOf()) { - return noDisplay; - } - - return date.toDateString(); - } + var Handlebars = require('handlebars'); + var Origin = require('core/origin'); + var Moment = require('moment'); + + var helpers = { + console: function(context) { + return console.log(context); + }, + + lowerCase: function(text) { + return text.toLowerCase(); + }, + + numbers: function(index) { + return index+1; + }, + + capitalise: function(text) { + return text.charAt(0).toUpperCase() + text.slice(1); + }, + + odd: function (index) { + return (index +1) % 2 === 0 ? 'even' : 'odd'; + }, + + stringToClassName: function(text) { + if (!text) return; + // Check if first character is an underscore and remove + // Normally used for attribute with '_'s + if (text.slice(1) === '_') { + text = text.slice(1); + } + // Remove _ and spaces with dashes + return text.replace(/_| /g, "-").toLowerCase(); + }, + + keyToTitleString: function(key) { + if (!key) return; + // Take in key value and remove all _'s and capitalise + var string = key.replace(/_/g, "").toLowerCase(); + return this.capitalise(string); + }, + + momentFormat: function(date, format) { + if (typeof date == 'undefined') { + return '-'; + } + return Moment(date).format(format); + }, - return noDisplay; - }, - momentFormat: function(date, format) { - if (typeof date == 'undefined') { - return '-'; - } - - return moment(date).format(format); - }, - formatDuration: function(duration) { - var zero = '0', hh, mm, ss; - var time = new Date(0, 0, 0, 0, 0, Math.floor(duration), 0); - - hh = time.getHours(); - mm = time.getMinutes(); - ss = time.getSeconds(); - - // Pad zero values to 00 - hh = (zero+hh).slice(-2); - mm = (zero+mm).slice(-2); - ss = (zero+ss).slice(-2); - - return hh + ':' + mm + ':' + ss; - }, - // checks for http/https and www. prefix - isAssetExternal: function(url) { - if (url && url.length > 0) { - var urlRegEx = new RegExp(/^(https?:\/\/)|^(www\.)/); - return url.match(urlRegEx) !== null; - } else { - return true; - } - }, - ifValueEquals: function(value, text, block) { - if (value === text) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - ifUserIsMe: function(userId, block) { - if (userId === Origin.sessionModel.get('id')) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - selected: function(option, value){ - if (option === value) { - return ' selected'; - } else { - return '' - } - }, - counterFromZero: function(n, block) { - var sum = ''; - for (var i = 0; i <= n; ++i) - sum += block.fn(i); - return sum; - }, - counterFromOne: function(n, block) { - var sum = ''; - for (var i = 1; i <= n; ++i) - sum += block.fn(i); - return sum; - }, - t: function(str, options) { - for (var placeholder in options.hash) { - options[placeholder] = options.hash[placeholder]; - } - return Origin.l10n.t(str, options); - }, - stripHtml: function(html) { - return new Handlebars.SafeString(html); - }, - bytesToSize: function(bytes) { - if (bytes == 0) return '0 B'; - - var k = 1000, - sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], - i = Math.floor(Math.log(bytes) / Math.log(k)); - - return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; - }, - renderBooleanOptions: function(selectedValue) { - var options = ["true", "false"]; - var html = ''; - - for (var i = 0; i < options.length; i++) { - var selected = selectedValue == options[i] ? ' selected' : ''; - html += ''; - } - - return new Handlebars.SafeString(html); - }, - pickCSV: function (list, key, separator) { - var vals = []; - separator = (separator && separator.length) ? separator : ','; - if (list && list.length) { - for (var i = 0; i < list.length; ++i) { - if (key && list[i][key]) { - vals.push(list[i][key]); - } else { - vals.push(list[i]); - } - } - } - return vals.join(separator); - }, - renderTags: function(list, key) { - var html = ''; + formatDuration: function(duration) { + var zero = '0', hh, mm, ss; + var time = new Date(0, 0, 0, 0, 0, Math.floor(duration), 0); - if (list && list.length) { - html = '' - } + // checks for http/https and www. prefix + isAssetExternal: function(url) { + if (!url || !url.length) { + return true; + } + var urlRegEx = new RegExp(/^(https?:\/\/)|^(www\.)/); + return url.match(urlRegEx) !== null; + }, + + ifValueEquals: function(value, text, block) { + return (value === text) ? block.fn(this) : block.inverse(this); + }, + + ifUserIsMe: function(userId, block) { + var isMe = userId === Origin.sessionModel.get('id'); + return isMe ? block.fn(this) : block.inverse(this); + }, + + selected: function(option, value){ + return (option === value) ? ' selected' : ''; + }, + + counterFromZero: function(n, block) { + var sum = ''; + for (var i = 0; i <= n; ++i) sum += block.fn(i); + return sum; + }, + + counterFromOne: function(n, block) { + var sum = ''; + for (var i = 1; i <= n; ++i) sum += block.fn(i); + return sum; + }, + + t: function(str, options) { + for (var placeholder in options.hash) { + options[placeholder] = options.hash[placeholder]; + } + return Origin.l10n.t(str, options); + }, - return html; - }, - decodeHTML: function(html) { - var el = document.createElement('div'); - el.innerHTML = html; - return el.childNodes.length === 0 ? "" : el.childNodes[0].nodeValue; - }, - - ifHasPermissions: function(permissions, block) { - var permissionsArray = permissions.split(','); - if (Origin.permissions.hasPermissions(permissions)) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, + stripHtml: function(html) { + return new Handlebars.SafeString(html); + }, - ifMailEnabled: function(block) { - if (Origin.constants.useSmtp === true) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, + bytesToSize: function(bytes) { + if (bytes == 0) return '0 B'; - ifImageIsCourseAsset: function(url, block) { - if (url.length !== 0 && url.indexOf('course/assets') == 0) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - - ifAssetIsExternal: function(url, block) { - if(Handlebars.helpers.isAssetExternal(url)) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - - ifAssetIsHeroImage: function(url, block) { - var urlSplit = url.split('/') - if (urlSplit.length === 1) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - - copyStringToClipboard: function(data) { - - var textArea = document.createElement("textarea"); - - textArea.value = data; - - // Place in top-left corner of screen regardless of scroll position. - textArea.style.position = 'fixed'; - textArea.style.top = 0; - textArea.style.left = 0; - - // Ensure it has a small width and height. Setting to 1px / 1em - // doesn't work as this gives a negative w/h on some browsers. - textArea.style.width = '2em'; - textArea.style.height = '2em'; - - // We don't need padding, reducing the size if it does flash render. - textArea.style.padding = 0; - - // Clean up any borders. - textArea.style.border = 'none'; - textArea.style.outline = 'none'; - textArea.style.boxShadow = 'none'; - - // Avoid flash of white box if rendered for any reason. - textArea.style.background = 'transparent'; - - document.body.appendChild(textArea); - - textArea.select(); - - var success = document.execCommand('copy'); - - document.body.removeChild(textArea); - - return success; - }, - - // checks for at least one child object - validateCourseContent: function(currentCourse, callback) { - var containsAtLeastOneChild = true; - var alerts = []; - var iterateOverChildren = function(model, index, doneIterator) { - if(!model._childTypes) { - return doneIterator(); - } - model.fetchChildren(function(currentChildren) { - if (currentChildren.length > 0) { - return helpers.forParallelAsync(currentChildren, iterateOverChildren, doneIterator); - } - containsAtLeastOneChild = false; - var children = _.isArray(model._childTypes) ? model._childTypes.join('/') : model._childTypes; - alerts.push(model.get('_type') + " '" + model.get('title') + "' missing " + children); - doneIterator(); - }); - }; - // start recursion - iterateOverChildren(currentCourse, null, function() { - var errorMessage = ""; - if(alerts.length > 0) { - for(var i = 0, len = alerts.length; i < len; i++) { - errorMessage += "
  • " + alerts[i] + "
  • "; - } - return callback(new Error(errorMessage)); - } - callback(null, true); - }); - }, - - isValidEmail: function(value) { - var regEx = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - if (value.length === 0 || !regEx.test(value)) { - return false; + var k = 1000; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; + }, + + renderBooleanOptions: function(selectedValue) { + var options = ["true", "false"]; + var html = ''; + + for (var i = 0; i < options.length; i++) { + var selected = selectedValue == options[i] ? ' selected' : ''; + html += ''; + } + return new Handlebars.SafeString(html); + }, + + pickCSV: function (list, key, separator) { + var vals = []; + separator = (separator && separator.length) ? separator : ','; + if (list && list.length) { + return vals.join(separator); + } + for (var i = 0; i < list.length; ++i) { + if (key && list[i][key]) { + vals.push(list[i][key]); } else { - return true; + vals.push(list[i]); } - }, - - contentModelMap: function(type) { - var contentModels = { - contentobject: 'core/models/contentObjectModel', - article: 'core/models/articleModel', - block: 'core/models/blockModel', - component: 'core/models/componentModel', - courseasset: 'core/models/courseAssetModel' - }; - if(contentModels.hasOwnProperty(type)) { - return require(contentModels[type]); - } - }, - - /** - * Ensures list is iterated (doesn't guarantee order), even if using async iterator - * @param list Array or Backbone.Collection - * @param func Function to use as iterator. Will be passed item, index and callback function - * @param callback Function to be called on completion - */ - forParallelAsync: function(list, func, callback) { - if(!list.hasOwnProperty('length') || list.length === 0) { - if(typeof callback === 'function') callback(); - return; + } + return vals.join(separator); + }, + + renderTags: function(list, key) { + if (!list || !list.length) { + return ''; + } + var html = ''; + }, + + decodeHTML: function(html) { + var el = document.createElement('div'); + el.innerHTML = html; + return el.childNodes.length === 0 ? "" : el.childNodes[0].nodeValue; + }, + + ifHasPermissions: function(permissions, block) { + var hasPermission = Origin.permissions.hasPermissions(permissions.split(',')); + return hasPermission ? block.fn(this) : block.inverse(this); + }, + + ifMailEnabled: function(block) { + return Origin.constants.useSmtp === true ? block.fn(this) : block.inverse(this); + }, + + ifImageIsCourseAsset: function(url, block) { + var isCourseAsset = url.length !== 0 && url.indexOf('course/assets') == 0; + return isCourseAsset ? block.fn(this) : block.inverse(this); + }, + + ifAssetIsExternal: function(url, block) { + var isExternal = Handlebars.helpers.isAssetExternal(url); + return isExternal ? block.fn(this) : block.inverse(this); + }, + + ifAssetIsHeroImage: function(url, block) { + var isMultiPart = url.split('/').length === 1; + return isMultiPart ? block.fn(this) : block.inverse(this); + }, + + copyStringToClipboard: function(data) { + var textArea = document.createElement("textarea"); + + textArea.value = data; + // Place in top-left corner of screen regardless of scroll position. + textArea.style.position = 'fixed'; + textArea.style.top = 0; + textArea.style.left = 0; + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = 0; + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; + + document.body.appendChild(textArea); + + textArea.select(); + + var success = document.execCommand('copy'); + + document.body.removeChild(textArea); + + return success; + }, + + // checks for at least one child object + validateCourseContent: function(currentCourse, callback) { + var containsAtLeastOneChild = true; + var alerts = []; + var iterateOverChildren = function(model, index, doneIterator) { + if(!model._childTypes) { + return doneIterator(); } - // make a copy in case func modifies the original - var listCopy = list.models ? list.models.slice() : list.slice(); - var doneCount = 0; - var _checkCompletion = function() { - if((++doneCount === listCopy.length) && typeof callback === 'function') { - callback(); + model.fetchChildren(function(currentChildren) { + if (currentChildren.length > 0) { + return helpers.forParallelAsync(currentChildren, iterateOverChildren, doneIterator); + } + containsAtLeastOneChild = false; + var children = _.isArray(model._childTypes) ? model._childTypes.join('/') : model._childTypes; + alerts.push(model.get('_type') + " '" + model.get('title') + "' missing " + children); + doneIterator(); + }); + }; + // start recursion + iterateOverChildren(currentCourse, null, function() { + var errorMessage = ""; + if(alerts.length > 0) { + for(var i = 0, len = alerts.length; i < len; i++) { + errorMessage += "
  • " + alerts[i] + "
  • "; } - }; - for(var i = 0, count = listCopy.length; i < count; i++) { - func(listCopy[i], i, _checkCompletion); + return callback(new Error(errorMessage)); } - }, - - /** - * Ensures list is iterated in order, even if using async iterator - * @param list Array or Backbone.Collection - * @param func Function to use as iterator. Will be passed item, index and callback function - * @param callback Function to be called on completion - */ - forSeriesAsync: function(list, func, callback) { - if(!list.hasOwnProperty('length') || list.length === 0) { - if(typeof callback === 'function') callback(); - return; + callback(null, true); + }); + }, + + isValidEmail: function(value) { + var regEx = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return value.length > 0 && regEx.test(value); + }, + + contentModelMap: function(type) { + var contentModels = { + contentobject: 'core/models/contentObjectModel', + article: 'core/models/articleModel', + block: 'core/models/blockModel', + component: 'core/models/componentModel', + courseasset: 'core/models/courseAssetModel' + }; + if(contentModels.hasOwnProperty(type)) { + return require(contentModels[type]); + } + }, + + /** + * Ensures list is iterated (doesn't guarantee order), even if using async iterator + * @param list Array or Backbone.Collection + * @param func Function to use as iterator. Will be passed item, index and callback function + * @param callback Function to be called on completion + */ + forParallelAsync: function(list, func, callback) { + if(!list.hasOwnProperty('length') || list.length === 0) { + if(typeof callback === 'function') callback(); + return; + } + // make a copy in case func modifies the original + var listCopy = list.models ? list.models.slice() : list.slice(); + var doneCount = 0; + var _checkCompletion = function() { + if((++doneCount === listCopy.length) && typeof callback === 'function') { + callback(); } - // make a copy in case func modifies the original - var listCopy = list.models ? list.models.slice() : list.slice(); - var doneCount = -1; - var _doAsync = function() { - if(++doneCount === listCopy.length) { - if(typeof callback === 'function') callback(); - return; - } - var nextItem = listCopy[doneCount]; - if(!nextItem) { - console.error('Invalid item at', doneCount + ':', nextItem); - } - func(nextItem, doneCount, _doAsync); - }; - _doAsync(); - }, - - /** - * Does a fetch for model in models, and returns the latest data in the - * passed callback - * @param models {Array of Backbone.Models} - * @param callback {Function to call when complete} - */ - multiModelFetch: function(models, callback) { - var collatedData = {}; - helpers.forParallelAsync(models, function(model, index, done) { - model.fetch({ - success: function(data) { - collatedData[index] = data; - done(); - }, - error: function(data) { - console.error('Failed to fetch data for', model.get('_id'), + data.responseText); - done(); - } - }); - }, function doneAll() { - var orderedKeys = Object.keys(collatedData).sort(); - var returnArr = []; - for(var i = 0, count = orderedKeys.length; i < count; i++) { - returnArr.push(collatedData[orderedKeys[i]]); + }; + for(var i = 0, count = listCopy.length; i < count; i++) { + func(listCopy[i], i, _checkCompletion); + } + }, + + /** + * Does a fetch for model in models, and returns the latest data in the + * passed callback + * @param models {Array of Backbone.Models} + * @param callback {Function to call when complete} + */ + multiModelFetch: function(models, callback) { + var collatedData = {}; + helpers.forParallelAsync(models, function(model, index, done) { + model.fetch({ + success: function(data) { + collatedData[index] = data; + done(); + }, + error: function(data) { + console.error('Failed to fetch data for', model.get('_id'), + data.responseText); + done(); } - callback(returnArr); }); - } - }; - - for(var name in helpers) { - if(helpers.hasOwnProperty(name)) { - Handlebars.registerHelper(name, helpers[name]); + }, function doneAll() { + var orderedKeys = Object.keys(collatedData).sort(); + var returnArr = []; + for(var i = 0, count = orderedKeys.length; i < count; i++) { + returnArr.push(collatedData[orderedKeys[i]]); } + callback(returnArr); + }); + } + }; + + for(var name in helpers) { + if(!helpers.hasOwnProperty(name)) { + continue; } + Handlebars.registerHelper(name, helpers[name]); + } - return helpers; + return helpers; }); diff --git a/frontend/src/modules/projects/templates/sharedProject.hbs b/frontend/src/modules/projects/templates/sharedProject.hbs index 232d8703c7..b815344735 100644 --- a/frontend/src/modules/projects/templates/sharedProject.hbs +++ b/frontend/src/modules/projects/templates/sharedProject.hbs @@ -8,10 +8,10 @@ - + - + {{#if heroImage}}
    @@ -28,7 +28,7 @@
    {{t 'app.recent'}}
    -
    {{formatDate updatedAt}}
    +
    {{momentFormat updatedAt "Do MMMM YYYY"}}
    {{#if tags.length}}