diff --git a/.gitignore b/.gitignore index 8d11a415a6..92b516257b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ /nbproject/ /master/ +package-lock.json + /plugins/content/component/versions/ /plugins/content/component/componentcache/ /plugins/content/extension/versions/ @@ -31,3 +33,4 @@ .idea .project +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 7780b1c05f..7a390b312b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes to the Adapt authoring tool are documented in this file. _Note that we adhere to the [semantic versioning](http://semver.org/) scheme for release numbering._ +## [0.4.1] - 2018-02-05 + +This is a patch release fixing issues introduced in v0.4.0. + +### Fixed +- Major performance fix for large installs ([\#1758](https://github.com/adaptlearning/adapt_authoring/issues/1758)) +- Various minor fixes for install and upgrade scripts ([\#1749](https://github.com/adaptlearning/adapt_authoring/issues/1749), [\#1760](https://github.com/adaptlearning/adapt_authoring/issues/1760), [\#1763](https://github.com/adaptlearning/adapt_authoring/issues/1763), [\#1797](https://github.com/adaptlearning/adapt_authoring/issues/1797), [\#1838](https://github.com/adaptlearning/adapt_authoring/issues/1838), [\#1842](https://github.com/adaptlearning/adapt_authoring/issues/1842)) +- Dashboard paging fixed ([\#1759](https://github.com/adaptlearning/adapt_authoring/issues/1759)) +- Asset management paging fixed ([\#1790](https://github.com/adaptlearning/adapt_authoring/issues/1790)) +- Fixed issue whereby the page editor screen would occasionally render the same block twice ([\#1834](https://github.com/adaptlearning/adapt_authoring/issues/1834)) +- Asset preview image has been fixed for contrib-media video assets ([\#1835](https://github.com/adaptlearning/adapt_authoring/issues/1835)) +- Fixed menu editor rendering quirks ([\#1837](https://github.com/adaptlearning/adapt_authoring/issues/1837)) + ## [0.4.0] - 2017-10-17 Major refactor of the front-end application. @@ -379,6 +392,7 @@ Initial release. - Loading screen of death - Session cookie security issues +[0.4.1]: https://github.com/adaptlearning/adapt_authoring/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.2.2...v0.3.0 [0.2.2]: https://github.com/adaptlearning/adapt_authoring/compare/v0.2.1...v0.2.2 diff --git a/Gruntfile.js b/Gruntfile.js index f018329bed..dfdb310d28 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -31,7 +31,7 @@ module.exports = function(grunt) { { expand: true, flatten: true, - src: ['frontend/src/core/libraries/ace/**/*'], + src: ['frontend/src/libraries/ace/*'], dest: 'frontend/build/js/ace' } ] @@ -260,23 +260,21 @@ module.exports = function(grunt) { // Compiles frontend plugins grunt.registerMultiTask('requireBundle', 'Generates a .js file with a bunch of imports for the path files', function() { - var requirePaths = ''; + var modulePaths = ''; // Go through each subfolder in the plugins directory var foldersArray = grunt.file.expand({ filter: "isDirectory" }, this.data.src); // Check if any plugins are available if (foldersArray.length === 0) { - requirePaths += "'"; + modulePaths += "'"; } foldersArray.forEach(function(path, index, folders) { // Strip off front of path to make relative path to config file var relativePath = path.replace(grunt.config.get('requirejs').dev.options.baseUrl, '').slice(1); var splitter = "','"; if (index === folders.length - 1) splitter = "'"; - requirePaths += relativePath + '/index' + splitter; + modulePaths += relativePath + '/index' + splitter; }); - - var defineStatement = "define('" + this.target + "',['" + requirePaths +"]);"; - grunt.file.write(this.data.dest, defineStatement); + grunt.file.write(this.data.dest, "define(['" + modulePaths +"], function() {});"); }); grunt.registerMultiTask('less', 'Compile Less files to CSS', function() { diff --git a/conf/config-sample.json b/conf/config-sample.json deleted file mode 100644 index 6e69cfbac9..0000000000 --- a/conf/config-sample.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "root": "", - "dataRoot": "data", - "rootUrl": "http://localhost:5000/", - "sessionSecret": "your-session-secret", - "outputPlugin": "adapt", - "auth": "local", - "isProduction": true, - "serverName": "localhost", - "serverPort": 5000, - "dbType": "mongoose", - "dbName": "adapt-tenant-master", - "dbHost": "localhost", - "dbPort": 27017, - "useSmtp": false, - "smtpService": "", - "smtpUsername": "", - "smtpPassword": "", - "fromAddress": "", - "masterTenantName": "adapt-tenant-master", - "masterTenantID": "" -} \ No newline at end of file diff --git a/frontend/src/core/app.js b/frontend/src/core/app.js index daea747a51..ec050d43ef 100644 --- a/frontend/src/core/app.js +++ b/frontend/src/core/app.js @@ -43,46 +43,30 @@ }); } - function loadAddOns(callback) { - /* - * FIXME we want to just be able to require these - * (this doesn't work in production mode) - */ - // ['modules/modules','plugins/plugins'] + function loadModules(callback) { require([ - // modules - 'modules/actions/index', - 'modules/assetManagement/index', - 'modules/contentPane/index', - 'modules/contextMenu/index', - 'modules/editor/index', - 'modules/filters/index', - 'modules/globalMenu/index', - 'modules/help/index', - 'modules/location/index', - 'modules/modal/index', - 'modules/navigation/index', - 'modules/notify/index', - 'modules/options/index', - 'modules/pluginManagement/index', - 'modules/projects/index', - 'modules/scaffold/index', - 'modules/sidebar/index', - 'modules/user/index', - 'modules/userManagement/index', + 'modules/modules' + ], callback); + } + + function loadPlugins(callback) { + require([ + 'plugins/plugins' ], callback); } /** - * Start app load - */ + * Start app load + */ loadLibraries(function() { loadCore(function() { - loadAddOns(function() { - // start session - // FIXME required here to avoid errors - require(['modules/user/models/sessionModel'], function(SessionModel) { - origin.startSession(new SessionModel()); + loadModules(function() { + loadPlugins(function() { + // start session + // FIXME required here to avoid errors + require(['modules/user/models/sessionModel'], function(SessionModel) { + origin.startSession(new SessionModel()); + }); }); }); }); diff --git a/frontend/src/core/collections/contentCollection.js b/frontend/src/core/collections/contentCollection.js new file mode 100644 index 0000000000..4aec220c22 --- /dev/null +++ b/frontend/src/core/collections/contentCollection.js @@ -0,0 +1,35 @@ +// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE +define(function(require) { + var Backbone = require('backbone'); + var Origin = require('core/origin'); + var Helpers = require('core/helpers'); + + var ContentCollection = Backbone.Collection.extend({ + initialize : function(models, options) { + this._type = options._type; + this.model = Helpers.contentModelMap(this._type); + this._courseId = options._courseId; + this._parentId = options._parentId; + this.url = options.url || 'api/content/' + options._type + this.buildQuery(); + + this.on('reset', this.loadedData, this); + }, + + buildQuery: function() { + var query = ''; + if(this._courseId) { + query += '_courseId=' + this._courseId + } + if(this._parentId) { + query += '_parentId=' + this._parentId + } + return query ? '?' + query : ''; + }, + + loadedData: function() { + Origin.trigger('contentCollection:dataLoaded', this._type); + } + }); + + return ContentCollection; +}); diff --git a/frontend/src/core/helpers.js b/frontend/src/core/helpers.js index 2438a44219..0765d47a7e 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -1,379 +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(); - } - - 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 = ''; - - if (list && list.length) { - html = '' - } - - 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); - } - }, - - ifMailEnabled: function(block) { - if (Origin.constants.useSmtp === true) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - - getAssetFromValue: function(url) { - var urlSplit = url.split('/') - var fileName = urlSplit[urlSplit.length - 1]; - // Get courseAsset model - var courseAsset = Origin.editor.data.courseassets.findWhere({_fieldName: fileName}); - - if (courseAsset) { - var courseAssetId = courseAsset.get('_assetId'); - - return '/api/asset/serve/' + courseAssetId; - } else { - return ''; - } - }, - - ifImageIsCourseAsset: function(url, block) { - if (url.length !== 0 && url.indexOf('course/assets') == 0) { - return block.fn(this); - } else { - return block.inverse(this); - } - }, - - getThumbnailFromValue: function(url) { - - var urlSplit = url.split('/') - var fileName = urlSplit[urlSplit.length - 1]; - // Get courseAsset model - var courseAsset = Origin.editor.data.courseassets.findWhere({_fieldName: fileName}); - if (courseAsset) { - var courseAssetId = courseAsset.get('_assetId'); - return '/api/asset/thumb/' + courseAssetId; - } else { - return '/api/asset/thumb/' + url; - } - - }, - - 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); + 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); + }, - textArea.select(); + formatDuration: function(duration) { + var zero = '0', hh, mm, ss; + var time = new Date(0, 0, 0, 0, 0, Math.floor(duration), 0); - var success = document.execCommand('copy'); + hh = time.getHours(); + mm = time.getMinutes(); + ss = time.getSeconds(); - document.body.removeChild(textArea); + // Pad zero values to 00 + hh = (zero+hh).slice(-2); + mm = (zero+mm).slice(-2); + ss = (zero+ss).slice(-2); - return success; - }, + return hh + ':' + mm + ':' + ss; + }, - validateCourseContent: function(currentCourse) { - // Let's do a standard check for at least one child object - var containsAtLeastOneChild = true; + // 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); + }, - var alerts = []; + stripHtml: function(html) { + return new Handlebars.SafeString(html); + }, - function iterateOverChildren(model) { - // Return the function if no children - on components - if(!model._children) return; + bytesToSize: function(bytes) { + if (bytes == 0) return '0 B'; - var currentChildren = model.getChildren(); + var k = 1000; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); - // Do validate across each item - if (currentChildren.length == 0) { - containsAtLeastOneChild = false; + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; + }, - var children = _.isArray(model._children) ? model._children.join('/') : model._children; - alerts.push( - "There seems to be a " - + model.get('_type') - + " with the title - '" - + model.get('title') - + "' with no " - + children - ); + renderBooleanOptions: function(selectedValue) { + var options = ["true", "false"]; + var html = ''; - return; - } else { - // Go over each child and call validation again - currentChildren.each(function(childModel) { - iterateOverChildren(childModel); - }); - } + 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 { + vals.push(list[i]); + } + } + 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(); + } + model.fetchChildren(function(currentChildren) { + if (currentChildren.length > 0) { + return helpers.forParallelAsync(currentChildren, iterateOverChildren, doneIterator); } - - iterateOverChildren(currentCourse); - - if(alerts.length > 0) { - var errorMessage = ""; - for(var i = 0, len = alerts.length; i < len; i++) { - errorMessage += "
  • " + alerts[i] + "
  • "; - } - - Origin.Notify.alert({ - type: 'error', - title: Origin.l10n.t('app.validationfailed'), - text: errorMessage, - callback: _.bind(this.validateCourseConfirm, this) - }); + 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 containsAtLeastOneChild; - }, - - validateCourseConfirm: function(isConfirmed) { - if (isConfirmed) { - Origin.trigger('editor:courseValidation'); + return callback(new Error(errorMessage)); } - }, - - 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; - } else { - return true; + 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(); } + }; + for(var i = 0, count = listCopy.length; i < count; i++) { + func(listCopy[i], i, _checkCompletion); } - }; - - for(var name in helpers) { - if(helpers.hasOwnProperty(name)) { - Handlebars.registerHelper(name, helpers[name]); + }, + + /** + * 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]]); } + 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/core/models/articleModel.js b/frontend/src/core/models/articleModel.js index e3ec105ccd..854d5d62aa 100644 --- a/frontend/src/core/models/articleModel.js +++ b/frontend/src/core/models/articleModel.js @@ -4,9 +4,9 @@ define(function(require) { var ArticleModel = ContentModel.extend({ urlRoot: '/api/content/article', - _parent: 'contentObjects', - _siblings: 'articles', - _children: 'blocks' + _parentType: 'contentobject', + _siblingTypes: 'article', + _childTypes: 'block' }); return ArticleModel; diff --git a/frontend/src/core/models/blockModel.js b/frontend/src/core/models/blockModel.js index a3d9425b9b..46187ebba2 100644 --- a/frontend/src/core/models/blockModel.js +++ b/frontend/src/core/models/blockModel.js @@ -4,9 +4,9 @@ define(function(require) { var BlockModel = ContentModel.extend({ urlRoot: '/api/content/block', - _parent: 'articles', - _siblings: 'blocks', - _children: 'components', + _parentType: 'article', + _siblingTypes: 'block', + _childTypes: 'component', // Block specific properties layoutOptions: null, dragLayoutOptions: null, @@ -27,7 +27,9 @@ define(function(require) { 'title', '_extensions', 'themeSettings', - '_onScreen' + '_onScreen', + '_isVisible', + '_isHidden' ] }); diff --git a/frontend/src/core/models/componentModel.js b/frontend/src/core/models/componentModel.js index 8ab83cedf9..a406abb659 100644 --- a/frontend/src/core/models/componentModel.js +++ b/frontend/src/core/models/componentModel.js @@ -4,8 +4,8 @@ define(function(require) { var ComponentModel = ContentModel.extend({ urlRoot: '/api/content/component', - _parent: 'blocks', - _siblings: 'components', + _parentType: 'block', + _siblingTypes: 'component', // These are the only attributes which should be permitted on a save // TODO look into this... whitelistAttributes: [ @@ -26,7 +26,10 @@ define(function(require) { 'title', 'version', 'themeSettings', - '_onScreen' + '_onScreen', + '_isVisible', + '_isHidden', + 'instruction' ] }); diff --git a/frontend/src/core/models/componentTypeModel.js b/frontend/src/core/models/componentTypeModel.js index 4621605a66..29ffdf6af7 100644 --- a/frontend/src/core/models/componentTypeModel.js +++ b/frontend/src/core/models/componentTypeModel.js @@ -5,7 +5,7 @@ define(function(require) { var ComponentTypeModel = ContentModel.extend({ idAttribute: '_id', urlRoot: '/api/componenttype', - _parent: 'blocks', + _parent: 'block', comparator: function(model) { return model.get('displayName'); diff --git a/frontend/src/core/models/contentModel.js b/frontend/src/core/models/contentModel.js index 5eee95b80b..59f81fe23e 100644 --- a/frontend/src/core/models/contentModel.js +++ b/frontend/src/core/models/contentModel.js @@ -2,6 +2,8 @@ define(function(require) { var Backbone = require('backbone'); var Origin = require('core/origin'); + var Helpers = require('core/helpers'); + var ContentCollection = require('core/collections/contentCollection'); var ContentModel = Backbone.Model.extend({ idAttribute: '_id', @@ -10,91 +12,77 @@ define(function(require) { initialize: function(options) { this.on('sync', this.loadedData, this); this.on('change', this.loadedData, this); - this.fetch(); }, loadedData: function() { - if (this._siblings) { - this._type = this._siblings; - } - Origin.trigger('editorModel:dataLoaded', this._type, this.get('_id')); + if(this._siblingTypes) this._type = this._siblingTypes; }, - getChildren: function() { - var self = this; - var getChildrenDelegate = function(type) { - if (Origin.editor.data[type]) { - var children = Origin.editor.data[type].where({ _parentId: self.get('_id') }); - var childrenCollection = new Backbone.Collection(children); - return childrenCollection; - } - return null; - }; - if(_.isArray(this._children)) { - var allChildren; - for(var i = 0, count = this._children.length; i < count; i++) { - var children = getChildrenDelegate(this._children[i]); - if(children) { - if(!allChildren) allChildren = children; - else allChildren.add(children.models); + fetchChildren: function(callback) { + var childTypes = _.isArray(this._childTypes) ? this._childTypes : [this._childTypes]; + // has to be a plain old array because we may have multiple model types + var children = []; + Helpers.forParallelAsync(childTypes, _.bind(function(childType, index, done) { + (new ContentCollection(null, { + _type: childType, + _parentId: this.get('_id') + })).fetch({ + success: function(collection) { + children = children.concat(collection.models); + done(); + }, + error: function(collecion, response) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); } - } - return allChildren; - } else { - return getChildrenDelegate(this._children); - } + }); + }, this), function() { + callback(children); + }); }, - getParent: function() { - var currentType = this.get('_type'); - var parent; - var currentParentId = this.get('_parentId'); - - if (currentType === 'menu' || currentType === 'page') { - if (currentParentId === Origin.editor.data.course.get('_id')) { - parent = Origin.editor.data.course; - } else { - parent = Origin.editor.data.contentObjects.findWhere({ _id: currentParentId }); - } - } else if (currentType != 'course'){ - parent = Origin.editor.data[this._parent].findWhere({ _id: currentParentId }); + fetchParent: function(callback) { + if(!this._parentType || !this.get('_parentId')) { + return callback(); } - - return parent; - }, - - getSiblings: function(returnMyself) { - if (returnMyself) { - var siblings = Origin.editor.data[this._siblings].where({ _parentId: this.get('_parentId') }); - return new Backbone.Collection(siblings); + if(this.get('_parentId') === Origin.editor.data.course.get('_id')) { + return callback(Origin.editor.data.course); } - var siblings = _.reject(Origin.editor.data[this._siblings].where({ - _parentId: this.get('_parentId') - }), _.bind(function(model){ - return model.get('_id') == this.get('_id'); - }, this)); - - return new Backbone.Collection(siblings); - }, - - setOnChildren: function(key, value, options) { - var args = arguments; - - if(!this._children) return; - - this.getChildren().each(function(child){ - child.setOnChildren.apply(child, args); + // create model instance using _parentType and _parentId + var modelClass = Helpers.contentModelMap(this._parentType); + var model = new modelClass({ _id: this.get('_parentId') }); + model.fetch({ + success: _.bind(callback, this), + error: _.bind(function(jqXHR) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + callback.call(this); + }, this) }); }, - getPossibleAncestors: function() { - var map = { - 'contentObjects': { 'ancestorType': 'page' }, - 'articles': { 'ancestorType': 'article' }, - 'blocks': { 'ancestorType': 'block' } - }; - ancestors = Origin.editor.data[this._parent].where({ _type: map[this._parent].ancestorType }); - return new Backbone.Collection(ancestors); + fetchSiblings: function(callback) { + var siblings = new ContentCollection(null, { + _type: this._siblingTypes, + _parentId: this.get('_parentId') + }); + siblings.fetch({ + success: _.bind(function(collection) { + collection.remove(this); + callback(collection); + }, this), + error: _.bind(function(jqXHR) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + callback.call(this); + }, this) + }); }, serialize: function() { @@ -112,19 +100,6 @@ define(function(require) { } }); } - }, - - serializeChildren: function() { - var children = this.getChildren(); - var serializedJson = ''; - - if (children) { - _.each(children.models, function(child) { - serializedJson += child.serialize(); - }); - } - - return serializedJson; } }); diff --git a/frontend/src/core/models/contentObjectModel.js b/frontend/src/core/models/contentObjectModel.js index 1ba306df62..6d15570995 100644 --- a/frontend/src/core/models/contentObjectModel.js +++ b/frontend/src/core/models/contentObjectModel.js @@ -5,9 +5,9 @@ define(function(require) { var ContentObjectModel = ContentModel.extend({ urlRoot: '/api/content/contentobject', - _parent: 'contentObjects', - _siblings: 'contentObjects', - _children: ['contentObjects', 'articles'], + _parentType: 'contentobject', + _siblingTypes: 'contentobject', + _childTypes: ['contentobject', 'article'], defaults: { _isSelected: false, diff --git a/frontend/src/core/models/courseModel.js b/frontend/src/core/models/courseModel.js index 9da53ed0e0..5544639c65 100644 --- a/frontend/src/core/models/courseModel.js +++ b/frontend/src/core/models/courseModel.js @@ -7,7 +7,7 @@ define(function(require) { var CourseModel = ContentModel.extend({ urlRoot: '/api/content/course', _type: 'course', - _children: 'contentObjects', + _childTypes: 'contentobject', getHeroImageURI: function () { if(Helpers.isAssetExternal(this.get('heroImage'))) { diff --git a/frontend/src/core/views/originView.js b/frontend/src/core/views/originView.js index 8508a8ff74..24b2320fd4 100644 --- a/frontend/src/core/views/originView.js +++ b/frontend/src/core/views/originView.js @@ -50,22 +50,23 @@ define(function(require){ Origin.trigger('origin:hideLoading'); }, - setUserPreference: function(key, value) { - if (this.settings.preferencesKey && typeof(Storage) !== "undefined") { + setUserPreference: function(key, value, sessionOnly) { + if (this.settings.preferencesKey) { var preferences = (Origin.sessionModel.get(this.settings.preferencesKey) || {}); - // persist data preferences[key] = value; + // Store in session model Origin.sessionModel.set(this.settings.preferencesKey, preferences); - + // set in browser storage + var data = {}; + data[key] = value; + Origin.browserStorage.set(this.settings.preferencesKey, data, sessionOnly || false, false); } }, getUserPreferences: function() { - if (this.settings.preferencesKey && typeof(Storage) !== "undefined" - && localStorage.getItem(this.settings.preferencesKey)) { - return Origin.sessionModel.get(this.settings.preferencesKey); - } else { - return {}; + if (this.settings.preferencesKey) { + var saveData = Origin.browserStorage.get(this.settings.preferencesKey); + return saveData || {}; } }, diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index d5e14e1137..606e8064b5 100755 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -9,7 +9,7 @@ define(function(require) { var AssetModel = require('./models/assetModel'); var TagsCollection = require('core/collections/tagsCollection'); - Origin.on('app:dataReady login:changed', function() { + Origin.on('origin:dataReady login:changed', function() { Origin.globalMenu.addItem({ "location": "global", "text": Origin.l10n.t('app.assetmanagement'), @@ -24,51 +24,46 @@ define(function(require) { }); Origin.on('router:assetManagement', function(location, subLocation, action) { - Origin.assetManagement = { filterData: {} }; - if(!location) { - loadCollectionView(); - } - else if(location === 'new') { - loadAssetView(); - } - else if(subLocation === 'edit') { - loadAssetView(location); - } + Origin.assetManagement = { + filterData: {} + }; + if(!location) return loadAssetsView(); + if(location === 'new') return loadNewAssetView(); + if(subLocation === 'edit') loadEditAssetView(location); }); - function loadCollectionView() { - // Sidebar needs access to collection, so create now - new TagsCollection().fetch({ - success: function(tags) { + function loadAssetsView() { + (new TagsCollection()).fetch({ + success: function(tagsCollection) { + // Load asset collection before so sidebarView has access to it + var assetCollection = new AssetCollection(); + // No need to fetch as the collectionView takes care of this + // Mainly due to serverside filtering Origin.trigger('location:title:hide'); - Origin.sidebar.addView(new AssetManagementSidebarView({ collection: tags }).$el); - // Fetch is done in collectionView due to server-side filtering - Origin.contentPane.setView(AssetManagementView, { collection: new AssetCollection() }); + Origin.sidebar.addView(new AssetManagementSidebarView({ collection: tagsCollection }).$el); + Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); Origin.trigger('assetManagement:loaded'); }, error: function() { - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorfetchingdata') - }); + console.log('Error occured getting the tags collection - try refreshing your page'); } }); } - function loadAssetView(id) { - var isNew = id === undefined; - var model = new AssetModel(isNew ? {} : { _id: id }); - - if(isNew) loadView(); - else model.fetch({ success: loadView }); + function loadNewAssetView() { + Origin.trigger('location:title:update', { title: 'New Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); + } - function loadView() { - Origin.trigger('location:title:update', { title: isNew ? Origin.l10n.t('app.newasset') : Origin.l10n.t('app.editasset') } ); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el, { - "backButtonText": Origin.l10n.t('app.backtoassets'), - "backButtonRoute": "/#/assetManagement" - }); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); - }; + function loadEditAssetView(location) { + // Fetch existing asset model + (new AssetModel({ _id: location })).fetch({ + success: function(model) { + Origin.trigger('location:title:update', { title: 'Edit Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); + } + }); } }); diff --git a/frontend/src/modules/assetManagement/less/asset.less b/frontend/src/modules/assetManagement/less/asset.less index a421973117..21da7efc46 100755 --- a/frontend/src/modules/assetManagement/less/asset.less +++ b/frontend/src/modules/assetManagement/less/asset.less @@ -16,8 +16,8 @@ } .asset-management-assets-container { - width:75%; float:left; + overflow-y: auto; .asset-management-no-assets { text-align:center; position:relative; @@ -31,7 +31,6 @@ } .asset-management-preview-container { - width:25%; float:right; position: fixed; right: 0; diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index ce196eb7fe..de3a607435 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -1,205 +1,189 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); - var Origin = require('core/origin'); - var OriginView = require('core/views/originView'); - var AssetItemView = require('./assetManagementItemView'); - var AssetModel = require('../models/assetModel'); - var AssetManagementPreview = require('./assetManagementPreviewView'); - - var AssetCollectionView = OriginView.extend({ - - tagName: "div", - - className: "asset-management-collection", - - events: {}, - - preRender: function(options) { - this.sort = { createdAt: -1 }; - this.search = (options.search || {}); - // Set to minus so we can have more DRY code - this.assetLimit = -32; - this.assetDenominator = 32; - this.filters = (this.search.assetType) ? options.search.assetType.$in : []; - this.tags = []; - this.collectionLength = 0; - this.shouldStopFetches = false; - this.listenTo(this.collection, 'add', this.appendAssetItem); - this.listenTo(Origin, 'assetManagement:sidebarFilter:add', this.addFilter); - this.listenTo(Origin, 'assetManagement:sidebarFilter:remove', this.removeFilter); - this.listenTo(Origin, 'assetManagement:sidebarView:filter', this.filterBySearchInput); - this.listenTo(Origin, 'assetManagement:assetManagementSidebarView:filterByTags', this.filterByTags); - this.listenTo(this.collection, 'sync', this.onCollectionSynced); - this.listenTo(Origin, 'assetManagement:collection:refresh', this.updateCollection); - }, - - onCollectionSynced: function () { - if (this.collection.length === 0) { - $('.asset-management-no-assets').removeClass('display-none'); - } else { - $('.asset-management-no-assets').addClass('display-none'); - } - - // FIX: Purely and lovingly put in for a rendering issue with chrome. - // For when the items being re-rendering after a search return an - // amount of items that means the container is not scrollable - if (this.assetLimit < this.assetDenominator) { - $('.asset-management-assets-container').hide(); - _.delay(function() { - $('.asset-management-assets-container').show(); - }, 10); - } - - }, - - setupLazyScrolling: function() { - - var $assetContainer = $('.asset-management-assets-container'); - var $assetContainerInner = $('.asset-management-assets-container-inner'); - // Remove event before attaching - $assetContainer.off('scroll'); - - $assetContainer.on('scroll', _.bind(function() { - - var scrollTop = $assetContainer.scrollTop(); - var scrollableHeight = $assetContainerInner.height(); - var containerHeight = $assetContainer.height(); - - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } - } - - }, this)); - }, - - updateCollection: function (reset) { - // If removing items, we need to reset our limits - if (reset) { - // Trigger event to kill zombie views - Origin.trigger('assetManagement:assetViews:remove'); - - // Reset fetches cache - this.shouldStopFetches = false; - - this.assetLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } - - if (!Origin.permissions.hasPermissions(["*"])) { - this.search = _.extend(this.search, {_isDeleted: false}); - } - - this.search = _.extend(this.search, { - tags: { - $all: this.tags - } - }, { - assetType: { - $in: this.filters - } - }); - - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { - return; - } - - this.collection.fetch({ - remove: reset, - data: { - search: this.search, - operators : { - skip: this.assetLimit, - limit: this.assetDenominator, - sort: this.sort - } - }, - success: _.bind(function() { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } - - this.isCollectionFetching = false; - Origin.trigger('assetManagement:assetManagementCollection:fetched'); - }, this) - }); - }, - - appendAssetItem: function (asset) { - this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.assetLimit += this.assetDenominator; - this.updateCollection(false); - }, - - postRender: function() { - this.setupLazyScrolling(); - - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - $('.asset-management-assets-container').trigger('scroll'); - this.setViewToReady(); - }, - - addFilter: function(filterType) { - // add filter to this.filters - this.filters.push(filterType); - this.filterCollection(); - }, - - removeFilter: function(filterType) { - // remove filter from this.filters - this.filters = _.filter(this.filters, function(item) { - return item != filterType; - }); - - this.filterCollection(); - }, - - filterCollection: function() { - this.search.assetType = this.filters.length - ? { $in: this.filters } - : null ; - this.updateCollection(true); + var Origin = require('core/origin'); + var OriginView = require('core/views/originView'); + var AssetItemView = require('./assetManagementItemView'); + + var AssetCollectionView = OriginView.extend({ + className: "asset-management-collection", + + sort: { createdAt: -1 }, + search: {}, + filters: [], + tags: [], + fetchCount: 0, + shouldStopFetches: false, + pageSize: 1, + + preRender: function(options) { + if(options.search) { + this.search = options.search; + var assetType = this.search.assetType; + if(assetType) this.filters = assetType.$in; + } + this.initEventListeners(); + + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 400), this); + }, + + postRender: function() { + this.initPaging(); + // init lazy scrolling + $('.asset-management-assets-container').on('scroll', this._doLazyScroll); + $(window).on('resize', this._onResize); + }, + + initEventListeners: function() { + this.listenTo(Origin, { + 'assetManagement:sidebarFilter:add': this.addFilter, + 'assetManagement:sidebarFilter:remove': this.removeFilter, + 'assetManagement:sidebarView:filter': this.filterBySearchInput, + 'assetManagement:assetManagementSidebarView:filterByTags': this.filterByTags, + 'assetManagement:collection:refresh': this.resetCollection + }); + this.listenTo(this.collection, 'add', this.appendAssetItem); + }, + + initPaging: function() { + this.resetCollection(_.bind(function(collection) { + var containerHeight = $('.asset-management-assets-container').outerHeight(); + var containerWidth = $('.asset-management-assets-container').outerWidth(); + var itemHeight = $('.asset-management-list-item').outerHeight(true); + var itemWidth = $('.asset-management-list-item').outerWidth(true); + var columns = Math.floor(containerWidth/itemWidth); + var rows = Math.floor(containerHeight/itemHeight); + // columns stack nicely, but need to add extra row if it's not a clean split + if((containerHeight % itemHeight) > 0) rows++; + this.pageSize = columns*rows; + + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); + }, this)); + }, + + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); + }, + + /** + * Collection manipulation + */ + + fetchCollection: function(cb) { + if(this.shouldStopFetches || this.isCollectionFetching) { + return; + } + this.isCollectionFetching = true; + + this.collection.fetch({ + data: { + search: _.extend(this.search, { + tags: { $all: this.tags }, + assetType: { $in: this.filters } + }), + operators : { + skip: this.fetchCount, + limit: this.pageSize, + sort: this.sort + } }, - - filterBySearchInput: function (filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - this.search = { title: pattern, description: pattern }; - this.updateCollection(true); - - $(".asset-management-modal-filter-search" ).focus(); - }, - - removeLazyScrolling: function() { - $('.asset-management-assets-container').off('scroll'); - }, - - filterByTags: function(tags) { - this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); + success: _.bind(function(collection, response) { + this.isCollectionFetching = false; + this.fetchCount += response.length; + // stop further fetching if this is the last page + if(response.length < this.pageSize) this.shouldStopFetches = true; + + $('.asset-management-no-assets').toggleClass('display-none', this.fetchCount > 0); + + Origin.trigger('assetManagement:assetManagementCollection:fetched'); + if(typeof cb === 'function') cb(collection); + }, this), + error: function(error) { + console.log(error); + this.isCollectionFetching = false; } - - }, { - template: 'assetManagementCollection' - }); - - return AssetCollectionView; - + }); + }, + + resetCollection: function(cb, shouldFetch) { + // to remove old views + Origin.trigger('assetManagement:assetViews:remove'); + + this.shouldStopFetches = false; + this.fetchCount = 0; + this.collection.reset(); + + if (shouldFetch === undefined || shouldFetch === true) { + this.fetchCollection(cb); + } + }, + + /** + * Filtering + */ + + filterCollection: function() { + this.resetCollection(null, false); + this.search.assetType = this.filters.length ? { $in: this.filters } : null; + this.fetchCollection(); + }, + + addFilter: function(filterType) { + this.filters.push(filterType); + this.filterCollection(); + }, + + removeFilter: function(filterType) { + // remove filter from this.filters + this.filters = _.filter(this.filters, function(item) { return item !== filterType; }); + this.filterCollection(); + }, + + filterBySearchInput: function (filterText) { + this.resetCollection(null, false); + var pattern = '.*' + filterText.toLowerCase() + '.*'; + this.search = { title: pattern, description: pattern }; + this.fetchCollection(); + + $(".asset-management-modal-filter-search" ).focus(); + }, + + filterByTags: function(tags) { + this.resetCollection(null, false); + this.tags = _.pluck(tags, 'id'); + this.fetchCollection(); + }, + + /** + * Event handling + */ + + onResize: function() { + this.initPaging(); + }, + + doLazyScroll: function(e) { + if(this.isCollectionFetching) { + return; + } + var $el = $(e.currentTarget); + var scrollableHeight = this.$el.height() - this.$el.height(); + var pxRemaining = this.$el.height() - ($el.scrollTop() + $el.height()); + var scrollTriggerAmmount = $('.asset-management-list-item').first().outerHeight()/2; + // we're at the bottom, fetch more + if (pxRemaining <= scrollTriggerAmmount) this.fetchCollection(); + }, + + remove: function() { + $('.asset-management-assets-container').off('scroll', this._doLazyScroll); + $(window).on('resize', this._onResize); + + OriginView.prototype.remove.apply(this, arguments); + } + + }, { + template: 'assetManagementCollection' + }); + return AssetCollectionView; }); diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js b/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js index dac2364e8a..c7d5f9c6ac 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js @@ -54,7 +54,6 @@ define(function(require){ var $uploadFileErrormsg = $uploadFile.prev('label').find('span.error'); $.each(this.$('.required'), function (index, el) { - console.log(el.val, el); var $errormsg = $(el).prev('label').find('span.error'); if (!$.trim($(el).val())) { validated = false; @@ -116,7 +115,6 @@ define(function(require){ }); this.$('#tags').val(tags); - var self = this; this.$('.asset-form').ajaxSubmit({ uploadProgress: function(event, position, total, percentComplete) { $(".progress-container").css("visibility", "visible"); @@ -130,13 +128,13 @@ define(function(require){ text: xhr.responseJSON.message }); }, - success: function(data, status, xhr) { + success: _.bind(function(data, status, xhr) { Origin.once('assetManagement:assetManagementCollection:fetched', function() { Origin.trigger('assetManagement:modal:selectItem', data._id); }) Origin.trigger('assetManagement:collection:refresh', true); - self.remove(); - } + this.remove(); + }, this) }); // Return false to prevent the page submitting diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6ab909531c..6885f17775 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -1,85 +1,83 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - - var Backbone = require('backbone'); - var Origin = require('core/origin'); - var AssetModel = require('../models/assetModel'); - var AssetManagementCollectionView = require('./assetManagementCollectionView'); - var AssetManagementPreviewView = require('./assetManagementPreviewView'); - var AssetManagementView = require('./assetManagementView'); - var AssetManagementModalFiltersView = require('./assetManagementModalFiltersView'); - var AssetManagementModelAutofillView = require('./assetManagementModalAutofillView'); - - var AssetManagementModalView = AssetManagementView.extend({ - - preRender: function(options) { - this.options = options; - AssetManagementView.prototype.preRender.apply(this, arguments); - }, - - postRender: function() { - this.setupSubViews(); - this.setupFilterAndSearchView(); - if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { - this.setupImageAutofillButton(); - } - this.resizeAssetPanels(); - }, - - setupSubViews: function() { - this.search = {}; - // Replace Asset and : so we can have both filtered and all asset types - var assetType = this.options.assetType.replace('Asset', '').replace(':', ''); - - if (assetType) { - var filters = [assetType]; - this.search.assetType = { $in: filters }; - } - - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection, search: this.search}).$el); - }, - - setupFilterAndSearchView: function() { - new AssetManagementModalFiltersView(this.options); - }, - - setupImageAutofillButton: function() { - new AssetManagementModelAutofillView({modalView: this}); - }, - - resizeAssetPanels: function() { - var navigationHeight = $('.navigation').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); - }, - - onAssetClicked: function(model) { - this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ - model: model - }).$el); - - var filename = model.get('filename'); - var selectedFileAlias = 'course/assets/' + filename; - var assetId = model.get('_id'); - var assetObject = { - assetLink: selectedFileAlias, - assetId: assetId, - assetFilename: filename - } - this.data = assetObject; - Origin.trigger('modal:assetSelected', assetObject); - }, - - getData: function() { - return this.data; - } - - }); - - return AssetManagementModalView; - + var Origin = require('core/origin'); + var AssetManagementCollectionView = require('./assetManagementCollectionView'); + var AssetManagementPreviewView = require('./assetManagementPreviewView'); + var AssetManagementView = require('./assetManagementView'); + var AssetManagementModalFiltersView = require('./assetManagementModalFiltersView'); + var AssetManagementModelAutofillView = require('./assetManagementModalAutofillView'); + + var AssetManagementModalView = AssetManagementView.extend({ + preRender: function(options) { + this.options = options; + AssetManagementView.prototype.preRender.apply(this, arguments); + }, + + postRender: function() { + this.setupSubViews(); + this.setupFilterAndSearchView(); + if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { + this.setupImageAutofillButton(); + } + this.resizePanels(); + }, + + setupSubViews: function() { + this.search = {}; + // Replace Asset and : so we can have both filtered and all asset types + var assetType = this.options.assetType.replace('Asset', '').replace(':', ''); + + if (assetType) { + var filters = [assetType]; + this.search.assetType = { $in: filters }; + } + // Push collection through to collection view + var view = new AssetManagementCollectionView({ + collection: this.collection, + search: this.search + }); + this.$('.asset-management-assets-container-inner').append(view.$el); + }, + + setupFilterAndSearchView: function() { + new AssetManagementModalFiltersView(this.options); + }, + + setupImageAutofillButton: function() { + new AssetManagementModelAutofillView({ modalView: this }); + }, + + resizePanels: function() { + var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); + var windowWidth = $(window).width(); + this.$('.asset-management-assets-container') + .width(windowWidth*0.75) + .height(actualHeight); + this.$('.asset-management-preview-container') + .width(windowWidth*0.25) + .height(actualHeight); + }, + + onAssetClicked: function(model) { + this.$('.asset-management-no-preview').hide(); + + var previewView = new AssetManagementPreviewView({ model: model }); + this.$('.asset-management-preview-container-inner').html(previewView.$el); + + var filename = model.get('filename'); + var assetObject = { + assetLink: 'course/assets/' + filename, + assetId: model.get('_id'), + assetFilename: filename + }; + this.data = assetObject; + Origin.trigger('modal:assetSelected', assetObject); + }, + + getData: function() { + return this.data; + } + }); + + return AssetManagementModalView; }); diff --git a/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js b/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js index 73ad9e0520..6f4cd9a3bf 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js @@ -10,7 +10,7 @@ define(function(require){ className: 'asset-management-new-asset', events: { - 'change .asset-file' : 'onChangeFile', + 'change .asset-file': 'onChangeFile', }, preRender: function() { diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index dbc757171c..fcc1d462b4 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -1,69 +1,60 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); - var OriginView = require('core/views/originView'); var Origin = require('core/origin'); - var AssetModel = require('../models/assetModel'); + var OriginView = require('core/views/originView'); var AssetManagementCollectionView = require('./assetManagementCollectionView'); var AssetManagementPreviewView = require('./assetManagementPreviewView'); var AssetManagementView = OriginView.extend({ - tagName: 'div', - className: 'asset-management', - events: { - }, - preRender: function() { - this.listenTo(Origin, 'window:resize', this.resizeAssetPanels); - this.listenTo(Origin, 'assetManagement:assetItemView:preview', this.onAssetClicked); - this.listenTo(Origin, 'assetManagement:assetPreviewView:delete', this.onAssetDeleted); + this.listenTo(Origin, { + 'window:resize': this.resizePanels, + 'assetManagement:assetItemView:preview': this.onAssetClicked, + 'assetManagement:assetPreviewView:delete': this.onAssetDeleted + }); }, postRender: function() { - this.setupSubViews(); - this.resizeAssetPanels(); - // Set imageReady - _.defer(_.bind(this.setupImageReady, this)); + var view = new AssetManagementCollectionView({ collection: this.collection }); + this.$('.asset-management-assets-container-inner').append(view.$el); + + this.resizePanels(); + // defer setting ready status until images are ready + _.defer(function() { + view.$el.imageready(this.setViewToReady); + }); }, - setupImageReady: function() { - this.$el.imageready(this.setViewToReady); - }, - - setupSubViews: function() { - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection}).$el); - }, - - resizeAssetPanels: function() { - var navigationHeight = $('.navigation').outerHeight(); - var locationTitleHeight = $('.location-title').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + resizePanels: function() { + var navigationHeight = $('.navigation').outerHeight(); + var locationTitleHeight = $('.location-title').outerHeight(); + var windowHeight = $(window).height(); + var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); + var paneWidth = $('.asset-management').outerWidth(); + this.$('.asset-management-assets-container') + .height(actualHeight) + .width(paneWidth*0.75); + this.$('.asset-management-preview-container') + .height(actualHeight) + .width(paneWidth*0.25); }, onAssetClicked: function(model) { - this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ - model: model - }).$el); + this.$('.asset-management-no-preview').hide(); + + var view = new AssetManagementPreviewView({ model: model }); + this.$('.asset-management-preview-container-inner').html(view.$el); }, onAssetDeleted: function() { - this.$('.asset-management-no-preview').show(); + this.$('.asset-management-no-preview').show(); } - }, { template: 'assetManagement' }); return AssetManagementView; - }); diff --git a/frontend/src/modules/browserStorage/index.js b/frontend/src/modules/browserStorage/index.js new file mode 100644 index 0000000000..b7571f57ea --- /dev/null +++ b/frontend/src/modules/browserStorage/index.js @@ -0,0 +1,51 @@ +// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE +define(function(require) { + // don't bother doing anything if there's no storage + if(!Storage) return; + + var _ = require('underscore'); + var Origin = require('core/origin'); + + var userData = false; + + var BrowserStorage = { + initialise: function() { + var userId = Origin.sessionModel.get('id'); + userData = { + local: JSON.parse(localStorage.getItem(userId)) || {}, + session: JSON.parse(sessionStorage.getItem(userId)) || {} + }; + }, + + set: function(key, value, sessionOnly, replaceExisting) { + // determine what we're storing, and where + var storageObj = sessionOnly ? userData.session : userData.local; + var value = replaceExisting ? value : _.extend({}, storageObj[key], value); + // persist data + storageObj[key] = value; + this.save(); + }, + + get: function(key) { + return _.extend({}, userData.local[key], userData.session[key]); + }, + + // persist data to Storage + save: function() { + var userId = Origin.sessionModel.get('id'); + + if(!_.isEmpty(userData.session)) { + sessionStorage.setItem(userId, JSON.stringify(userData.session)); + } + if(!_.isEmpty(userData.local)) { + localStorage.setItem(userId, JSON.stringify(userData.local)); + } + } + }; + + // initialise + Origin.on('origin:dataReady login:changed', function() { + BrowserStorage.initialise(); + Origin.browserStorage = BrowserStorage; + }); +}); diff --git a/frontend/src/modules/editor/article/views/editorArticleEditView.js b/frontend/src/modules/editor/article/views/editorArticleEditView.js index c28cd1738e..9d05bbd3af 100644 --- a/frontend/src/modules/editor/article/views/editorArticleEditView.js +++ b/frontend/src/modules/editor/article/views/editorArticleEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorArticleEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); } }, { template: 'editorArticleEdit' diff --git a/frontend/src/modules/editor/block/views/editorBlockEditView.js b/frontend/src/modules/editor/block/views/editorBlockEditView.js index 422071b3a7..cb089ff51a 100644 --- a/frontend/src/modules/editor/block/views/editorBlockEditView.js +++ b/frontend/src/modules/editor/block/views/editorBlockEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorBlockEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); } }, { template: 'editorBlockEdit' diff --git a/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js b/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js index f2f4478cc9..2c9deb7e70 100644 --- a/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js +++ b/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js @@ -18,9 +18,14 @@ define(function(require) { cancelEditing: function(event) { event.preventDefault(); - var currentCourseId = Origin.editor.data.course.get('_id'); - var currentPageId = this.model.getParent().getParent().getParent().get('_id'); - Origin.router.navigateTo('editor/' + currentCourseId + '/page/' + currentPageId); + // FIXME got to be a better way to do this + this.model.fetchParent(function(parentBlock) { + parentBlock.fetchParent(function(parentArticle) { + parentArticle.fetchParent(function(parentPage) { + Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/page/' + parentPage.get('_id')); + }); + }); + }); } }, { template: 'editorComponentEditSidebar' diff --git a/frontend/src/modules/editor/component/views/editorComponentEditView.js b/frontend/src/modules/editor/component/views/editorComponentEditView.js index 42c6954d1c..c25be986a4 100644 --- a/frontend/src/modules/editor/component/views/editorComponentEditView.js +++ b/frontend/src/modules/editor/component/views/editorComponentEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorComponentEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); }, cancel: function (event) { diff --git a/frontend/src/modules/editor/contentObject/index.js b/frontend/src/modules/editor/contentObject/index.js index 3e70eca362..9a13eb68c5 100644 --- a/frontend/src/modules/editor/contentObject/index.js +++ b/frontend/src/modules/editor/contentObject/index.js @@ -4,10 +4,8 @@ define(function(require) { * This module handles both sections/menus and pages. */ var Origin = require('core/origin'); - var ContentObjectModel = require('core/models/contentObjectModel'); var EditorMenuSidebarView = require('./views/editorMenuSidebarView'); - var EditorPageComponentListView = require('./views/editorPageComponentListView'); var EditorPageEditView = require('./views/editorPageEditView'); var EditorPageEditSidebarView = require('./views/editorPageEditSidebarView'); var EditorPageSidebarView = require('./views/editorPageSidebarView'); @@ -15,37 +13,28 @@ define(function(require) { var Helpers = require('../global/helpers'); Origin.on('editor:contentObject', function(data) { - if(data.action === 'edit') renderContentObjectEdit(data); - else if(data.id) renderPageStructure(data); - else renderMenuStructure(data); - }); - - // component add is just a page overlay view, so handling it here - Origin.on('editor:block', function(data) { - if(data.action !== 'add') { - return; + var route = function() { + if(data.action === 'edit') renderContentObjectEdit(data); + else if(data.id) renderPageStructure(data); + else renderMenuStructure(data); + } + if(!data.id) { + return route(); } - var containingBlock = Origin.editor.data.blocks.findWhere({ _id: Origin.location.route3 }); - var layoutOptions = containingBlock.get('layoutOptions'); - var componentsModel = new Backbone.Model({ - title: Origin.l10n.t('app.addcomponent'), - body: Origin.l10n.t('app.pleaseselectcomponent'), - _parentId: Origin.location.route3, - componentTypes: Origin.editor.data.componenttypes.toJSON(), - layoutOptions: layoutOptions - }); - Origin.contentPane.setView(EditorPageComponentListView, { model: componentsModel }); - }); - - function renderContentObjectEdit(data) { (new ContentObjectModel({ _id: data.id })).fetch({ success: function(model) { + data.model = model; Helpers.setPageTitle(model); - var form = Origin.scaffold.buildForm({ model: model }); - Origin.sidebar.addView(new EditorPageEditSidebarView({ form: form }).$el); - Origin.contentPane.setView(EditorPageEditView, { model: model, form: form }); + route(); } }); + }); + + function renderContentObjectEdit(data) { + Helpers.setPageTitle(data.model, true); + var form = Origin.scaffold.buildForm({ model: data.model }); + Origin.sidebar.addView(new EditorPageEditSidebarView({ form: form }).$el); + Origin.contentPane.setView(EditorPageEditView, { model: data.model, form: form }); } function renderPageStructure(data) { @@ -66,7 +55,6 @@ define(function(require) { function renderMenuStructure(data) { Helpers.setPageTitle(Origin.editor.data.course, true); - Origin.editor.currentContentObjectId = data.id; Origin.editor.scrollTo = 0; Origin.sidebar.addView(new EditorMenuSidebarView().$el, { diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js b/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js index adeafc4414..f5336d90ab 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js @@ -24,11 +24,6 @@ define(function(require){ postRender: function() { this.setupEvents(); - // Check if the current item is expanded and update the next menuLayerView - // This can end up being recursive if an item is selected inside a few menu items - if(this.model.get('_isExpanded')) { - Origin.trigger('editorView:menuView:updateSelectedItem', this); - } }, remove: function() { @@ -39,31 +34,23 @@ define(function(require){ setupEvents: function() { this.listenTo(Origin, 'editorView:removeSubViews', this.remove); - this.listenTo(this.model, { - 'change:_isExpanded': this.onExpandedChange, - 'change:_isSelected': this.onSelectedChange - }); - // Handle the context menu clicks - this.on('contextMenu:' + this.model.get('_type') + ':edit', this.editMenuItem); - this.on('contextMenu:' + this.model.get('_type') + ':copy', this.copyMenuItem); - this.on('contextMenu:' + this.model.get('_type') + ':copyID', this.copyID); - this.on('contextMenu:' + this.model.get('_type') + ':delete', this.deleteItemPrompt); + var type = this.model.get('_type'); + + this.on('contextMenu:' + type + ':edit', this.editMenuItem); + this.on('contextMenu:' + type + ':copy', this.copyMenuItem); + this.on('contextMenu:' + type + ':copyID', this.copyID); + this.on('contextMenu:' + type + ':delete', this.deleteItemPrompt); this.$el.closest('.editor-menu').on('mousemove', _.bind(this.handleDrag, this)); }, setupClasses: function() { - var classString = ''; - if (this.model.get('_isSelected')) classString += 'selected '; - if(this.model.get('_isExpanded')) classString += 'expanded '; - classString += ('content-type-'+this.model.get('_type')); - this.$el.addClass(classString); + this.$el.addClass('content-type-' + this.model.get('_type')); }, onMenuItemClicked: function(event) { event && event.preventDefault(); - // select item regardless of single/double click - this.setItemAsSelected(); + this.trigger('click', this); // handle double-click if(this.clickTimerActive) { return this.onMenuItemDoubleClicked(event); @@ -78,75 +65,7 @@ define(function(require){ onMenuItemDoubleClicked: function(event) { event && event.preventDefault(); - var type = this.model.get('_type'); - if(type === 'page') { - this.gotoPageEditor(); - } - else if(type === 'menu') { - this.gotoSubMenuEditor(); - } - }, - - gotoPageEditor: function() { - Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/page/' + this.model.get('_id')); - }, - - gotoSubMenuEditor: function() { - Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/menu/' + this.model.get('_id') + '/edit'); - }, - - setItemAsSelected: function() { - if(this.model.get('_isSelected')) { - return; - } - if(this.model.get('_isExpanded')) { - // bit odd, but we need to remove and child views before we continue - this.model.set('_isExpanded', false); - } - else { - this.setSiblingsSelectedState(); - this.setParentSelectedState(); - } - this.model.set({ - _isExpanded: this.model.get('_type') === 'menu', - _isSelected: true - }); - // This event passes out the view to the editorMenuView to add - // a editorMenuLayerView and setup this.subView - Origin.trigger('editorView:menuView:updateSelectedItem', this); - }, - - setParentSelectedState: function() { - this.model.getParent().set('_isSelected', false); - }, - - setSiblingsSelectedState: function() { - this.model.getSiblings().each(function(sibling) { - sibling.set({ _isSelected: false, _isExpanded: false }); - }); - }, - - setChildrenSelectedState: function() { - this.model.getChildren().each(function(child) { - child.set({ _isSelected: false, _isExpanded: false }); - }) - }, - - onSelectedChange: function(model, isSelected) { - this.$el.toggleClass('selected', isSelected); - }, - - onExpandedChange: function(model, isExpanded) { - var isMenuType = (this.model.get('_type') === 'menu'); - if(isExpanded) { - this.$el.addClass('expanded'); - return; - } - if(isMenuType) { - this.setChildrenSelectedState(); - if (this.subView) this.subView.remove(); - } - this.$el.removeClass('expanded'); + this.trigger('dblclick', this); }, editMenuItem: function() { @@ -193,10 +112,6 @@ define(function(require){ deleteItem: function(event) { this.stopListening(Origin, 'editorView:cancelRemoveItem:'+ this.model.get('_id'), this.cancelDeleteItem); - this.model.set({ _isExpanded: false, _isSelected: false }); - // When deleting an item - the parent needs to be selected - this.model.getParent().set({ _isSelected: true, _isExpanded: true }); - // We also need to navigate to the parent element - but if it's the courseId let's // navigate up to the menu var type = this.model.get('_type'); @@ -204,12 +119,22 @@ define(function(require){ var parentId = isTopLevel ? '' : '/' + this.model.get('_parentId'); Origin.router.navigateTo('editor/' + Origin.editor.data.course.id + '/menu' + parentId); - if(this.model.destroy()) this.remove(); + this.model.destroy({ + success: _.bind(function(model) { + Origin.trigger('editorView:itemDeleted', model); + this.remove() + }, this), + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errordelete' + }); + } + }); }, cancelDeleteItem: function() { this.stopListening(Origin, 'editorView:removeItem:'+ this.model.get('_id'), this.deleteItem); - this.model.set({ _isSelected: true }); }, enableDrag: function(event) { diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js b/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js index e4d3cf7413..893a4fa6c9 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js @@ -9,37 +9,56 @@ define(function(require) { var EditorMenuLayerView = EditorOriginView.extend({ className: 'editor-menu-layer', + models: undefined, events: { - 'click button.editor-menu-layer-add-page': 'addPage', - 'click button.editor-menu-layer-add-menu': 'addMenu', + 'click button.editor-menu-layer-add-page': 'addNewPage', + 'click button.editor-menu-layer-add-menu': 'addNewMenu', 'click .editor-menu-layer-paste': 'pasteMenuItem', 'click .editor-menu-layer-paste-cancel': 'cancelPasteMenuItem' }, - preRender: function(options) { - if(options._parentId) this._parentId = options._parentId; + initialize: function(options) { + this.models = options.models; + EditorOriginView.prototype.initialize.apply(this, arguments); + }, - this.listenTo(Origin, { + preRender: function(options) { + this.childViews = []; + if (options._parentId) { + this._parentId = options._parentId; + } + var events = { 'editorView:removeSubViews': this.remove, 'editorMenuView:removeMenuViews': this.remove - }); - }, - - postRender: function() { - // Append the parentId value to the container to allow us to move pages, etc. - if(this._parentId) this.$el.attr('data-parentId', this._parentId); - this.setHeight(); + }; + events['editorView:pasted:' + this._parentId] = this.onPaste; + this.listenTo(Origin, events); }, render: function() { var data = this.data ? this.data : false; var template = Handlebars.templates[this.constructor.template]; + this.$el.html(template(data)); + this.renderMenuItems(); + _.defer(_.bind(this.postRender, this)); return this; }, + renderMenuItems: function() { + for(var i = 0, count = this.models.length; i < count; i++) { + this.addMenuItemView(this.models[i]); + } + }, + + postRender: function() { + // Append the parentId value to the container to allow us to move pages, etc. + if (this._parentId) this.$el.attr('data-parentid', this._parentId); + this.setHeight(); + }, + setHeight: function() { var windowHeight = $(window).height(); var offsetTop = $('.editor-menu-inner').offset().top; @@ -48,19 +67,19 @@ define(function(require) { this.$('.editor-menu-layer-inner').height(windowHeight-(offsetTop+controlsHeight)); }, - addMenu: function(event) { - this.addMenuItem(event, 'menu'); + addNewMenu: function(event) { + this.addNewMenuItem(event, 'menu'); }, - addPage: function(event) { - this.addMenuItem(event, 'page'); + addNewPage: function(event) { + this.addNewMenuItem(event, 'page'); }, /** * Adds a new contentObject of a given type * @param {String} type Given contentObject type, i.e. 'menu' or 'page' */ - addMenuItem: function(event, type) { + addNewMenuItem: function(event, type) { event && event.preventDefault(); var newMenuItemModel = new ContentObjectModel({ @@ -75,6 +94,8 @@ define(function(require) { }); // Instantly add the view for UI purposes var newMenuItemView = this.addMenuItemView(newMenuItemModel); + newMenuItemView.$el.addClass('syncing'); + newMenuItemModel.save(null, { error: function(error) { // fade out menu item and alert @@ -86,15 +107,16 @@ define(function(require) { _.delay(newMenuItemView.remove, 3000); }, success: _.bind(function(model) { - Origin.editor.data.contentObjects.add(model); + Origin.trigger('editorView:menuView:addItem', model); // Force setting the data-id attribute as this is required for drag-drop sorting + newMenuItemView.$el.attr('data-id', model.get('_id')); newMenuItemView.$el.children('.editor-menu-item-inner').attr('data-id', model.get('_id')); if (type === 'page') { // HACK -- This should be removed and placed on the server-side this.addNewPageArticleAndBlock(model, newMenuItemView); return; } - newMenuItemView.$el.removeClass('syncing').addClass('synced'); + newMenuItemView.$el.removeClass('syncing'); this.setHeight(); }, this) }); @@ -135,13 +157,10 @@ define(function(require) { _.delay(newMenuItemView.remove, 3000); }, success: _.bind(function(model, response, options) { - // Add this new element to the collect - Origin.editor.data[model.get('_type') + 's'].add(model); - if (typeToAdd === 'article') { this.addNewPageArticleAndBlock(model, newMenuItemView); } else { - newMenuItemView.$el.removeClass('syncing').addClass('synced'); + newMenuItemView.$el.removeClass('syncing'); } }, this) }); @@ -149,7 +168,14 @@ define(function(require) { addMenuItemView: function(model) { var newMenuItemView = new EditorMenuItemView({ model: model }); - this.$('.editor-menu-layer-inner').append(newMenuItemView.$el.addClass('syncing')); + this.$('.editor-menu-layer-inner').append(newMenuItemView.$el); + + newMenuItemView.on({ + 'click': _.bind(this.onMenuItemClicked, this), + 'dblclick': _.bind(this.onMenuItemDblclicked, this) + }); + + this.childViews.push(newMenuItemView); return newMenuItemView; }, @@ -167,7 +193,50 @@ define(function(require) { _courseId: Origin.editor.data.course.get('_id') }); Origin.trigger('editorView:pasteCancel', target); + }, + + onMenuItemClicked: function(menuItem) { + // if item's already selected, don't bother continuing + if (menuItem.$el.hasClass('selected')) { + return; + } + Origin.trigger('editorView:menuView:updateSelectedItem', menuItem.model); + }, + + onMenuItemDblclicked: function(menuItem) { + var courseId = Origin.editor.data.course.get('_id'); + var id = menuItem.model.get('_id'); + var type = menuItem.model.get('_type'); + + var route = 'editor/' + courseId + '/' + type + '/' + id; + if (type === 'menu') route += '/edit'; + + Origin.router.navigateTo(route); + }, + + // called after a successful paste + onPaste: function(data) { + (new ContentObjectModel({ _id: data._id})).fetch({ + success: _.bind(function(model) { + this.addMenuItemView(model); + }, this), + error: function() { + + } + }); + }, + + removeChildViews: function() { + for (var i = 0; i < this.childViews.length; i++) { + this.childViews[i].remove(); + } + }, + + remove: function() { + this.removeChildViews(); + EditorOriginView.prototype.remove.apply(this, arguments); } + }, { template: 'editorMenuLayer' }); diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuView.js b/frontend/src/modules/editor/contentObject/views/editorMenuView.js index 2e790093e0..eb0ef42367 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuView.js @@ -1,6 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Origin = require('core/origin'); + var Helpers = require('core/helpers'); + var ContentCollection = require('core/collections/contentCollection'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorMenuLayerView = require('./editorMenuLayerView'); var EditorMenuItemView = require('./editorMenuItemView'); @@ -10,103 +12,142 @@ define(function(require){ tagName: "div", preRender: function() { + this.layerViews = {}; this.listenTo(Origin, { - 'editorView:menuView:updateSelectedItem': this.updateSelectedItem, + 'editorView:menuView:updateSelectedItem': this.onSelectedItemChanged, + 'editorView:menuView:addItem': this.onItemAdded, + 'editorView:itemDeleted': this.onItemDeleted, 'window:resize': this.setupHorizontalScroll }); }, postRender: function() { - this.setupMenuViews(); - _.defer(this.setViewToReady); - }, - - setupMenuViews: function() { - this.addMenuLayerView(this); - if (!Origin.editor.currentContentObjectId) { - return; - } - this.restoreCurrentMenuState(); + this.contentobjects = new ContentCollection(null, { + _type: 'contentobject', + _courseId: Origin.editor.data.course.get('_id') + }); + this.contentobjects.fetch({ + success: _.bind(function(children) { + this.contentobjects = children; + this.renderLayers(); + _.defer(this.setViewToReady); + }, this), + error: console.error + }); }, /** - * Recursive function which shows the expanded children for a given context model - * @param {Model} A given contextObject model - */ - addMenuLayerView: function(view) { - var menuLayer = this.renderMenuLayerView(view); - // Add children views of current model - view.model.getChildren().each(function(contentObject) { - menuLayer.append(new EditorMenuItemView({ model: contentObject }).$el); - }, this); - - _.defer(_.bind(function() { - this.setupDragDrop(); - var $window = $(window); - this.setupHorizontalScroll($window.width(), $window.height()); - this.scrollToElement(); - }, this)); - }, + * Renders all menu layers from the current course to the Origin.editor.currentContentObject + */ + renderLayers: function() { + var selectedModel = Origin.editor.currentContentObject || Origin.editor.data.course; + this.getItemHeirarchy(selectedModel, function(hierarchy) { + var ids = []; + + for (var i = 0; i < hierarchy.length; i++) { + var item = hierarchy[i]; + var id = item.get('_id'); + ids.push(id); + if (!this.layerViews.hasOwnProperty(id)) { + this.renderLayer(item); + } + } - /** - * Appemds a menu item layer for a given ID to the editor - * @param {String} parentId Unique identifier of the parent - */ - renderMenuLayerView: function(view) { - // Get the current views _id to store as the _parentId - var parentId = view.model.get('_id'); - // Create MenuLayerView - var menuLayerView = new EditorMenuLayerView({ _parentId: parentId }); - // Set subview on layerView so this can be removed - view.subView = menuLayerView; - // Render and append the view - $('.editor-menu-inner').append(menuLayerView.$el); - // Return the container ready to render menuItemView's - return menuLayerView.$('.editor-menu-layer-inner'); + // remove all unused layerviews + for (var id in this.layerViews) { + if (!this.layerViews.hasOwnProperty(id) || ids.indexOf(id) > -1) { + continue; + } + this.layerViews[id].remove(); + delete this.layerViews[id]; + } + + _.defer(_.bind(function() { + this.removeSelectedItemStyling(); + this.addSelectedItemStyling(selectedModel.get('_id')); + this.setUpInteraction(); + }, this)); + }); }, /** - * Restores the current menu state by finding the current element - * then setting it's parent recursively to _isExpanded + * Renders a single menu layer */ - restoreCurrentMenuState: function() { - // Find current menu item - var currentSelectedMenuItem = Origin.editor.data.contentObjects.findWhere({ - _id: Origin.editor.currentContentObjectId + renderLayer: function(model) { + var menuLayerView = new EditorMenuLayerView({ + _parentId: model.get('_id'), + models: this.contentobjects.where({ _parentId: model.get('_id') }) }); - currentSelectedMenuItem.set({ _isSelected: true, _isExpanded: true }); - this.setParentElementToSelected(currentSelectedMenuItem); + this.layerViews[model.get('_id')] = menuLayerView; + $('.editor-menu-inner').append(menuLayerView.$el); + }, + + updateItemViews: function(previousParent, model) { + // since we remove the childViews when the layerView is destroyed + // we must move menuItemView to its new layerView + var index = -1; + for (var i = 0; i < this.layerViews[previousParent].childViews.length; i++) { + var v = this.layerViews[previousParent].childViews[i]; + if (v.model.get('_id') === model.get('_id')) { + index = i; + break; + } + } + var view = this.layerViews[previousParent].childViews.splice(index, 1); + this.layerViews[model.get('_parentId')].childViews.push(view[0]); }, - /** - * This is triggered when an item is clicked - */ - updateSelectedItem: function(view) { - // store the ID of the currently selected contentObject - Origin.editor.currentContentObjectId = view.model.get('_id'); + setUpInteraction: function() { + this.setupDragDrop(); + var $window = $(window); + this.setupHorizontalScroll($window.width(), $window.height()); + this.scrollToElement(); + }, - if(view.model.get('_type') === 'menu') { - this.addMenuLayerView(view); - return; + addSelectedItemStyling: function(id) { + this.$('.editor-menu-item[data-id="' + id + '"]').addClass('selected'); + var model = this.contentobjects.findWhere({ _id: id }); + var parentId = model && model.get('_parentId'); + if (parentId) { + // recurse + this.addSelectedItemStyling(parentId); } - this.scrollToElement(); }, - /** - * Recursive function which shows any children for a given contentObject and sets - * the UI element to 'expanded' - * @param {Model} selectedItem A given contextObject model - */ - setParentElementToSelected: function(selectedItem) { - var parentId = selectedItem.get('_parentId'); + removeSelectedItemStyling: function() { + this.$('.editor-menu-item').removeClass('selected'); + }, - if(parentId === Origin.editor.data.course.get('_id')) { - return; + /** + * Generates an array with the inheritence line from a given contentobject to the current course + * @param {Model} contentModel + * @return {Array} + */ + getItemHeirarchy: function(model, done) { + var hierarchy = []; + if (model.get('_type') === 'menu') { + hierarchy.push(model); } - var parentModel = Origin.editor.data.contentObjects.findWhere({ _id: parentId }); - parentModel.set('_isExpanded', true); + var __this = this; + var _getParent = function(model, callback) { + var parent = __this.contentobjects.findWhere({ _id: model.get('_parentId') }); + if (parent) { + hierarchy.push(parent); + return _getParent(parent, callback); + } + hierarchy.push(Origin.editor.data.course); + callback(); + }; + _getParent(model, function() { + if (typeof done === 'function') done.call(__this, hierarchy.reverse()); + }); + }, + + onSelectedItemChanged: function(model) { + if (model && model.get('_id') === Origin.editor.currentContentObject && Origin.editor.currentContentObject.get('_id')) return; - this.setParentElementToSelected(parentModel); + Origin.editor.currentContentObject = model; + this.renderLayers(); }, setupHorizontalScroll: function(windowWidth, windowHeight) { @@ -138,15 +179,24 @@ define(function(require){ connectWith: ".editor-menu-layer-inner", scroll: true, helper: 'clone', - stop: function(event,ui) { + stop: _.bind(function(event,ui) { var $draggedElement = ui.item; var id = $('.editor-menu-item-inner', $draggedElement).attr('data-id'); var sortOrder = $draggedElement.index() + 1; var parentId = $draggedElement.closest('.editor-menu-layer').attr('data-parentId'); - var currentModel = Origin.editor.data.contentObjects.findWhere({ _id: id }); - currentModel.save({ _sortOrder: sortOrder, _parentId: parentId }, { patch: true }); + var currentModel = this.contentobjects.findWhere({ _id: id }); + var previousParent = currentModel.get('_parentId'); + currentModel.save({ + _sortOrder: sortOrder, + _parentId: parentId + }, { + patch: true, + success: _.bind(function(model, response, options) { + this.updateItemViews(previousParent, model); + }, this) + }); currentModel.set('_isDragging', false); - }, + }, this), over: function(event, ui) { $(event.target).closest('.editor-menu-layer').attr('data-over', true); }, @@ -158,6 +208,25 @@ define(function(require){ if (ui.item.hasClass('content-type-menu')) ui.sender.sortable("cancel"); } }); + }, + + onItemAdded: function(newModel) { + this.contentobjects.add(newModel); + }, + + onItemDeleted: function(oldModel) { + this.contentobjects.fetch({ + success: _.bind(function() { + // select the parent of the deleted item + Origin.trigger('editorView:menuView:updateSelectedItem', this.contentobjects.findWhere({ _id: oldModel.get('_parentId') })); + }, this), + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorMenu' diff --git a/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js b/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js index a7d79a4edf..8f5e7d0630 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js @@ -1,9 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var BlockModel = require('core/models/blockModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageBlockView = require('./editorPageBlockView'); @@ -25,12 +22,10 @@ define(function(require){ }, postRender: function() { - this.addBlockViews(); + if (!this._skipRender) { + this.addBlockViews(); + } this.setupDragDrop(); - _.defer(_.bind(function(){ - this.trigger('articleView:postRender'); - Origin.trigger('pageView:itemRendered'); - }, this)); }, listenToEvents: function() { @@ -40,8 +35,8 @@ define(function(require){ var id = this.model.get('_id'); var events = {}; events['editorView:moveBlock:' + id] = this.render; - events['editorView:cutBlock:' + id] = this.onCutBlock; events['editorView:deleteArticle:' + id] = this.deletePageArticle; + events['editorView:pasted:' + id] = this.onPaste; this.listenTo(Origin, events); } @@ -49,18 +44,10 @@ define(function(require){ 'contextMenu:article:edit': this.loadArticleEdit, 'contextMenu:article:copy': this.onCopy, 'contextMenu:article:copyID': this.onCopyID, - 'contextMenu:article:cut': this.onCut, 'contextMenu:article:delete': this.deleteArticlePrompt }); }, - onCutBlock: function(view) { - this.once('articleView:postRender', function() { - view.showPasteZones(); - }); - this.render(); - }, - addBlockViews: function() { this.$('.article-blocks').empty(); // Insert the 'pre' paste zone for blocks @@ -73,72 +60,64 @@ define(function(require){ }); this.$('.article-blocks').append(view.$el); // Iterate over each block and add it to the article - this.model.getChildren().each(this.addBlockView, this); + this.model.fetchChildren(_.bind(function(children) { + Origin.editor.blockCount += children.length; + for(var i = 0, count = children.length; i < count; i++) { + this.addBlockView(children[i]); + } + }, this)); }, addBlockView: function(blockModel, scrollIntoView) { - var newBlockView = new EditorPageBlockView({model: blockModel}); - var sortOrder = blockModel.get('_sortOrder'); - - // Add syncing class - if (blockModel.isNew()) { - newBlockView.$el.addClass('syncing'); - } - scrollIntoView = scrollIntoView || false; - this.$('.article-blocks').append(newBlockView.$el); + var newBlockView = new EditorPageBlockView({ model: blockModel }); + var $blocks = this.$('.article-blocks .block'); + var sortOrder = blockModel.get('_sortOrder'); + var index = sortOrder > 0 ? sortOrder-1 : undefined; + var shouldAppend = index === undefined || index >= $blocks.length || $blocks.length === 0; - if (scrollIntoView) { - $.scrollTo(newBlockView.$el, 200); + if(shouldAppend) { // add to the end of the article + this.$('.article-blocks').append(newBlockView.$el); + } else { // 'splice' block into the new position + $($blocks[index]).before(newBlockView.$el); } - + if (scrollIntoView) $.scrollTo(newBlockView.$el, 200); // Increment the sortOrder property - blockModel.set('_pasteZoneSortOrder', ++sortOrder); - + blockModel.set('_pasteZoneSortOrder', (blockModel.get('_sortOrder')+1)); // Post-block paste zone - sort order of placeholder will be one greater - this.$('.article-blocks').append(new EditorPasteZoneView({model: blockModel}).$el); - // Return the block view so syncing can be shown - return newBlockView; + this.$('.article-blocks').append(new EditorPasteZoneView({ model: blockModel }).$el); }, addBlock: function(event) { event && event.preventDefault(); - - var self = this; - var layoutOptions = [{ - type: 'left', - name: 'app.layoutleft', - pasteZoneRenderOrder: 2 - }, { - type: 'full', - name: 'app.layoutfull', - pasteZoneRenderOrder: 1 - }, { - type: 'right', - name: 'app.layoutright', - pasteZoneRenderOrder: 3 - }]; - - var newPageBlockModel = new BlockModel({ + var model = new BlockModel(); + model.save({ title: Origin.l10n.t('app.placeholdernewblock'), displayTitle: Origin.l10n.t('app.placeholdernewblock'), body: '', - _parentId: self.model.get('_id'), + _parentId: this.model.get('_id'), _courseId: Origin.editor.data.course.get('_id'), - layoutOptions: layoutOptions, + layoutOptions: [{ + type: 'left', + name: 'app.layoutleft', + pasteZoneRenderOrder: 2 + }, { + type: 'full', + name: 'app.layoutfull', + pasteZoneRenderOrder: 1 + }, { + type: 'right', + name: 'app.layoutright', + pasteZoneRenderOrder: 3 + }], _type: 'block' - }); - - newPageBlockModel.save(null, { + }, { + success: _.bind(function(model, response, options) { + this.addBlockView(model, true); + }, this), error: function() { Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingblock') }); - }, - success: function(model, response, options) { - var newBlockView = self.addBlockView(model, true); - Origin.editor.data.blocks.add(model); - newBlockView.$el.removeClass('syncing').addClass('synced'); - newBlockView.reRender(); } }); }, @@ -241,6 +220,20 @@ define(function(require){ $container.scrollTop($(this).offset().top*-1); } }); + }, + + onPaste: function(data) { + (new BlockModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.addBlockView(model); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorPageArticle' diff --git a/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js b/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js index 148b4b5e1c..de41e63203 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js @@ -1,9 +1,7 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var ComponentModel = require('core/models/componentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageComponentView = require('./editorPageComponentView'); @@ -11,59 +9,89 @@ define(function(require){ var EditorPageComponentListView = require('./editorPageComponentListView'); var EditorPageBlockView = EditorOriginView.extend({ - className: 'block editable block-draggable', + className: 'block editable block-draggable page-content-syncing', tagName: 'div', - settings: { + settings: _.extend({}, EditorOriginView.prototype.settings, { + hasAsyncPostRender: true, autoRender: false - }, + }), events: _.extend({}, EditorOriginView.prototype.events, { 'click a.block-delete': 'deleteBlockPrompt', 'click a.add-component': 'showComponentList', 'click a.open-context-block': 'openContextMenu', - 'dblclick': 'loadBlockEdit' + 'dblclick': 'loadBlockEdit', + 'transitionend': 'onTransitionEnd' }), preRender: function() { this.listenToEvents(); this.model.set('componentTypes', Origin.editor.data.componenttypes.toJSON()); - // seems odd calling re-render here, but it does what we want - this.reRender(); + this.render(); + }, + + render: function() { + this.model.fetchChildren(_.bind(function(components) { + this.children = components; + var layouts = this.getAvailableLayouts(); + // FIXME why do we have two attributes with the same value? + this.model.set({ layoutOptions: layouts, dragLayoutOptions: layouts }); + + EditorOriginView.prototype.render.apply(this); + + this.addComponentViews(); + this.setupDragDrop(); + + this.handleAsyncPostRender(); + }, this)); + }, + + animateIn: function() { + this.$el.removeClass('page-content-syncing'); + }, + + handleAsyncPostRender: function() { + var renderedChildren = []; + if(this.children.length === 0) { + return this.animateIn(); + } + this.listenTo(Origin, 'editorPageComponent:postRender', function(view) { + var id = view.model.get('_id'); + if(this.children.indexOf(view.model) !== -1 && renderedChildren.indexOf(id) === -1) { + renderedChildren.push(id); + } + if(renderedChildren.length === this.children.length) { + this.stopListening(Origin, 'editorPageComponent:postRender'); + this.animateIn(); + } + }); }, listenToEvents: function() { var id = this.model.get('_id'); - var events = {}; - events['editorView:removeSubViews editorPageView:removePageSubViews'] = this.remove; - events['editorView:removeComponent:' + id] = this.handleRemovedComponent; - events['editorView:moveComponent:' + id] = this.reRender; - events['editorView:cutComponent:' + id] = this.onCutComponent; - events['editorView:addComponent:' + id] = this.addComponent; - events['editorView:deleteBlock:' + id] = this.deleteBlock; + var events = { + 'editorView:removeSubViews editorPageView:removePageSubViews': this.remove + }; + events[ + 'editorView:addComponent:' + id + ' ' + + 'editorView:removeComponent:' + id + ' ' + + 'editorView:moveComponent:' + id + ] = this.render; + events['editorView:pasted:' + id] = this.onPaste; this.listenTo(Origin, events); this.listenTo(this, { 'contextMenu:block:edit': this.loadBlockEdit, 'contextMenu:block:copy': this.onCopy, 'contextMenu:block:copyID': this.onCopyID, - 'contextMenu:block:cut': this.onCut, 'contextMenu:block:delete': this.deleteBlockPrompt }); }, postRender: function() { - this.addComponentViews(); - this.setupDragDrop(); - - _.defer(_.bind(function(){ - this.trigger('blockView:postRender'); - Origin.trigger('pageView:itemRendered'); - }, this)); - }, - - reRender: function() { - this.evaluateComponents(this.render); + this.trigger('blockView:postRender'); + Origin.trigger('pageView:itemRendered', this); }, getAvailableLayouts: function() { @@ -72,26 +100,17 @@ define(function(require){ left: { type: 'left', name: 'app.layoutleft', pasteZoneRenderOrder: 2 }, right: { type: 'right', name: 'app.layoutright', pasteZoneRenderOrder: 3 } }; - var components = this.model.getChildren(); - if (components.length === 0) { + if (this.children.length === 0) { return [layoutOptions.full,layoutOptions.left,layoutOptions.right]; } - if (components.length === 1) { - var layout = components.at(0).get('_layout'); - if(layout === 'left') return [layoutOptions.right]; - if(layout === 'right') return [layoutOptions.left]; + if (this.children.length === 1) { + var layout = this.children[0].get('_layout'); + if(layout === layoutOptions.left.type) return [layoutOptions.right]; + if(layout === layoutOptions.right.type) return [layoutOptions.left]; } return []; }, - evaluateComponents: function(callback) { - this.model.set({ - layoutOptions: this.getAvailableLayouts(), - dragLayoutOptions: this.getAvailableLayouts() - }); - if(callback) callback.apply(this); - }, - deleteBlockPrompt: function(event) { event && event.preventDefault(); @@ -99,16 +118,12 @@ define(function(require){ type: 'warning', title: Origin.l10n.t('app.deleteblock'), text: Origin.l10n.t('app.confirmdeleteblock') + '
    ' + '
    ' + Origin.l10n.t('app.confirmdeleteblockwarning'), - callback: _.bind(this.deleteBlockConfirm, this) + callback: _.bind(function(confirmed) { + if (confirmed) this.deleteBlock(); + }, this) }); }, - deleteBlockConfirm: function(confirmed) { - if (confirmed) { - Origin.trigger('editorView:deleteBlock:' + this.model.get('_id')); - } - }, - deleteBlock: function(event) { this.model.destroy({ success: _.bind(this.remove, this), @@ -118,18 +133,6 @@ define(function(require){ }); }, - handleRemovedComponent: function() { - this.reRender(); - }, - - onCutComponent: function(view) { - this.once('blockView:postRender', function() { - view.showPasteZones(); - }); - - this.reRender(); - }, - setupDragDrop: function() { var view = this; var autoScrollTimer = false; @@ -194,26 +197,20 @@ define(function(require){ addComponentViews: function() { this.$('.page-components').empty(); - var components = this.model.getChildren(); - var addPasteZonesFirst = components.length && components.at(0).get('_layout') != 'full'; - this.addComponentButtonLayout(components); - - if (addPasteZonesFirst) { - this.setupPasteZones(); - } + var addPasteZonesFirst = this.children.length && this.children[0].get('_layout') !== 'full'; + this.addComponentButtonLayout(this.children); + if (addPasteZonesFirst) this.setupPasteZones(); // Add component elements - this.model.getChildren().each(function(component) { - this.$('.page-components').append(new EditorPageComponentView({ model: component }).$el); - }, this); - - if (!addPasteZonesFirst) { - this.setupPasteZones(); + for(var i = 0, count = this.children.length; i < count; i++) { + var view = new EditorPageComponentView({ model: this.children[i] }); + this.$('.page-components').append(view.$el); } + if (!addPasteZonesFirst) this.setupPasteZones(); }, - addComponentButtonLayout: function(components){ + addComponentButtonLayout: function(components) { if(components.length === 2) { return; } @@ -221,7 +218,10 @@ define(function(require){ this.$('.add-component').addClass('full'); return; } - var className = (components.models[0].attributes._layout === 'left') ? 'right' : 'left'; + var layout = components[0].get('_layout'); + var className = ''; + if(layout === 'left') className = 'right'; + if(layout === 'right') className = 'left'; this.$('.add-component').addClass(className); }, @@ -255,15 +255,8 @@ define(function(require){ setupPasteZones: function() { // Add available paste zones - var layouts = []; - var dragLayouts = []; - - _.each(this.model.get('dragLayoutOptions'), function (dragLayout) { - dragLayouts.push(dragLayout); - }); - _.each(this.model.get('layoutOptions'), function (layout) { - layouts.push(layout); - }); + var layouts = this.model.get('layoutOptions').slice(); + var dragLayouts = this.model.get('dragLayoutOptions').slice(); _.each(this.sortArrayByKey(dragLayouts, 'pasteZoneRenderOrder'), function(layout) { var pasteComponent = new ComponentModel(); @@ -284,18 +277,26 @@ define(function(require){ }, this); }, - swapLayout: function (layout) { - if (layout === 'full') { - return layout; - } - return (layout == 'left') ? 'right' : 'left'; + onPaste: function(data) { + (new ComponentModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.children.push(model); + this.render(); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); }, - toggleAddComponentsButton: function() { - var layoutOptions = this.model.get('layoutOptions') || []; - // display-none if we've no layout options - this.$('.add-control').toggleClass('display-none', layoutOptions.length === 0); + onTransitionEnd: function(event) { + if (event.originalEvent.propertyName !== 'transform') return; + Origin.trigger('pageView:itemAnimated', this); } + }, { template: 'editorPageBlock' }); diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js index 511ec06c78..056b933ab7 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js @@ -1,8 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - var Backbone = require('backbone'); var Origin = require('core/origin'); - var ComponentModel = require('core/models/componentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageComponentView = require('./editorPageComponentView'); @@ -64,12 +62,10 @@ define(function(require) { addComponent: function(layout) { Origin.trigger('editorComponentListView:remove'); - var componentName = this.model.get('name'); - var componentType = _.find(Origin.editor.data.componenttypes.models, function(type){ - return type.get('name') == componentName; - }); + var componentType = Origin.editor.data.componenttypes.findWhere({ name: this.model.get('name') }); + var model = new ComponentModel(); - var newComponentModel = new ComponentModel({ + model.save({ title: Origin.l10n.t('app.placeholdernewcomponent'), displayTitle: Origin.l10n.t('app.placeholdernewcomponent'), body: '', @@ -81,28 +77,17 @@ define(function(require) { _component: componentType.get('component'), _layout: layout, version: componentType.get('version') - }); - - var newComponentView = new EditorPageComponentView({ model: newComponentModel }).$el.addClass('syncing'); - - this.$parentElement - .find('.page-components') - .append(newComponentView); - - newComponentModel.save(null, { + }, { + success: _.bind(function(model) { + var parentId = model.get('_parentId'); + Origin.trigger('editorView:addComponent:' + parentId); + $('html').css('overflow-y', ''); + $.scrollTo('.block[data-id=' + parentId + ']'); + }, this), error: function() { $('html').css('overflow-y', ''); Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingcomponent') }); - }, - success: _.bind(function() { - Origin.editor.data.components.add(newComponentModel); - this.parentView.evaluateComponents(this.parentView.toggleAddComponentsButton); - // Re-render the block - this.parentView.reRender(); - newComponentView.addClass('synced'); - $('html').css('overflow-y', ''); - $.scrollTo(newComponentView.$el); - }, this) + } }); } }, { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js index 669ea1dd9f..89440642ac 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js @@ -43,9 +43,6 @@ define(function(require){ url:'/api/content/component/' + componentId, data: newData, success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(newData); - // Re-render the move-from block Origin.trigger('editorView:moveComponent:' + blockId); if (blockId !== parentId) { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js index 103ef4883c..3a501c2b57 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js @@ -1,7 +1,5 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var EditorOriginView = require('../../global/views/editorOriginView'); @@ -9,6 +7,10 @@ define(function(require){ className: 'component editable component-draggable', tagName: 'div', + settings: _.extend({}, EditorOriginView.prototype.settings, { + autoRender: false, + }), + events: _.extend({}, EditorOriginView.prototype.events, { 'click a.component-delete': 'deleteComponentPrompt', 'click a.component-move': 'evaluateMove', @@ -18,23 +20,24 @@ define(function(require){ preRender: function() { this.$el.addClass('component-' + this.model.get('_layout')); - this.listenTo(Origin, 'editorView:removeSubViews', this.remove); - this.listenTo(Origin, 'editorPageView:removePageSubViews', this.remove); - - this.evaluateLayout(); - - this.on('contextMenu:component:edit', this.loadComponentEdit); - this.on('contextMenu:component:copy', this.onCopy); - this.on('contextMenu:component:copyID', this.onCopyID); - this.on('contextMenu:component:cut', this.onCut); - this.on('contextMenu:component:delete', this.deleteComponentPrompt); + this.listenTo(Origin, 'editorView:removeSubViews editorPageView:removePageSubViews', this.remove); + this.on({ + 'contextMenu:component:edit': this.loadComponentEdit, + 'contextMenu:component:copy': this.onCopy, + 'contextMenu:component:copyID': this.onCopyID, + 'contextMenu:component:delete': this.deleteComponentPrompt + }); + this.evaluateLayout(_.bind(function(layouts) { + this.model.set('_movePositions', layouts); + this.render(); + }, this)); }, postRender: function () { this.setupDragDrop(); _.defer(_.bind(function(){ this.trigger('componentView:postRender'); - Origin.trigger('pageView:itemRendered'); + Origin.trigger('pageView:itemRendered', this); }, this)); }, @@ -45,23 +48,22 @@ define(function(require){ type: 'warning', title: Origin.l10n.t('app.deletecomponent'), text: Origin.l10n.t('app.confirmdeletecomponent') + '
    ' + '
    ' + Origin.l10n.t('app.confirmdeletecomponentwarning'), - callback: _.bind(this.deleteComponentConfirm, this) + callback: _.bind(function(confirmed) { + if(confirmed) this.deleteComponent(); + }, this) }); }, - deleteComponentConfirm: function(confirmed) { - if(confirmed) { - this.deleteComponent(); - } - }, - deleteComponent: function() { - var parentId = this.model.get('_parentId'); - - if (this.model.destroy()) { - this.remove(); - Origin.trigger('editorView:removeComponent:' + parentId); - } + this.model.destroy({ + success: _.bind(function(model) { + this.remove(); + Origin.trigger('editorView:removeComponent:' + model.get('_parentId')); + }, this), + error: function(response) { + console.error(response); + } + }) }, loadComponentEdit: function(event) { @@ -137,11 +139,10 @@ define(function(require){ }, getSupportedLayout: function() { - var componentType = _.find(Origin.editor.data.componenttypes.models, function(type){ - return type.get('component') === this.model.get('_component'); - }, this); - + var componentType = Origin.editor.data.componenttypes.findWhere({ component: this.model.get('_component') }); var supportedLayout = componentType.get('properties')._supportedLayout; + // allow all layouts by default + if(!supportedLayout) return { full: true, half: true }; return { full: _.indexOf(supportedLayout.enum, 'full-width') > -1, @@ -149,114 +150,58 @@ define(function(require){ } }, - evaluateLayout: function() { + evaluateLayout: function(cb) { var supportedLayout = this.getSupportedLayout(); - var isFullWidthSupported = supportedLayout.full; - var isHalfWidthSupported = supportedLayout.half; - var movePositions = { left: false, right: false, full: false }; - - if (isHalfWidthSupported) { - var siblings = this.model.getSiblings(); - var showFull = !siblings.length && isFullWidthSupported; - var type = this.model.get('_layout'); - - switch (type) { + this.model.fetchSiblings(_.bind(function(siblings) { + var showFull = supportedLayout.full && siblings.length < 1; + switch(this.model.get('_layout')) { case 'left': - movePositions.right = true; + movePositions.right = supportedLayout.half; movePositions.full = showFull; break; case 'right': - movePositions.left = true; + movePositions.left = supportedLayout.half; movePositions.full = showFull; break; case 'full': - movePositions.left = true; - movePositions.right = true; + movePositions.left = supportedLayout.half; + movePositions.right = supportedLayout.half; break } - } - - this.model.set('_movePositions', movePositions); - + cb(movePositions); + }, this)); }, evaluateMove: function(event) { event && event.preventDefault(); - var left = $(event.currentTarget).hasClass('component-move-left'); - var right = $(event.currentTarget).hasClass('component-move-right'); - var newComponentLayout = (!left && !right) ? 'full' : (left ? 'left' : 'right'); - var siblings = this.model.getSiblings(); - - if (siblings && siblings.length > 0) { - var siblingId = siblings.models[0].get('_id'); - } - - if (siblingId) { - this.moveSiblings(newComponentLayout, siblingId); - } else { - this.moveComponent(newComponentLayout); - } + var $btn = $(event.currentTarget); + this.model.fetchSiblings(_.bind(function(siblings) { + var isLeft = $btn.hasClass('component-move-left'); + var isRight = $btn.hasClass('component-move-right'); + var isFull = $btn.hasClass('component-move-full'); + // move self to layout of clicked button + this.moveComponent(this.model.get('_id'), (isLeft ? 'left' : isRight ? 'right' : 'full')); + // move sibling to inverse of self + var siblingId = siblings && siblings.length > 0 && siblings.models[0].get('_id'); + if (siblingId) this.moveComponent(siblingId, (isLeft ? 'right' : 'left')); + }, this)); }, - moveComponent: function (layout) { - var componentId = this.model.get('_id'); + moveComponent: function (id, layout) { var parentId = this.model.get('_parentId'); - var layoutData = { - _layout: layout, - _parentId: parentId - }; - $.ajax({ type: 'PUT', - url:'/api/content/component/' + componentId, - data: layoutData, - success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(layoutData); - - // Re-render the block - Origin.trigger('editorView:moveComponent:' + parentId); - }, - error: function(jqXHR, textStatus, errorThrown) { - Origin.Notify.alert({ - type: 'error', - text: jqXHR.responseJSON.message - }); - } - }); - }, - - moveSiblings: function (layout, siblingId) { - var componentId = this.model.get('_id'); - var parentId = this.model.get('_parentId'); - var newSiblingLayout = (layout == 'left') ? 'right' : 'left'; - var layoutData = { - newLayout: { + url:'/api/content/component/' + id, + data: { _layout: layout, _parentId: parentId }, - siblingLayout: { - _layout: newSiblingLayout, - _parentId: parentId - } - }; - $.ajax({ - type: 'PUT', - url:'/api/content/component/switch/' + componentId +'/'+ siblingId, - data: layoutData, success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(layoutData.newLayout); - - var siblingModel = Origin.editor.data.components.get(siblingId); - siblingModel.set(layoutData.siblingLayout); - - // Re-render the block Origin.trigger('editorView:moveComponent:' + parentId); }, error: function(jqXHR, textStatus, errorThrown) { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index d247d76588..33ebd6a0fe 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -1,11 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var ArticleModel = require('core/models/articleModel'); - var ContentModel = require('core/models/contentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageArticleView = require('./editorPageArticleView'); var EditorPasteZoneView = require('../../global/views/editorPasteZoneView'); @@ -13,69 +10,58 @@ define(function(require){ var EditorPageView = EditorOriginView.extend({ className: 'page', tagName: 'div', - childrenCount: 0, childrenRenderedCount: 0, events: _.extend({}, EditorOriginView.prototype.events, { - 'click a.add-article': 'addArticle', + 'click a.add-article': 'addNewArticle', 'click a.page-edit-button': 'openContextMenu', 'dblclick .page-detail': 'loadPageEdit', 'click .paste-cancel': 'onPasteCancel' }), preRender: function() { - this.setupChildCount(); - - this.listenTo(Origin, { + Origin.editor.blockCount = 0; + var id = this.model.get('_id'); + var originEvents = { 'editorView:removeSubViews': this.remove, - 'pageView:itemRendered': this.evaluateChildStatus - }); - this.listenTo(Origin, 'editorView:moveArticle:' + this.model.get('_id'), this.render); - this.listenTo(Origin, 'editorView:cutArticle:' + this.model.get('_id'), this.onCutArticle); - }, + 'pageView:itemAnimated': this.evaluateChildStatus + }; + originEvents['editorView:moveArticle:' + id] = this.render; + originEvents['editorView:pasted:' + id] = this.onPaste; + this.listenTo(Origin, originEvents); - resize: function() { - _.defer(_.bind(function() { - var windowHeight = $(window).height(); - this.$el.height(windowHeight - this.$el.offset().top); - }, this)); + this._onScroll = _.bind(_.throttle(this.onScroll, 400), this); }, - setupChildCount: function() { - var articles = Origin.editor.data.articles.where({_parentId: this.model.get('_id')}); - var articleList = [], blockList = []; - - _.each(articles, function(article) { - articleList.push(article.get('_id')); - }); + render: function() { + var returnVal = EditorOriginView.prototype.render.apply(this, arguments); - var blocks = _.filter(Origin.editor.data.blocks.models, function (block) { - return _.contains(articleList, block.get('_parentId')); - }); + this.addArticleViews(); - _.each(blocks, function(block) { - blockList.push(block.get('_id')); - }); + return returnVal; + }, - var components = _.filter(Origin.editor.data.components.models, function(component) { - return _.contains(blockList, component.get('_parentId')); - }); + postRender: function() { + this.resize(); + }, - this.childrenCount = articles.length + blocks.length + components.length; + resize: function() { + _.defer(_.bind(function() { + var windowHeight = $(window).height(); + this.$el.height(windowHeight - this.$el.offset().top); + }, this)); }, evaluateChildStatus: function() { this.childrenRenderedCount++; + + if (this.childrenRenderedCount < Origin.editor.blockCount) return; + this.allChildrenRendered(); }, postRender: function() { - this.addArticleViews(); - - _.defer(_.bind(function(){ - this.resize(); - this.trigger('pageView:postRender'); - this.setViewToReady(); - }, this)); + this.setupScrollListener(); + this.resize(); }, addArticleViews: function() { @@ -89,19 +75,30 @@ define(function(require){ }); this.$('.page-articles').append(new EditorPasteZoneView({ model: prePasteArticle }).$el); // Iterate over each article and add it to the page - this.model.getChildren().each(this.addArticleView, this); + this.model.fetchChildren(_.bind(function(children) { + for(var i = 0, count = children.length; i < count; i++) { + if(children[i].get('_type') !== 'article') { + continue; + } + this.addArticleView(children[i]); + } + }, this)); }, - addArticleView: function(articleModel, scrollIntoView, addNewBlock) { + addArticleView: function(articleModel, scrollIntoView) { + scrollIntoView = scrollIntoView || false; + var newArticleView = new EditorPageArticleView({ model: articleModel }); var sortOrder = articleModel.get('_sortOrder'); - // Add syncing class - if (articleModel.isNew()) { - newArticleView.$el.addClass('syncing'); + var $articles = this.$('.page-articles .article'); + var index = sortOrder > 0 ? sortOrder-1 : undefined; + var shouldAppend = index === undefined || index >= $articles.length || $articles.length === 0; + + if(shouldAppend) { // add to the end of the article + this.$('.page-articles').append(newArticleView.$el); + } else { // 'splice' block into the new position + $($articles[index]).before(newArticleView.$el); } - scrollIntoView = scrollIntoView || false; - - this.$('.page-articles').append(newArticleView.$el); if (scrollIntoView) { $.scrollTo(newArticleView.$el, 200); @@ -109,37 +106,30 @@ define(function(require){ // Increment the 'sortOrder' property articleModel.set('_pasteZoneSortOrder', sortOrder++); // Post-article paste zone - sort order of placeholder will be one greater - this.$('.page-articles').append(new EditorPasteZoneView({model: articleModel}).$el); - // Return the article view so syncing can be shown + this.$('.page-articles').append(new EditorPasteZoneView({ model: articleModel }).$el); return newArticleView; }, - addArticle: function(event) { + addNewArticle: function(event) { event && event.preventDefault(); - - var _this = this; - var newPageArticleModel = new ArticleModel({ + (new ArticleModel()).save({ title: Origin.l10n.t('app.placeholdernewarticle'), displayTitle: Origin.l10n.t('app.placeholdernewarticle'), body: '', - _parentId: _this.model.get('_id'), + _parentId: this.model.get('_id'), _courseId: Origin.editor.data.course.get('_id'), _type:'article' - }); - - var newArticleView = _this.addArticleView(newPageArticleModel); - - newPageArticleModel.save(null, { + }, { + success: _.bind(function(model, response, options) { + var articleView = this.addArticleView(model); + articleView._skipRender = true; // prevent render of blocks in postRender + articleView.addBlock(); + }, this), error: function() { Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingarticle') }); - }, - success: function(model, response, options) { - Origin.editor.data.articles.add(model); - newArticleView.$el.removeClass('syncing').addClass('synced'); - newArticleView.addBlock(); } }); }, @@ -154,7 +144,8 @@ define(function(require){ // TODO fragile HACK, refactor context menu code to allow what I want to do later... openContextMenu: function(event) { if(!event) return console.log('Error: needs a current target to attach the menu to...'); - event.preventDefault() && event.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); var fakeModel = new Backbone.Model({ _id: this.model.get('_id'), _type: 'page-min' }); var fakeView = new Backbone.View({ model: fakeModel }); @@ -166,9 +157,53 @@ define(function(require){ Origin.trigger('contextMenu:open', fakeView, event); }, + onPaste: function(data) { + (new ArticleModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.addArticleView(model); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); + }, + onCutArticle: function(view) { this.once('pageView:postRender', view.showPasteZones); this.render(); + }, + + setupScrollListener: function() { + $('.contentPane').on('scroll', this._onScroll); + }, + + onScroll: function(event) { + var scrollPos = event.currentTarget.scrollTop; + Origin.editor.scrollTo = scrollPos; + }, + + removeScrollListener: function() { + $('.contentPane').off('scroll', this._onScroll); + }, + + allChildrenRendered: function() { + if (Origin.editor.scrollTo > 0) { + this.removeScrollListener(); + } + $('.contentPane').scrollTo(Origin.editor.scrollTo, { + duration: 200, + onAfter: _.bind(function() { + this.setupScrollListener(); + }, this) + }); + }, + + remove: function() { + this.removeScrollListener(); + EditorOriginView.prototype.remove.apply(this, arguments); } }, { template: 'editorPage' diff --git a/frontend/src/modules/editor/course/index.js b/frontend/src/modules/editor/course/index.js index e83036480a..e20af3e71a 100644 --- a/frontend/src/modules/editor/course/index.js +++ b/frontend/src/modules/editor/course/index.js @@ -5,7 +5,8 @@ define(function(require) { var CourseModel = require('core/models/courseModel'); var EditorCourseEditView = require('./views/editorCourseEditView'); var EditorCourseEditSidebarView = require('./views/editorCourseEditSidebarView'); - var Helpers = require('../global/helpers'); + var CoreHelpers = require('core/helpers'); + var EditorHelpers = require('../global/helpers'); Origin.on('router:project', function(route1, route2, route3, route4) { if(route1 === 'new') createNewCourse(); @@ -13,15 +14,15 @@ define(function(require) { Origin.on('editor:course', renderCourseEdit); function renderCourseEdit() { - (new CourseModel({ _id: Origin.location.route1 })).fetch({ - success: function(model) { - // for the location module - model.title = Origin.l10n.t('app.editorsettings'); - Helpers.setPageTitle(model); - var form = Origin.scaffold.buildForm({ model: model }); - Origin.contentPane.setView(EditorCourseEditView, { model: model, form: form }); - Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); - } + var courseModel = new CourseModel({ _id: Origin.location.route1 }); + // FIXME need to fetch config to ensure scaffold has the latest extensions data + CoreHelpers.multiModelFetch([ courseModel, Origin.editor.data.config ], function(data) { + // for the location module + model.title = Origin.l10n.t('app.editorsettings'); + EditorHelpers.setPageTitle(model); + var form = Origin.scaffold.buildForm({ model: courseModel }); + Origin.contentPane.setView(EditorCourseEditView, { model: courseModel, form: form }); + Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); }); } diff --git a/frontend/src/modules/editor/course/views/editorCourseEditView.js b/frontend/src/modules/editor/course/views/editorCourseEditView.js index d215aef577..172e6c01bf 100644 --- a/frontend/src/modules/editor/course/views/editorCourseEditView.js +++ b/frontend/src/modules/editor/course/views/editorCourseEditView.js @@ -1,8 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { var Origin = require('core/origin'); - - var ConfigModel = require('core/models/configModel'); var ContentObjectModel = require('core/models/contentObjectModel'); var ArticleModel = require('core/models/articleModel'); var BlockModel = require('core/models/blockModel'); @@ -23,44 +21,35 @@ define(function(require) { this.$el.addClass('project-detail-hide-hero'); // Initialise the 'tags' property for a new course this.model.set('tags', []); - } else { - // Ensure that the latest config model is always up-to-date when entering this screen - Origin.editor.data.config = new ConfigModel({_courseId: this.model.get('_id')}); } // This next line is important for a proper PATCH request on saveProject() this.originalAttributes = _.clone(this.model.attributes); }, getAttributesToSave: function() { - // set tags - var tags = []; - _.each(this.model.get('tags'), function(item) { - item._id && tags.push(item._id); - }); - this.model.set('tags', tags); + this.model.set('tags', _.pluck(this.model.get('tags'), '_id')); var changedAttributes = this.model.changedAttributes(this.originalAttributes); if(changedAttributes) { return _.pick(this.model.attributes, _.keys(changedAttributes)); } - return null; }, onSaveSuccess: function(model, response, options) { - if (this.isNew) { - this.populateNewCourse(model); - } else { + if(!this.isNew) { EditorOriginView.prototype.onSaveSuccess.apply(this, arguments); + return; } + this.populateNewCourse(model); }, - // TODO not really good enough to handle model save errors and other errors here + // FIXME not really good enough to handle model save errors and other errors here onSaveError: function(model, response, options) { - if(arguments.length == 2) { - return EditorOriginView.prototype.onSaveError.apply(this, arguments); + if(arguments.length === 2) { + EditorOriginView.prototype.onSaveError.apply(this, arguments); + return; } - var messageText = typeof response.responseJSON == 'object' && response.responseJSON.message; EditorOriginView.prototype.onSaveError.call(this, null, messageText); }, @@ -145,7 +134,6 @@ define(function(require) { }, this) }); } - }, { template: 'editorCourseEdit' }); diff --git a/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js b/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js index 5730ab0965..db28df58d3 100644 --- a/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js +++ b/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js @@ -2,9 +2,9 @@ define(function(require) { var Backbone = require('backbone'); var Origin = require('core/origin'); - - var ConfigModel = require('core/models/configModel'); var EditorOriginView = require('../../global/views/editorOriginView'); + var ExtensionModel = require('core/models/extensionModel'); + var EditorCollection = require('../../global/collections/editorCollection'); var EditorExtensionsEditView = EditorOriginView.extend({ className: "extension-management", @@ -20,6 +20,7 @@ define(function(require) { }, preRender: function() { + var _this = this; this.currentSelectedIds = []; this.listenTo(Origin, { @@ -29,29 +30,62 @@ define(function(require) { // assumption: extensions are always switched between enabled and available this.listenTo(this.model, 'change:enabledExtensions', this.render); - this.setupExtensions(); - - // TODO is defer a good idea? - _.defer(_.bind(this.postRender, this)); + this.setupExtensions(function() { + _this.postRender() + }); }, - setupExtensions: function() { + setupExtensions: function(callback) { var enabledExtensionNames = _.pluck(Origin.editor.data.config.get('_enabledExtensions'), 'name'); var enabledExtensions = []; var disabledExtensions = []; + var _this = this; - Origin.editor.data.extensiontypes.each(function(model) { - var extension = model.toJSON(); - if(_.indexOf(enabledExtensionNames, extension.name) > -1) { - enabledExtensions.push(extension); - } else if(extension._isAvailableInEditor) { - disabledExtensions.push(extension); - } - }); - this.model.set({ - enabledExtensions: enabledExtensions, - availableExtensions: disabledExtensions + var extensionTypes = new EditorCollection(null, { + autoFetch: false, + model: ExtensionModel, + url: ExtensionModel.prototype.urlRoot, + _type: 'extension' }); + + extensionTypes.fetch({ + success: function() { + extensionTypes.each(function(model) { + var extension = model.toJSON(); + if (_.indexOf(enabledExtensionNames, extension.name) > -1) { + enabledExtensions.push(extension); + } else if (extension._isAvailableInEditor) { + disabledExtensions.push(extension); + } + }); + + enabledExtensions.sort(function(a, b){ + if(a.displayName < b.displayName) return -1; + if(a.displayName > b.displayName) return 1; + return 0; + }); + + disabledExtensions.sort(function(a, b){ + if(a.displayName < b.displayName) return -1; + if(a.displayName > b.displayName) return 1; + return 0; + }); + + _this.model.set({ + enabledExtensions: enabledExtensions, + availableExtensions: disabledExtensions + }); + + if(callback){ + return callback(); + } + }, + error: function(err) { + if(callback){ + return callback(err); + } + } + }) }, postRender: function() { diff --git a/frontend/src/modules/editor/global/collections/editorCollection.js b/frontend/src/modules/editor/global/collections/editorCollection.js index f7fcb4df3c..6b777eff58 100644 --- a/frontend/src/modules/editor/global/collections/editorCollection.js +++ b/frontend/src/modules/editor/global/collections/editorCollection.js @@ -8,7 +8,9 @@ define(function(require) { this.url = options.url; this._type = options._type; this.on('reset', this.loadedData, this); - this.fetch({ reset:true }); + if(options.autoFetch !== false){ + this.fetch({ reset:true }); + } }, loadedData: function() { diff --git a/frontend/src/modules/editor/global/editorDataLoader.js b/frontend/src/modules/editor/global/editorDataLoader.js index 9a9635d032..19207796e9 100644 --- a/frontend/src/modules/editor/global/editorDataLoader.js +++ b/frontend/src/modules/editor/global/editorDataLoader.js @@ -3,12 +3,8 @@ define(function(require) { var _ = require('underscore'); var Origin = require('core/origin'); - var ArticleModel = require('core/models/articleModel'); - var BlockModel = require('core/models/blockModel'); var ClipboardModel = require('core/models/clipboardModel'); - var ComponentModel = require('core/models/componentModel'); var ComponentTypeModel = require('core/models/componentTypeModel'); - var ContentObjectModel = require('core/models/contentObjectModel'); var ConfigModel = require('core/models/configModel'); var CourseAssetModel = require('core/models/courseAssetModel'); var CourseModel = require('core/models/courseModel'); @@ -20,20 +16,13 @@ define(function(require) { // used to check what's preloaded var globalData = { - courses: false, - extensiontypes: false, - componenttypes: false + componenttypes: false, + extensiontypes: false }; // used to check what's loaded var courseData = { - clipboards: false, course: false, - config: false, - contentObjects: false, - articles: false, - blocks: false, - components: false, - courseassets: false + config: false }; // Public API @@ -49,9 +38,6 @@ define(function(require) { ensureEditorData(); resetLoadStatus(globalData); // create the global collections - if(!Origin.editor.data.courses) { - Origin.editor.data.courses = createCollection(CourseModel); - } if(!Origin.editor.data.extensiontypes) { Origin.editor.data.extensiontypes = createCollection(ExtensionModel); } @@ -90,13 +76,7 @@ define(function(require) { if(!isAlreadyLoaded) { _.extend(Origin.editor.data, { course: new CourseModel({ _id: courseId }), - config: new ConfigModel({ _courseId: courseId }), - contentObjects: createCollection(ContentObjectModel), - articles: createCollection(ArticleModel), - blocks: createCollection(BlockModel), - components: createCollection(ComponentModel), - clipboards: createCollection(ClipboardModel, '&createdBy=' + Origin.sessionModel.get('id')), - courseassets: createCollection(CourseAssetModel) + config: new ConfigModel({ _courseId: courseId }) }); } // fetch all collections @@ -199,14 +179,14 @@ define(function(require) { function createCollection(Model, query) { var courseId = Origin.location.route1; var url = Model.prototype.urlRoot; - var siblings = Model.prototype._siblings; + var siblingTypes = Model.prototype._siblingTypes; /** * FIXME for non course-specific data without a model._type. * Adding siblings will break the below check... */ var inferredType = url.split('/').slice(-1) + 's'; // FIXME not the best check for course-specific collections - if(siblings !== undefined) { + if(siblingTypes !== undefined) { if(!courseId) throw new Error('No Editor.data.course specified, cannot load ' + url); url += '?_courseId=' + courseId + (query || ''); } @@ -214,7 +194,7 @@ define(function(require) { autoFetch: false, model: Model, url: url, - _type: siblings || inferredType + _type: siblingTypes || inferredType }); } diff --git a/frontend/src/modules/editor/global/less/editor.less b/frontend/src/modules/editor/global/less/editor.less index f3f2872496..93670892dc 100644 --- a/frontend/src/modules/editor/global/less/editor.less +++ b/frontend/src/modules/editor/global/less/editor.less @@ -149,17 +149,27 @@ i.asset-clear { font-weight: @font-weight-bold; } +.module-editor .block { + transition: transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1) !important; +} +.page-content-syncing { + &.block { + min-height: 150px; + transform: scale(0.95, 0.95); + opacity: 0.6; + .component { + &-inner { + display: none; + } + } + } +} + .syncing { transform: scale(0.8, 0.8); opacity:0.6; } -.synced { - transition:transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1)!important; - transform: scale(1, 1); - opacity:1; -} - .not-synced { transition:transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1)!important; transform: scale(0.4, 0.4); diff --git a/frontend/src/modules/editor/global/views/editorOriginView.js b/frontend/src/modules/editor/global/views/editorOriginView.js index 19ecbc31fd..2c7a6e965c 100644 --- a/frontend/src/modules/editor/global/views/editorOriginView.js +++ b/frontend/src/modules/editor/global/views/editorOriginView.js @@ -24,6 +24,14 @@ define(function(require){ }); }, + render: function() { + OriginView.prototype.render.apply(this, arguments); + if(this.model) { + this.$el.attr('data-id', this.model.get('_id')); + } + return this; + }, + postRender: function() { if (!this.form) { return this.setViewToReady(); @@ -186,11 +194,6 @@ define(function(require){ Origin.trigger('editorView:copyID', this.model); }, - onCut: function(e) { - e && e.preventDefault(); - Origin.trigger('editorView:cut', this); - }, - onPaste: function(e) { if(e) { e.stopPropagation(); diff --git a/frontend/src/modules/editor/global/views/editorPasteZoneView.js b/frontend/src/modules/editor/global/views/editorPasteZoneView.js index c74417f002..2ad025997c 100644 --- a/frontend/src/modules/editor/global/views/editorPasteZoneView.js +++ b/frontend/src/modules/editor/global/views/editorPasteZoneView.js @@ -55,18 +55,12 @@ define(function(require){ _parentId: parentId, _sortOrder: $('.paste-' + type, this.$el).attr('data-sort-order') }, - success: _.bind(function() { - // fetch collection for the pasted type, and send motification - Origin.editor.data[this.model._siblings].fetch().done(function() { - var eventPrefix = 'editorView:move' + Helpers.capitalise(type) + ':'; - var itemId = (droppedOnId === parentId) ? droppedOnId : parentId; - // notify the old parent that the child's gone - if(itemId !== droppedOnId) { - Origin.trigger(eventPrefix + droppedOnId); - } - Origin.trigger(eventPrefix + itemId); - }); - }, this), + success: function() { + var eventPrefix = 'editorView:move' + Helpers.capitalise(type) + ':'; + Origin.trigger(eventPrefix + droppedOnId); + // notify the old parent that the child's gone + if(droppedOnId !== parentId) Origin.trigger(eventPrefix + parentId); + }, error: function(jqXHR) { Origin.Notify.alert({ type: 'error', text: jqXHR.responseJSON.message }); } diff --git a/frontend/src/modules/editor/global/views/editorView.js b/frontend/src/modules/editor/global/views/editorView.js index 9af67089cc..ba903c2582 100644 --- a/frontend/src/modules/editor/global/views/editorView.js +++ b/frontend/src/modules/editor/global/views/editorView.js @@ -4,7 +4,6 @@ */ define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var helpers = require('core/helpers'); @@ -35,7 +34,6 @@ define(function(require){ preRender: function(options) { this.currentView = options.currentView; - Origin.editor.pasteParentModel = false; Origin.editor.isPreviewPending = false; this.currentCourseId = Origin.editor.data.course.get('_id'); this.currentCourse = Origin.editor.data.course; @@ -45,13 +43,23 @@ define(function(require){ 'editorView:refreshView': this.setupEditor, 'editorView:copy': this.addToClipboard, 'editorView:copyID': this.copyIdToClipboard, - 'editorView:cut': this.cutContent, 'editorView:paste': this.pasteFromClipboard, - 'editorCommon:download': this.downloadProject, - 'editorCommon:preview': this.previewProject, - 'editorCommon:export': this.exportProject + 'editorCommon:download': function(event) { + this.validateProject(event, this.downloadProject); + }, + 'editorCommon:preview': function(event) { + var previewWindow = window.open('/loading', 'preview'); + this.validateProject(event, function(error) { + if(error) { + return previewWindow.close(); + } + this.previewProject(previewWindow); + }); + }, + 'editorCommon:export': function(event) { + this.validateProject(event, this.exportProject); + } }); - this.render(); this.setupEditor(); }, @@ -64,157 +72,112 @@ define(function(require){ this.renderCurrentEditorView(); }, - downloadProject: function(e) { + validateProject: function(e, next) { e && e.preventDefault(); + helpers.validateCourseContent(this.currentCourse, _.bind(function(error) { + if(error) { + Origin.Notify.alert({ type: 'error', text: "There's something wrong with your course:

    " + error }); + } + next.call(this, error); + }, this)); + }, - var self = this; - - if (helpers.validateCourseContent(this.currentCourse) && !Origin.editor.isDownloadPending) { - $('.editor-common-sidebar-download-inner').addClass('display-none'); - $('.editor-common-sidebar-downloading').removeClass('display-none'); - - var courseId = Origin.editor.data.course.get('_id'); - var tenantId = Origin.sessionModel.get('tenantId'); - - $.ajax({ - method: 'get', - url: '/api/output/' + Origin.constants.outputPlugin + '/publish/' + this.currentCourseId, - success: function (jqXHR, textStatus, errorThrown) { - if (jqXHR.success) { - if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) != 'undefined' && jqXHR.payload.pollUrl != '') { - // Ping the remote URL to check if the job has been completed - self.updateDownloadProgress(jqXHR.payload.pollUrl); - } else { - self.resetDownloadProgress(); - - var $downloadForm = $('#downloadForm'); - - $downloadForm.attr('action', '/download/' + tenantId + '/' + courseId + '/' + jqXHR.payload.zipName + '/download.zip'); - $downloadForm.submit(); - } - } else { - self.resetDownloadProgress(); - - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - self.resetDownloadProgress(); - - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - } - }); - } else { - return false; + previewProject: function(previewWindow) { + if(Origin.editor.isPreviewPending) { + return; } + Origin.editor.isPreviewPending = true; + $('.navigation-loading-indicator').removeClass('display-none'); + $('.editor-common-sidebar-preview-inner').addClass('display-none'); + $('.editor-common-sidebar-previewing').removeClass('display-none'); + + $.get('/api/output/' + Origin.constants.outputPlugin + '/preview/' + this.currentCourseId, _.bind(function(jqXHR, textStatus, errorThrown) { + if(!jqXHR.success) { + this.resetPreviewProgress(); + Origin.Notify.alert({ + type: 'error', + text: Origin.l10n.t('app.errorgeneratingpreview') + }); + previewWindow.close(); + return; + } + if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) !== undefined && jqXHR.payload.pollUrl) { + // Ping the remote URL to check if the job has been completed + this.updatePreviewProgress(jqXHR.payload.pollUrl, previewWindow); + return; + } + this.updateCoursePreview(previewWindow); + this.resetPreviewProgress(); + }, this)).fail(_.bind(function(jqXHR, textStatus, errorThrown) { + this.resetPreviewProgress(); + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + previewWindow.close(); + }, this)); }, - exportProject: function(e) { - e && e.preventDefault(); + downloadProject: function() { + if(Origin.editor.isDownloadPending) { + return; + } + $('.editor-common-sidebar-download-inner').addClass('display-none'); + $('.editor-common-sidebar-downloading').removeClass('display-none'); - // aleady processing, don't try again - if(this.exporting) return; + $.get('/api/output/' + Origin.constants.outputPlugin + '/publish/' + this.currentCourseId, _.bind(function(jqXHR, textStatus, errorThrown) { - var courseId = Origin.editor.data.course.get('_id'); - var tenantId = Origin.sessionModel.get('tenantId'); - - this.showExportAnimation(); - this.exporting = true; + if (!jqXHR.success) { + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + this.resetDownloadProgress(); + return; + } + if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) !== undefined && jqXHR.payload.pollUrl) { + // Ping the remote URL to check if the job has been completed + this.updateDownloadProgress(jqXHR.payload.pollUrl); + return; + } + this.resetDownloadProgress(); + + var $downloadForm = $('#downloadForm'); + $downloadForm.attr('action', '/download/' + Origin.sessionModel.get('tenantId') + '/' + Origin.editor.data.course.get('_id') + '/' + jqXHR.payload.zipName + '/download.zip'); + $downloadForm.submit(); - var self = this; - $.ajax({ - url: '/export/' + tenantId + '/' + courseId, - success: function(data, textStatus, jqXHR) { - self.showExportAnimation(false); - self.exporting = false; - - // get the zip - var form = document.createElement('form'); - self.$el.append(form); - form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip'); - form.submit(); - }, - error: function(jqXHR, textStatus, errorThrown) { - var messageText = errorThrown; - if(jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.message) messageText += ':
    ' + jqXHR.responseJSON.message; - - self.showExportAnimation(false); - self.exporting = false; - - Origin.Notify.alert({ - type: 'error', - title: Origin.l10n.t('app.exporterrortitle'), - text: messageText - }); - } - }); + }, this)).fail(_.bind(function (jqXHR, textStatus, errorThrown) { + this.resetDownloadProgress(); + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + }, this)); }, - showExportAnimation: function(show) { - if(show !== false) { - $('.editor-common-sidebar-export-inner').addClass('display-none'); - $('.editor-common-sidebar-exporting').removeClass('display-none'); - } else { - $('.editor-common-sidebar-export-inner').removeClass('display-none'); - $('.editor-common-sidebar-exporting').addClass('display-none'); + exportProject: function() { + if(this.exporting) { + return; } - }, + this.showExportAnimation(); + this.exporting = true; - updateCoursePreview: function(previewWindow) { var courseId = Origin.editor.data.course.get('_id'); var tenantId = Origin.sessionModel.get('tenantId'); - previewWindow.location.href = '/preview/' + tenantId + '/' + courseId + '/'; - }, - - previewProject: function(e) { - e && e.preventDefault(); - - var self = this; - if (helpers.validateCourseContent(this.currentCourse) && !Origin.editor.isPreviewPending) { - var previewWindow = window.open('/loading', 'preview'); - Origin.editor.isPreviewPending = true; - $('.navigation-loading-indicator').removeClass('display-none'); - $('.editor-common-sidebar-preview-inner').addClass('display-none'); - $('.editor-common-sidebar-previewing').removeClass('display-none'); - - $.ajax({ - method: 'get', - url: '/api/output/' + Origin.constants.outputPlugin + '/preview/' + this.currentCourseId, - success: function (jqXHR, textStatus, errorThrown) { - if (jqXHR.success) { - if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) != 'undefined' && jqXHR.payload.pollUrl != '') { - // Ping the remote URL to check if the job has been completed - self.updatePreviewProgress(jqXHR.payload.pollUrl, previewWindow); - } else { - self.updateCoursePreview(previewWindow); - self.resetPreviewProgress(); - } - } else { - self.resetPreviewProgress(); - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneratingpreview') - }); - previewWindow.close(); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - self.resetPreviewProgress(); - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - previewWindow.close(); - } + $.get('/export/' + tenantId + '/' + courseId, _.bind(function(data, textStatus, jqXHR) { + // success + var form = document.createElement('form'); + this.$el.append(form); + form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/download.zip'); + form.submit(); + }, this)).fail(_.bind(function(jqXHR, textStatus, errorThrown) { + // failure + var messageText = errorThrown; + if(jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.message) { + messageText += ':
    ' + jqXHR.responseJSON.message; + } + Origin.Notify.alert({ + type: 'error', + title: Origin.l10n.t('app.exporterrortitle'), + text: messageText }); - } + }, this)).always(_.bind(function() { + // always + this.showExportAnimation(false); + this.exporting = false; + }, this)); }, updatePreviewProgress: function(url, previewWindow) { @@ -269,17 +232,30 @@ define(function(require){ Origin.editor.isDownloadPending = false; }, - addToClipboard: function(model) { - _.defer(_.bind(function() { _.invoke(Origin.editor.data.clipboards.models, 'destroy') }, this)); + showExportAnimation: function(show) { + if(show !== false) { + $('.editor-common-sidebar-export-inner').addClass('display-none'); + $('.editor-common-sidebar-exporting').removeClass('display-none'); + } else { + $('.editor-common-sidebar-export-inner').removeClass('display-none'); + $('.editor-common-sidebar-exporting').addClass('display-none'); + } + }, + updateCoursePreview: function(previewWindow) { + var courseId = Origin.editor.data.course.get('_id'); + var tenantId = Origin.sessionModel.get('tenantId'); + previewWindow.location.href = '/preview/' + tenantId + '/' + courseId + '/'; + }, + + addToClipboard: function(model) { var postData = { objectId: model.get('_id'), courseId: Origin.editor.data.course.get('_id'), - referenceType: model._siblings + referenceType: model._siblingTypes }; $.post('/api/content/clipboard/copy', postData, _.bind(function(jqXHR) { Origin.editor.clipboardId = jqXHR.clipboardId; - Origin.editor.pasteParentModel = model.getParent(); this.showPasteZones(model.get('_type')); }, this)).fail(_.bind(function (jqXHR, textStatus, errorThrown) { Origin.Notify.alert({ @@ -307,23 +283,20 @@ define(function(require){ }, pasteFromClipboard: function(parentId, sortOrder, layout) { - var data = { + Origin.trigger('editorView:pasteCancel'); + var postData = { id: Origin.editor.clipboardId, parentId: parentId, layout: layout, sortOrder: sortOrder, courseId: Origin.editor.data.course.get('_id') }; - $.post('/api/content/clipboard/paste', data, function(jqXHR) { + $.post('/api/content/clipboard/paste', postData, function(data) { Origin.editor.clipboardId = null; - Origin.editor.pasteParentModel = null; - Origin.trigger('editor:refreshData', function() { - /** - * FIXME views should handle rendering the new data, - * we shouldn't need to refresh the whole page - */ - Backbone.history.loadUrl(); - }, this); + Origin.trigger('editorView:pasted:' + postData.parentId, { + _id: data._id, + sortOrder: postData.sortOrder + }); }).fail(function(jqXHR, textStatus, errorThrown) { Origin.Notify.alert({ type: 'error', @@ -369,24 +342,19 @@ define(function(require){ }, renderEditorPage: function() { - var view = new EditorPageView({ - model: Origin.editor.data.contentObjects.findWhere({ _id: this.currentPageId }) - }); - this.$('.editor-inner').html(view.$el); - }, - - cutContent: function(view) { - var type = helpers.capitalise(view.model.get('_type')); - var collectionType = view.model._siblings; - - this.addToClipboard(view.model); - - // Remove model from collection (to save fetching) and destroy it - Origin.editor.data[collectionType].remove(view.model); - view.model.destroy(); - - _.defer(function () { - Origin.trigger('editorView:cut' + type + ':' + view.model.get('_parentId'), view); + (new ContentObjectModel({ + _id: this.currentPageId + })).fetch({ + success: function(model) { + var view = new EditorPageView({ model: model }); + this.$('.editor-inner').html(view.$el); + }, + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } }); }, diff --git a/frontend/src/modules/modal/less/modal.less b/frontend/src/modules/modal/less/modal.less index 0e485c3a72..18c1dd0afe 100644 --- a/frontend/src/modules/modal/less/modal.less +++ b/frontend/src/modules/modal/less/modal.less @@ -31,10 +31,10 @@ width:100%; height:100%; z-index:4000; + overflow-y: scroll; .modal-popup { background-color: @ui-content-color; height: 100%; - overflow-y: scroll; } i { diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index 0d8683dca8..a5106b1cc5 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -1,10 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - var Origin = require('core/origin'); var ModalView = Backbone.View.extend({ - className: 'modal', events: { @@ -13,22 +11,26 @@ define(function(require) { }, initialize: function(options) { - var defaults = { + this.view = options.view; + this.options = _.extend({ _shouldShowCancelButton: true, _shouldShowDoneButton: true, + _shouldShowScrollbar: true, _shouldDisableCancelButton: false, _shouldDisableDoneButton: false - } - this.view = options.view; - this.options = _.extend(defaults, options.options); + }, options.options); this.context = options.context; - this.listenTo(Origin, 'remove:views', this.remove); - this.listenTo(Origin, 'modal:onCancel', this.onCloseButtonClicked); - this.listenTo(Origin, 'modal:onUpdate', this.onDoneButtonClicked); - this.listenTo(Origin, 'modal:disableCancelButton', this.onCloseButtonDisabled); - this.listenTo(Origin, 'modal:disableDoneButton', this.onDoneButtonDisabled); - this.listenTo(Origin, 'modal:enableCancelButton', this.onCloseButtonEnabled); - this.listenTo(Origin, 'modal:enableDoneButton', this.onDoneButtonEnabled); + + this.listenTo(Origin, { + 'remove:views': this.remove, + 'modal:onCancel': this.onCloseButtonClicked, + 'modal:onUpdate': this.onDoneButtonClicked, + 'modal:disableCancelButton': this.onCloseButtonDisabled, + 'modal:disableDoneButton': this.onDoneButtonDisabled, + 'modal:enableCancelButton': this.onCloseButtonEnabled, + 'modal:enableDoneButton': this.onDoneButtonEnabled + }); + this.render(); }, @@ -37,7 +39,7 @@ define(function(require) { var template = Handlebars.templates['modal']; this.$el.html(template(data)).appendTo('body'); _.defer(_.bind(this.postRender, this)); - + return this; }, @@ -48,6 +50,9 @@ define(function(require) { if (this.options._shouldDisableDoneButton) { this.onDoneButtonDisabled(); } + if(this.options._shouldShowScrollbar === false) { + this.$el.css('overflow-y', 'hidden'); + } this.modalView = new this.view(this.options); this.$('.modal-popup-content-inner').append(this.modalView.$el); $('html').addClass('no-scroll'); diff --git a/frontend/src/modules/projects/less/projects.less b/frontend/src/modules/projects/less/projects.less index d336974225..bdfbd872f4 100644 --- a/frontend/src/modules/projects/less/projects.less +++ b/frontend/src/modules/projects/less/projects.less @@ -38,7 +38,7 @@ ul.projects-options { padding: 30px 30px 0px 0px; } -.grid-layout { +.projects-list[data-layout=grid] { .project-list-item, .shared-project-list-item { margin-left: 25px; @@ -74,7 +74,7 @@ ul.projects-options { } } -.list-layout { +.projects-list[data-layout=list] { margin-left:30px; .project-list-item, diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index db30d099ab..e474dfc50e 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -185,21 +185,6 @@ .project-detail-title { margin-bottom:10px; display: inline-block; - - .project-detail-title-inner { - h4 { - height: 36px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - - & span.highlighted { - background-color: #ff0; - } - } - } } .project-details-row { @@ -212,8 +197,3 @@ margin-bottom:4px; } } - -.shared-projects-details-label { - color:#9d9d9d; - font-weight: 600; -} 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}}
    diff --git a/frontend/src/modules/projects/views/projectView.js b/frontend/src/modules/projects/views/projectView.js index 9ab4a47f30..406bacd2e5 100644 --- a/frontend/src/modules/projects/views/projectView.js +++ b/frontend/src/modules/projects/views/projectView.js @@ -76,14 +76,22 @@ define(function(require) { deleteProjectPrompt: function(event) { event && event.preventDefault(); if(this.model.get('_isShared') === true) { - Origin.Notify.confirm({ - type: 'warning', - title: Origin.l10n.t('app.deletesharedproject'), - text: Origin.l10n.t('app.confirmdeleteproject') + '

    ' + Origin.l10n.t('app.confirmdeletesharedprojectwarning'), - destructive: true, - callback: _.bind(this.deleteProjectConfirm, this) - }); + if(this.model.get('createdBy') === Origin.sessionModel.id){ + Origin.Notify.confirm({ + type: 'warning', + title: Origin.l10n.t('app.deletesharedproject'), + text: Origin.l10n.t('app.confirmdeleteproject') + '

    ' + Origin.l10n.t('app.confirmdeletesharedprojectwarning'), + destructive: true, + callback: _.bind(this.deleteProjectConfirm, this) + }); + } else { + Origin.Notify.alert({ + type: 'error', + text: Origin.l10n.t('app.errorpermission') + }); + } return; + } Origin.Notify.confirm({ type: 'warning', diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index e6ca357a4c..a6ea6a645b 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -1,7 +1,5 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var OriginView = require('core/views/originView'); var ProjectView = require('./projectView'); @@ -9,142 +7,85 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', - settings: { - autoRender: true, - preferencesKey: 'dashboard' + supportedLayouts: [ + "grid", + "list" + ], + + postRender: function() { + this.settings.preferencesKey = 'dashboard'; + this.initUserPreferences(); + this.initEventListeners(); + this.initPaging(); }, - preRender: function(options) { - this.setupFilterSettings(); + initEventListeners: function() { + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 250), this); this.listenTo(Origin, { - 'window:resize': this.resizeDashboard, - 'dashboard:layout:grid': this.switchLayoutToGrid, - 'dashboard:layout:list': this.switchLayoutToList, - 'dashboard:dashboardSidebarView:filterBySearch': this.filterBySearchInput, - 'dashboard:dashboardSidebarView:filterByTags': this.filterCoursesByTags, - 'dashboard:sidebarFilter:add': this.addTag, - 'dashboard:sidebarFilter:remove': this.removeTag, - // These need to pass in true to re-render the collections - 'dashboard:sort:asc': function() { this.sortAscending(true); }, - 'dashboard:sort:desc': function() { this.sortDescending(true); }, - 'dashboard:sort:updated': function() { this.sortLastUpdated(true); } + 'window:resize': this._onResize, + 'dashboard:dashboardSidebarView:filterBySearch': function(text) { this.doFilter(text) }, + 'dashboard:dashboardSidebarView:filterByTags': function(tags) { this.doFilter(null, tags) }, + 'dashboard:sort:asc': function() { this.doSort('asc'); }, + 'dashboard:sort:desc': function() { this.doSort('desc'); }, + 'dashboard:sort:updated': function() { this.doSort('updated'); } }); - this.listenTo(this.collection, { - 'add': this.appendProjectItem, - 'sync': this.checkIfCollectionIsEmpty - }); - }, + this.supportedLayouts.forEach(function(layout) { + this.listenTo(Origin, 'dashboard:layout:' + layout, function() { this.doLayout(layout); }); + }, this); - setupFilterSettings: function() { - // Setup filtering and lazy loading settings - this.sort = {createdAt: -1}; - this.search = {}; - this.courseLimit = -32; - this.courseDenominator = 32; - // Set empty filters - this.filters = []; - this.tags = []; - - this.collectionLength = 0; - this.shouldStopFetches = false; + this.listenTo(this.collection, 'add', this.appendProjectItem); - // set relevant filters as selected - $("a[data-callback='dashboard:layout:grid']").addClass('selected'); - $("a[data-callback='dashboard:sort:asc']").addClass('selected'); - }, - - resizeDashboard: function() { - var navigationHeight = $('.navigation').outerHeight(); - var locationTitleHeight = $('.location-title').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); - this.$el.css('height', actualHeight); - }, - - checkIfCollectionIsEmpty: function() { - this.$('.no-projects').toggleClass('display-none', this.collection.length > 0); - }, - - postRender: function() { - this.setupUserPreferences(); - - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - this.getProjectsContainer().trigger('scroll'); - this.lazyRenderCollection(); - this.resizeDashboard(); - this.setViewToReady(); - this.setupLazyScrolling(); + $('.contentPane').on('scroll', this._doLazyScroll); }, - switchLayoutToList: function() { - this.getProjectsContainer().removeClass('grid-layout').addClass('list-layout'); - this.setUserPreference('layout','list'); - }, + initUserPreferences: function() { + var prefs = this.getUserPreferences(); - switchLayoutToGrid: function() { - this.getProjectsContainer().removeClass('list-layout').addClass('grid-layout'); - this.setUserPreference('layout','grid'); + this.doLayout(prefs.layout); + this.doSort(prefs.sort, false); + this.doFilter(prefs.search, prefs.tags, false); + // set relevant filters as selected + $("a[data-callback='dashboard:layout:" + prefs.layout + "']").addClass('selected'); + $("a[data-callback='dashboard:sort:" + prefs.sort + "']").addClass('selected'); + // need to refresh this to get latest filters + prefs = this.getUserPreferences(); + Origin.trigger('options:update:ui', prefs); + Origin.trigger('sidebar:update:ui', prefs); }, - sortAscending: function(shouldRenderProjects) { - this.sort = { title: 1 }; - this.setUserPreference('sort','asc'); - if(shouldRenderProjects) this.updateCollection(true); - }, + // Set some default preferences + getUserPreferences: function() { + var prefs = OriginView.prototype.getUserPreferences.apply(this, arguments); - sortDescending: function(shouldRenderProjects) { - this.sort = { title: -1 }; - this.setUserPreference('sort','desc'); - if(shouldRenderProjects) this.updateCollection(true); - }, + if(!prefs.layout) prefs.layout = 'grid'; + if(!prefs.sort) prefs.sort = 'asc'; - sortLastUpdated: function(shouldRenderProjects) { - this.sort = { updatedAt: -1 }; - this.setUserPreference('sort','updated'); - if (shouldRenderProjects) this.updateCollection(true); + return prefs; }, - setupUserPreferences: function() { - // Preserve the user preferences or display default mode - var userPreferences = this.getUserPreferences(); - // Check if the user preferences are list view - // Else if nothing is set or is grid view default to grid view - if (userPreferences && userPreferences.layout === 'list') { - this.switchLayoutToList(); - } else { - this.switchLayoutToGrid(); + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; } - // Check if there's any user preferences for search and tags - // then set on this view - if (userPreferences) { - var searchString = (userPreferences.search || ''); - this.search = this.convertFilterTextToPattern(searchString); - this.setUserPreference('search', searchString); - this.tags = (_.pluck(userPreferences.tags, 'id') || []); - this.setUserPreference('tags', userPreferences.tags); - } - // Check if sort is set and sort the collection - if (userPreferences && userPreferences.sort === 'desc') { - this.sortDescending(); - } else if (userPreferences && userPreferences.sort === 'updated') { - this.sortLastUpdated(); - } else { - this.sortAscending(); - } - // Once everything has been setup - // refresh the userPreferences object - userPreferences = this.getUserPreferences(); - // Trigger event to update options UI - Origin.trigger('options:update:ui', userPreferences); - Origin.trigger('sidebar:update:ui', userPreferences); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.courseLimit += this.courseDenominator; - this.updateCollection(false); + // we need to load one course first to check page size + this.pageSize = 1; + this.resetCollection(_.bind(function(collection) { + var containerHeight = $(window).height()-this.$el.offset().top; + var containerWidth = this.$('.projects-inner').width(); + var itemHeight = $('.project-list-item').outerHeight(true); + var itemWidth = $('.project-list-item').outerWidth(true); + var columns = Math.floor(containerWidth/itemWidth); + var rows = Math.floor(containerHeight/itemHeight); + // columns stack nicely, but need to add extra row if it's not a clean split + if((containerHeight % itemHeight) > 0) rows++; + this.pageSize = columns*rows; + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); + }, this)); }, getProjectsContainer: function() { @@ -152,130 +93,113 @@ define(function(require){ }, emptyProjectsContainer: function() { - // Trigger event to kill zombie views Origin.trigger('dashboard:dashboardView:removeSubViews'); - // Empty collection container this.getProjectsContainer().empty(); }, - updateCollection: function(reset) { - // If removing items, we need to reset our limits - if (reset) { - // Empty container - this.emptyProjectsContainer(); - // Reset fetches cache - this.shouldStopFetches = false; - this.courseLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } - this.search = _.extend(this.search, { tags: { $all: this.tags } }); - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { + appendProjectItem: function(model) { + var viewClass = model.isEditable() ? ProjectView : SharedProjectView; + this.getProjectsContainer().append(new viewClass({ model: model }).$el); + }, + + convertFilterTextToPattern: function(filterText) { + var pattern = '.*' + filterText.toLowerCase() + '.*'; + return { title: pattern }; + }, + + resetCollection: function(cb) { + this.emptyProjectsContainer(); + this.fetchCount = 0; + this.shouldStopFetches = false; + this.collection.reset(); + this.fetchCollection(cb); + }, + + fetchCollection: function(cb) { + if(this.shouldStopFetches) { return; } + this.isCollectionFetching = true; this.collection.fetch({ - remove: reset, data: { - search: this.search, + search: _.extend(this.search, { tags: { $all: this.tags } }), operators : { - skip: this.courseLimit, - limit: this.courseDenominator, + skip: this.fetchCount, + limit: this.pageSize, sort: this.sort } }, - success: _.bind(function(data) { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } + success: _.bind(function(collection, response) { this.isCollectionFetching = false; + this.fetchCount += response.length; + // stop further fetching if this is the last page + if(response.length < this.pageSize) this.shouldStopFetches = true; + + this.$('.no-projects').toggleClass('display-none', this.fetchCount > 0); + if(typeof cb === 'function') cb(collection); }, this) }); }, - appendProjectItem: function(projectModel) { - projectModel.attributes.title=this.highlight(projectModel.attributes.title) - - if (!projectModel.isEditable()) { - this.getProjectsContainer().append(new SharedProjectView({ model: projectModel }).$el); - } else { - this.getProjectsContainer().append(new ProjectView({ model: projectModel }).$el); + doLazyScroll: function(e) { + if(this.isCollectionFetching) { + return; } + var $el = $(e.currentTarget); + var pxRemaining = this.getProjectsContainer().height() - ($el.scrollTop() + $el.height()); + // we're at the bottom, fetch more + if (pxRemaining <= 0) this.fetchCollection(); }, - highlight: function(text) { - var search = this.getUserPreferences().search || ''; - // replace special characters: .*+?|()[]{}\$^ - search.replace(/[.*+?|()\[\]{}\\$^]/g, "\\$&"); - // add the span - return text.replace(new RegExp(search, "gi"), function(term) { - return '' + term + ''; - }); - }, - - addTag: function(filterType) { - // add filter to this.filters - this.tags.push(filterType); - this.filterCollection(); + doLayout: function(layout) { + if(this.supportedLayouts.indexOf(layout) === -1) { + return; + } + this.getProjectsContainer().attr('data-layout', layout); + this.setUserPreference('layout', layout); + }, + + doSort: function(sort, fetch) { + switch(sort) { + case "desc": + this.sort = { title: -1 }; + break; + case "updated": + this.sort = { updatedAt: -1 }; + break; + case "asc": + default: + sort = "asc"; + this.sort = { title: 1 }; + } + this.setUserPreference('sort', sort); + if(fetch !== false) this.resetCollection(); }, - removeTag: function(filterType) { - // remove filter from this.filters - this.tags = _.filter(this.tags, function(item) { return item != filterType; }); - this.filterCollection(); - }, + doFilter: function(text, tags, fetch) { + text = text || ''; + this.filterText = text; + this.search = this.convertFilterTextToPattern(text); + this.setUserPreference('search', text, true); - filterCollection: function() { - this.search.tags = this.tags.length - ? { $all: this.tags } - : null ; - this.updateCollection(true); - }, + tags = tags || []; + this.tags = _.pluck(tags, 'id'); + this.setUserPreference('tags', tags, true); - convertFilterTextToPattern: function(filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - return { title: pattern}; + if(fetch !== false) this.resetCollection(); }, - filterBySearchInput: function (filterText) { - this.filterText = filterText; - this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); - this.updateCollection(true); + onResize: function() { + this.initPaging(); }, - filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); - this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); - }, + remove: function() { + $('.contentPane').off('scroll', this._doLazyScroll); - setupLazyScrolling: function() { - var $projectContainer = $('.projects'); - var $projectContainerInner = $('.projects-inner'); - // Remove event before attaching - $projectContainer.off('scroll'); - - $projectContainer.on('scroll', _.bind(function() { - var scrollTop = $projectContainer.scrollTop(); - var scrollableHeight = $projectContainerInner.height(); - var containerHeight = $projectContainer.height(); - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } - } - }, this)); + OriginView.prototype.remove.apply(this, arguments); } + }, { template: 'projects' }); diff --git a/frontend/src/modules/scaffold/backboneFormsOverrides.js b/frontend/src/modules/scaffold/backboneFormsOverrides.js index bf18e15b68..522930be80 100644 --- a/frontend/src/modules/scaffold/backboneFormsOverrides.js +++ b/frontend/src/modules/scaffold/backboneFormsOverrides.js @@ -94,6 +94,33 @@ define(function(require) { return parts.join('
    '); }; + Backbone.Form.editors.List.prototype.removeItem = function(item) { + //Confirm delete + var confirmMsg = this.schema.confirmDelete; + + var remove = _.bind(function(isConfirmed) { + if (isConfirmed === false) return; + + var index = _.indexOf(this.items, item); + + this.items[index].remove(); + this.items.splice(index, 1); + + if (item.addEventTriggered) { + this.trigger('remove', this, item.editor); + this.trigger('change', this); + } + + if (!this.items.length && !this.Editor.isAsync) this.addItem(); + }, this); + + if (confirmMsg) { + window.confirm({ title: confirmMsg, type: 'warning', callback: remove }); + } else { + remove(); + } + }; + // Used to setValue with defaults Backbone.Form.editors.Base.prototype.setValue = function(value) { diff --git a/frontend/src/modules/scaffold/index.js b/frontend/src/modules/scaffold/index.js index 3f216bf134..4da27f8c2c 100644 --- a/frontend/src/modules/scaffold/index.js +++ b/frontend/src/modules/scaffold/index.js @@ -74,7 +74,8 @@ define(function(require) { itemType: 'Object', subSchema: field.items.properties, confirmDelete: Origin.l10n.t('app.confirmdelete'), - fieldType: 'List' + fieldType: 'List', + help: field.help } } @@ -96,7 +97,8 @@ define(function(require) { type: 'List', itemType:field.items.inputType, subSchema: field.items, - fieldType: field.items.inputType + fieldType: field.items.inputType, + help: field.help } } } diff --git a/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs index 73e5b4e089..2f7da0df5d 100644 --- a/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs +++ b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs @@ -1,89 +1,65 @@ {{#if value}} + {{! Work out which type it is and display the correct html markup}} -{{! Work out which type it is and display the correct html markup}} -{{#ifValueEquals type 'image'}} - {{#ifAssetIsExternal value}} - - {{else}} - {{#ifAssetIsHeroImage value}} - + {{#ifValueEquals type 'image'}} + {{#if thumbUrl}} + {{else}} - {{#if thumbnailPath}} - - {{else}} - {{#ifImageIsCourseAsset value}} - - {{else}} - - - - {{/ifImageIsCourseAsset}} - {{/if}} - {{/ifAssetIsHeroImage}} - {{/ifAssetIsExternal}} -{{/ifValueEquals}} + + + + {{/if}} + {{/ifValueEquals}} -{{#ifValueEquals type 'other'}} - {{#ifAssetIsExternal value}} - - -
    {{value}}
    -
    - {{else}} - - -
    {{value}}
    -
    - {{/ifAssetIsExternal}} + {{#ifValueEquals type 'other'}} + + +
    {{value}}
    +
    + {{/ifValueEquals}} -{{/ifValueEquals}} - -{{#ifValueEquals type 'video'}} - {{#ifAssetIsExternal value}} - - -
    {{value}}
    -
    - {{else}} - - {{/ifAssetIsExternal}} -{{/ifValueEquals}} - -{{#ifValueEquals type 'audio'}} - {{#ifAssetIsExternal value}} - - -
    {{value}}
    -
    - {{else}} - - -
    {{value}}
    -
    - {{/ifAssetIsExternal}} + {{#ifValueEquals type 'video'}} + {{#ifAssetIsExternal value}} + + +
    {{value}}
    +
    + {{else}} + + {{/ifAssetIsExternal}} + {{/ifValueEquals}} -{{/ifValueEquals}} + {{#ifValueEquals type 'audio'}} + + +
    {{value}}
    +
    + {{/ifValueEquals}} -
    - {{#ifAssetIsExternal value}} - - {{else}} - - {{/ifAssetIsExternal}} -
    +
    + +
    {{else}} -